[SOLVED] Virtual List + Ajax + search


#1

I cannot for the life of me seem to get the search to work with a Virtual List that is populated via AJAX - I can only ever get partial functionality - either the search works but the list cannot be repopulated, or the list can be repopulated but the search doesn’t work. Here is the version where the search does not work, but everything else does. Does anybody have any ideas?

	<template id="page-vehicle-list">
	<f7-page>
		<f7-navbar>
			<f7-nav-left>
				<f7-link icon-if-ios="f7:menu" icon-if-md="material:menu" panel-open="left"></f7-link>
			</f7-nav-left>
			<f7-nav-title>Inspections</f7-nav-title>
			<f7-subnavbar :inner="false">
			<f7-searchbar
				search-container=".virtual-list"
				search-item="li"
				search-in=".item-title"
			></f7-searchbar>
			</f7-subnavbar>
		</f7-navbar>
		<f7-list class="searchbar-not-found">
			<f7-list-item title="Nothing found"></f7-list-item>
		</f7-list>

		<div class="error" v-if="error">
  			Error: {{ error }}
		</div>
		
		<f7-list
			class="searchbar-found"
			media-list
			virtual-list
			v-if="items.length" 
			:virtual-list-params="{ items, searchAll, renderExternal, height: $theme.ios ? 63 : 73}" 
		>
			<ul>
				<f7-list-item 
					v-for="(item, index) in vlData.items" 
					:key="index" 
					media-item 
					:link="`/vehicle/` + item.id" 
					:title="item.registration" 
					:subtitle="[item.manufacturer, item.model, item.variant].join(' ')" 
					:style="`top: ${vlData.topPosition}px`">
				</f7-list-item>
			</ul>
		</f7-list>
	</f7-page>
</template>

Vue.component('page-vehicle-list', {
template: '#page-vehicle-list',
data() {
	return {
		items : [],
		vlData: {
			items: [],
		},
		error: null
	};
},
created: function() {
	// fetch the data when the view is created and the data is
	// already being observed		
	this.fetchData();	
},
watch: {
	// call again the method if the route changes
	'$route': 'fetchData'	
},
methods: {
	searchAll(query, items) {
		const found = [];
		for (let i = 0; i < items.length; i += 1) {
			
			var vehiclestr = [items[i].manufacturer, items[i].model, items[i].variant].join(' ');
			
			if ((items[i].registration &&items[i].registration.toLowerCase().indexOf(query.toLowerCase()) >= 0) 
				|| (vehiclestr && vehiclestr.toLowerCase().indexOf(query.toLowerCase()) >= 0)   
				|| query.trim() === '') found.push(i);
		}
		return found; // return array with matched indexes
	},
	renderExternal(vl, vlData) {
		this.vlData = vlData;
	},
	fetchData() {
		
		const self = this;
		
		self.$f7.preloader.show();
		self.error = null;
		self.vehicles = [];

		app.ajax({
			url: config.motorfinancecollections_api_base_url + '/vehicles',
			data: {
				username: localStorage.getItem('username'),
				usertoken: localStorage.getItem('usertoken')
			},
			success: function(data, status, xhr) {
				app.vehicles = data;
				self.items = app.vehicles;
				self.$f7.preloader.hide();
			},
			error: function(xhr, status) {
				// error
				alert('Sorry, the vehicle list is unavailable.');
			},
			store: true
		});
	}
}
});

#2

If I remove v-if=“items.length” from the f7-list and add:

self.$f7.virtualList.get().replaceAllItems(self.items)

to the AJAX success callback - search works - but the list fails to redraw after a second AJAX request.


#3

replaceAllItems is the correct method. I just tried and it works fine for me. Here is the working modified example from Kitchen Sink:

<template>
  <f7-page>
    <f7-navbar title="Virtual List" back-link="Back">
      <f7-subnavbar :inner="false">
        <f7-searchbar
          search-container=".virtual-list"
          search-item="li"
          search-in=".item-title"
        ></f7-searchbar>
      </f7-subnavbar>
    </f7-navbar>
    <f7-block>
      <p>Virtual List allows to render lists with huge amount of elements without loss of performance. And it is fully compatible with all Framework7 list components such as Search Bar, Infinite Scroll, Pull To Refresh, Swipeouts (swipe-to-delete) and Sortable.</p>
      <p>Here is the example of virtual list with 10 000 items:</p>
    </f7-block>
    <f7-list class="searchbar-not-found">
      <f7-list-item title="Nothing found"></f7-list-item>
    </f7-list>
    <f7-list
      class="searchbar-found"
      ref="f7List"
      medial-list
      virtual-list
      :virtual-list-params="{ items, searchAll, renderExternal, height: $theme.ios ? 63 : 73}"
    >
      <ul>
        <f7-list-item
          v-for="(item, index) in vlData.items"
          :key="index"
          media-item
          link="#"
          :title="item.title"
          :subtitle="item.subtitle"
          :style="`top: ${vlData.topPosition}px`"
        ></f7-list-item>
      </ul>
    </f7-list>
    <f7-block>
      <a @click="addItems">Add items</a>
    </f7-block>
  </f7-page>
</template>
<script>
  import { f7Navbar, f7Page, f7List, f7ListItem, f7Subnavbar, f7Searchbar, f7Block } from 'framework7-vue';

  export default {
    components: {
      f7Navbar, f7Page, f7List, f7ListItem, f7Subnavbar, f7Searchbar, f7Block,
    },
    data() {
      const items = [];
      for (let i = 1; i <= 100; i += 1) {
        items.push({
          title: `Item ${i}`,
          subtitle: `Subtitle ${i}`,
        });
      }
      return {
        items,
        vlData: {
          items: [],
        },
      };
    },
    methods: {
      addItems() {
        const last = this.items.length;
        const items = [];
        for (let i = last + 1; i <= last + 100; i += 1) {
          items.push({
            title: `Item ${i}`,
            subtitle: `Subtitle ${i}`,
          });
        }
        this.items = [...this.items, ...items];
        this.$refs.f7List.f7VirtualList.replaceAllItems(this.items)
      },
      searchAll(query, items) {
        const found = [];
        for (let i = 0; i < items.length; i += 1) {
          if (items[i].title.toLowerCase().indexOf(query.toLowerCase()) >= 0 || query.trim() === '') found.push(i);
        }
        return found; // return array with mathced indexes
      },
      renderExternal(vl, vlData) {
        this.vlData = vlData;
      },
    },
  };
</script>

#4

There’s something funny going on here… If we take the version where the search was working, and change the links on the next page (the item detail pages) to be be back links via class=“back” rather than explicitly going (back) to the vehicle-list page, everything seems to work. In isolation, your code may work, but there is something causing problems elsewhere - sorry to be so vague - I am considering abandoning the virtual list component and using a normal list instead, so i can have more understanding of what is actually happening… at what point does vlData get populated, for example? In the broken code, when the virtual list was populated for a second time, this is ending up empty.

Thank you for your response and apologies I can’t be more specific about what the issue may be - I have tried to narrow it down but with no luck!

One thing I did notice, in the chrome vue debugger, is that in the broken version, a second virtual list element was ending up in the “vue DOM”.


#5

I’ve got a bit further into tracking down this problem…

The $foundEl and $searchContainer properties of the Searchbar class are ending up with multiple items in them - so,

sb.virtualList = $searchContainer[0].f7VirtualList;

is failing to fetch the virual list (DOM element has been replaced when we go back to this view)?

sb.virtualList = $searchContainer[$searchContainer.length - 1].f7VirtualList;

fixes the problem - but I feel uncomfortable hacking away at the core code - is this a bug? do you understand what might be happening? It happens when the template that contains the virtual list/search bar is reloaded by navigating to it with the router - when we go back rather than going to it directly, there are no problems.


#6

How can they be multiple? In this case you need to specify more strict selector for Searchbar’s searchContainer and foundEl parameters to point only on specific elements


#7

The virtual list and search bar are created from the f7-searchbar and f7-list (with virtual list attr) tags respectively in a template - the problem is occurring when this template is loaded for a second time (sometimes, this template stays on the “stack” and the problem doesn’t happen)… My selector is .virtual-list but I think even if it was #my-virtual-list I would have the same problem - the reference to the DOM element is still in these arrays, even though it doesn’t exist anymore. Weirdly, the length of the arrays seems to fluctuate between one and two elements - it doesn’t just keep growing every time I revisit the template containing the virtual list.

Note - this is the only virtual list in my project - it’s in that one template (the “home page”) which we can go back to later via the router, for example, when we click an item, make some edits and save it).


#8

To clarify, app structure is something like this…

vehicle list > vehicle details

when we go back from vehicle details via a link with class=“back” it’s fine, when we navigate back to the vehicle list via the router (after a successful save) the problem sometimes happens.


#9

Making more strict selector is required if you may have few pages at a time with same Vl+Searchbar selectors. It can be not just a string but HTMLElement, like $(currentPage).find('.virtual-list') to explicity set in which DOM element it should do the search. In Vue you can get such elements via setting ref on VL list


#10

Thank you, what exactly should I replace?

search-container=".virtual-list"

with?

:search-container="$$('.page-current .virtual-list')" doesn’t seem to work (search fails without errors).

$$('.page-current .virtual-list') finds the virtual list, in devtools.


#11

Also tried using refs, with no luck!

					<f7-searchbar
						:search-container="$refs['mylist']"
						search-item="li"
						search-in=".item-title" 
						@change="searchTerm = $event.target.value"
					></f7-searchbar>

			<f7-list
				class="searchbar-found"
				media-list
				virtual-list
				:virtual-list-params="{ items, searchAll, renderExternal, height: $theme.ios ? 63 : 73}" 
				ref="mylist"
			>

#13

Using specific IDs seems to work! Thanks for your help!

    				<f7-searchbar
						:search-container="`#virtual-list-` + _uid"
						search-item="li"
						search-in=".item-title" 
						@change="searchTerm = $event.target.value"
					></f7-searchbar>
				<f7-list
					class="searchbar-found"
					media-list
					virtual-list
					:virtual-list-params="{ items, searchAll, renderExternal, height: $theme.ios ? 63 : 73}" 
					:id="`virtual-list-` + _uid"
				>