Skip to content

Commit 7a263c2

Browse files
balvigclaudeNicolasCARPi
authored
Fix virtual_scroll losing preloaded options after search (#998)
* Fix virtual_scroll losing preloaded options after search When using virtual_scroll with preload: true, searching and then clearing the search (or closing the dropdown) would show a mix of stale search results and HTML-only options, losing all preloaded data. Two issues: 1. default_values is set on initialize before preload fires, so it only captures HTML <option> elements. When clearOptions(clearFilter) runs during a search, preloaded options are discarded. 2. Pagination state for the empty query is lost when getUrl() calls clearPagination() for a new search query, so virtual scroll stops working after clearing the search. Fix: update default_values after the first loadCallback for the empty query to include preloaded options, and save/restore the empty-query pagination state when clearing search or closing the dropdown. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Put conditionals in brackets --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Nicolas CARPi <3043706+NicolasCARPi@users.noreply.github.com>
1 parent 2aabbdd commit 7a263c2

2 files changed

Lines changed: 119 additions & 0 deletions

File tree

src/plugins/virtual_scroll/plugin.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export default function(this:TomSelect) {
2828
var loading_more = false;
2929
var load_more_opt:HTMLElement;
3030
var default_values: string[] = [];
31+
var default_values_loaded = false;
32+
var default_pagination:any;
3133

3234
if( !self.settings.shouldLoadMore ){
3335

@@ -147,6 +149,16 @@ export default function(this:TomSelect) {
147149

148150
orig_loadCallback.call( self, options, optgroups);
149151

152+
// After the initial preload (empty query), update default_values to include
153+
// preloaded options, not just the HTML <option> elements captured on initialize
154+
if( !loading_more && !default_values_loaded ){
155+
default_values_loaded = true;
156+
if( self.lastValue === '' ){
157+
default_values = Object.keys(self.options);
158+
default_pagination = pagination[''];
159+
}
160+
}
161+
150162
loading_more = false;
151163
});
152164

@@ -191,6 +203,26 @@ export default function(this:TomSelect) {
191203
});
192204

193205

206+
// Restore preloaded options and pagination when clearing search
207+
const restoreDefaults = ():void => {
208+
if( !default_values_loaded ) {
209+
return;
210+
}
211+
self.clearOptions(clearFilter);
212+
if( default_pagination ) {
213+
pagination[''] = default_pagination;
214+
}
215+
};
216+
217+
self.on('type',(query:string) => {
218+
if( query === '' ){
219+
restoreDefaults();
220+
self.refreshOptions(false);
221+
}
222+
});
223+
224+
self.on('dropdown_close', restoreDefaults);
225+
194226
// add scroll listener and default templates
195227
self.on('initialize',()=>{
196228
default_values = Object.keys(self.options);

test/tests/plugins/virtual_scroll.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,93 @@ describe('plugin: virtual_scroll', function() {
164164
});
165165

166166

167+
it_n('restore preloaded options after search is cleared',async ()=>{
168+
169+
var load_calls = 0;
170+
171+
var test = setup_test('<input>',{
172+
plugins:['virtual_scroll'],
173+
labelField: 'value',
174+
valueField: 'value',
175+
searchField: 'value',
176+
preload: true,
177+
loadThrottle: 1,
178+
firstUrl: function(query){
179+
return [query,0];
180+
},
181+
load: function(query, callback) {
182+
load_calls++;
183+
var url_params = this.getUrl(query);
184+
var data = DataProvider(url_params[0],url_params[1]);
185+
this.setNextUrl(query,[query,url_params[1]+1]);
186+
callback(data.data);
187+
}
188+
});
189+
190+
// wait for preload to complete
191+
await waitFor(100);
192+
assert.equal( Object.keys(test.instance.options).length, 20, 'should have preloaded options');
193+
assert.equal( load_calls, 1);
194+
195+
// search for "a"
196+
await asyncClick(test.instance.control);
197+
await asyncType('a');
198+
await waitFor(100);
199+
assert.equal( load_calls, 2);
200+
201+
// clear search - preloaded options should be restored
202+
await asyncType('\b');
203+
await waitFor(100);
204+
assert.equal( Object.keys(test.instance.options).length, 20, 'should restore preloaded options after clearing search');
205+
206+
// verify none of the search results leaked through
207+
var option_keys = Object.keys(test.instance.options);
208+
var has_search_results = option_keys.some(k => k.startsWith('a-'));
209+
assert.isFalse( has_search_results, 'should not contain stale search results');
210+
});
211+
212+
213+
it_n('virtual scroll works after clearing search',async ()=>{
214+
215+
var load_calls = 0;
216+
217+
var test = setup_test('<input>',{
218+
plugins:['virtual_scroll'],
219+
labelField: 'value',
220+
valueField: 'value',
221+
searchField: 'value',
222+
preload: true,
223+
loadThrottle: 1,
224+
firstUrl: function(query){
225+
return [query,0];
226+
},
227+
load: function(query, callback) {
228+
load_calls++;
229+
var url_params = this.getUrl(query);
230+
var data = DataProvider(url_params[0],url_params[1]);
231+
this.setNextUrl(query,[query,url_params[1]+1]);
232+
callback(data.data);
233+
}
234+
});
235+
236+
// wait for preload
237+
await waitFor(100);
238+
assert.equal( load_calls, 1);
239+
240+
// search and clear
241+
await asyncClick(test.instance.control);
242+
await asyncType('a');
243+
await waitFor(100);
244+
await asyncType('\b');
245+
await waitFor(100);
246+
247+
// scroll to bottom - should trigger loading more
248+
test.instance.scroll(1000,'auto');
249+
await waitFor(500);
250+
assert.isAbove( Object.keys(test.instance.options).length, 20, 'should load more options after clearing search and scrolling');
251+
});
252+
253+
167254
it_n('keep default options',async ()=>{
168255

169256

0 commit comments

Comments
 (0)