0
# AJAX and Loading States
1
2
Built-in support for asynchronous data loading with loading state management, search event handling, and server-side filtering integration.
3
4
## Capabilities
5
6
### Loading State Management
7
8
Properties and methods for controlling loading indicators and asynchronous operations.
9
10
```javascript { .api }
11
/**
12
* Show loading state indicator
13
* Toggles adding 'loading' class to main wrapper
14
* Useful for controlling UI state during AJAX operations
15
*/
16
loading: Boolean // default: false
17
18
// Data properties (from ajax mixin)
19
data: {
20
/**
21
* Internal mutable loading state
22
* Synced with loading prop but can be controlled independently
23
*/
24
mutableLoading: Boolean // default: false
25
}
26
27
// Methods (from ajax mixin)
28
methods: {
29
/**
30
* Toggle loading state programmatically
31
* Can be called without parameters to toggle current state
32
* @param toggle - Optional boolean to set specific state
33
* @returns Current loading state after toggle
34
*/
35
toggleLoading(toggle?: Boolean): Boolean
36
}
37
```
38
39
### Search Event Integration
40
41
Event system specifically designed for AJAX search implementation.
42
43
```javascript { .api }
44
/**
45
* Emitted whenever search text changes
46
* Provides search text and loading toggle function for AJAX integration
47
* @param searchText - Current search input value
48
* @param toggleLoading - Function to control loading state
49
*/
50
'search': (searchText: String, toggleLoading: Function) => void
51
```
52
53
### AJAX-Related Configuration
54
55
Properties that commonly work together with AJAX functionality.
56
57
```javascript { .api }
58
/**
59
* When false, updating options will not reset selected value
60
* Useful when options are updated via AJAX without losing selection
61
*/
62
resetOnOptionsChange: Boolean | Function // default: false
63
64
/**
65
* Disable filtering when using server-side search
66
* Options should be pre-filtered by server response
67
*/
68
filterable: Boolean // default: true (set to false for server-side filtering)
69
```
70
71
## Usage Examples
72
73
### Basic AJAX Search
74
75
```vue
76
<template>
77
<v-select
78
v-model="selectedUser"
79
:options="searchResults"
80
:loading="isLoading"
81
@search="onSearch"
82
label="name"
83
placeholder="Search users..."
84
/>
85
</template>
86
87
<script>
88
import axios from 'axios';
89
90
export default {
91
data() {
92
return {
93
selectedUser: null,
94
searchResults: [],
95
isLoading: false
96
};
97
},
98
methods: {
99
async onSearch(search, toggleLoading) {
100
if (search.length < 2) {
101
this.searchResults = [];
102
return;
103
}
104
105
toggleLoading(true);
106
107
try {
108
const response = await axios.get('/api/users/search', {
109
params: { q: search }
110
});
111
this.searchResults = response.data;
112
} catch (error) {
113
console.error('Search failed:', error);
114
this.searchResults = [];
115
} finally {
116
toggleLoading(false);
117
}
118
}
119
}
120
};
121
</script>
122
```
123
124
### Debounced AJAX Search
125
126
```vue
127
<template>
128
<v-select
129
v-model="selectedProduct"
130
:options="products"
131
:loading="isSearching"
132
:filterable="false"
133
@search="debouncedSearch"
134
label="name"
135
placeholder="Search products..."
136
/>
137
</template>
138
139
<script>
140
import axios from 'axios';
141
import { debounce } from 'lodash';
142
143
export default {
144
data() {
145
return {
146
selectedProduct: null,
147
products: [],
148
isSearching: false
149
};
150
},
151
created() {
152
// Create debounced search function
153
this.debouncedSearch = debounce(this.performSearch, 300);
154
},
155
methods: {
156
async performSearch(searchText, toggleLoading) {
157
if (searchText.length < 2) {
158
this.products = [];
159
return;
160
}
161
162
this.isSearching = true;
163
164
try {
165
const response = await axios.get('/api/products', {
166
params: {
167
search: searchText,
168
limit: 20
169
}
170
});
171
this.products = response.data.products;
172
} catch (error) {
173
console.error('Product search failed:', error);
174
this.products = [];
175
} finally {
176
this.isSearching = false;
177
}
178
}
179
}
180
};
181
</script>
182
```
183
184
### AJAX with Caching
185
186
```vue
187
<template>
188
<v-select
189
v-model="selectedRepository"
190
:options="repositories"
191
:loading="isLoading"
192
@search="onSearch"
193
label="full_name"
194
placeholder="Search GitHub repositories..."
195
/>
196
</template>
197
198
<script>
199
import axios from 'axios';
200
201
export default {
202
data() {
203
return {
204
selectedRepository: null,
205
repositories: [],
206
isLoading: false,
207
searchCache: new Map(),
208
lastSearch: ''
209
};
210
},
211
methods: {
212
async onSearch(searchText, toggleLoading) {
213
if (searchText.length < 2) {
214
this.repositories = [];
215
return;
216
}
217
218
// Check cache first
219
if (this.searchCache.has(searchText)) {
220
this.repositories = this.searchCache.get(searchText);
221
return;
222
}
223
224
this.isLoading = true;
225
this.lastSearch = searchText;
226
227
try {
228
const response = await axios.get('https://api.github.com/search/repositories', {
229
params: {
230
q: searchText,
231
sort: 'stars',
232
order: 'desc',
233
per_page: 10
234
}
235
});
236
237
const results = response.data.items;
238
239
// Only update if this is still the latest search
240
if (searchText === this.lastSearch) {
241
this.repositories = results;
242
// Cache results
243
this.searchCache.set(searchText, results);
244
}
245
246
} catch (error) {
247
console.error('Repository search failed:', error);
248
if (searchText === this.lastSearch) {
249
this.repositories = [];
250
}
251
} finally {
252
if (searchText === this.lastSearch) {
253
this.isLoading = false;
254
}
255
}
256
}
257
}
258
};
259
</script>
260
```
261
262
### Server-Side Filtering with Pagination
263
264
```vue
265
<template>
266
<v-select
267
v-model="selectedItem"
268
:options="items"
269
:loading="isLoading"
270
:filterable="false"
271
@search="onSearch"
272
@open="loadInitialData"
273
label="title"
274
placeholder="Search with pagination..."
275
>
276
<template #list-footer v-if="hasMoreResults">
277
<div class="load-more">
278
<button @click="loadMore" :disabled="isLoadingMore">
279
{{ isLoadingMore ? 'Loading...' : 'Load More' }}
280
</button>
281
</div>
282
</template>
283
</v-select>
284
</template>
285
286
<script>
287
import axios from 'axios';
288
289
export default {
290
data() {
291
return {
292
selectedItem: null,
293
items: [],
294
isLoading: false,
295
isLoadingMore: false,
296
currentSearch: '',
297
currentPage: 1,
298
hasMoreResults: false
299
};
300
},
301
methods: {
302
async loadInitialData() {
303
if (this.items.length === 0) {
304
await this.performSearch('', 1);
305
}
306
},
307
308
async onSearch(searchText) {
309
this.currentSearch = searchText;
310
this.currentPage = 1;
311
await this.performSearch(searchText, 1, true);
312
},
313
314
async loadMore() {
315
this.currentPage++;
316
await this.performSearch(this.currentSearch, this.currentPage, false);
317
},
318
319
async performSearch(searchText, page, replace = true) {
320
const isFirstPage = page === 1;
321
322
if (replace) {
323
this.isLoading = true;
324
} else {
325
this.isLoadingMore = true;
326
}
327
328
try {
329
const response = await axios.get('/api/items', {
330
params: {
331
q: searchText,
332
page: page,
333
per_page: 20
334
}
335
});
336
337
const newItems = response.data.items;
338
339
if (replace) {
340
this.items = newItems;
341
} else {
342
this.items = [...this.items, ...newItems];
343
}
344
345
this.hasMoreResults = response.data.has_more;
346
347
} catch (error) {
348
console.error('Search failed:', error);
349
if (replace) {
350
this.items = [];
351
}
352
} finally {
353
this.isLoading = false;
354
this.isLoadingMore = false;
355
}
356
}
357
}
358
};
359
</script>
360
361
<style scoped>
362
.load-more {
363
padding: 10px;
364
text-align: center;
365
border-top: 1px solid #e9ecef;
366
}
367
.load-more button {
368
padding: 5px 15px;
369
border: 1px solid #007bff;
370
background: #007bff;
371
color: white;
372
border-radius: 4px;
373
cursor: pointer;
374
}
375
.load-more button:disabled {
376
opacity: 0.6;
377
cursor: not-allowed;
378
}
379
</style>
380
```
381
382
### AJAX with Error Handling
383
384
```vue
385
<template>
386
<div>
387
<v-select
388
v-model="selectedCity"
389
:options="cities"
390
:loading="isLoading"
391
@search="onSearch"
392
label="name"
393
placeholder="Search cities..."
394
>
395
<template #no-options>
396
<div class="no-results">
397
<div v-if="searchError" class="error-message">
398
<p>❌ Search failed: {{ searchError }}</p>
399
<button @click="retryLastSearch">Retry</button>
400
</div>
401
<div v-else-if="hasSearched && cities.length === 0">
402
No cities found for your search.
403
</div>
404
<div v-else>
405
Start typing to search cities...
406
</div>
407
</div>
408
</template>
409
</v-select>
410
411
<div v-if="searchError" class="error-banner">
412
Connection issues detected. Please check your internet connection.
413
</div>
414
</div>
415
</template>
416
417
<script>
418
import axios from 'axios';
419
420
export default {
421
data() {
422
return {
423
selectedCity: null,
424
cities: [],
425
isLoading: false,
426
searchError: null,
427
hasSearched: false,
428
lastSearchText: ''
429
};
430
},
431
methods: {
432
async onSearch(searchText, toggleLoading) {
433
if (searchText.length < 2) {
434
this.cities = [];
435
this.searchError = null;
436
this.hasSearched = false;
437
return;
438
}
439
440
this.lastSearchText = searchText;
441
this.searchError = null;
442
this.isLoading = true;
443
444
try {
445
const response = await axios.get('/api/cities', {
446
params: { q: searchText },
447
timeout: 5000 // 5 second timeout
448
});
449
450
this.cities = response.data;
451
this.hasSearched = true;
452
453
} catch (error) {
454
console.error('City search failed:', error);
455
456
if (error.code === 'ECONNABORTED') {
457
this.searchError = 'Request timed out';
458
} else if (error.response) {
459
this.searchError = `Server error: ${error.response.status}`;
460
} else if (error.request) {
461
this.searchError = 'Network error';
462
} else {
463
this.searchError = 'Unknown error occurred';
464
}
465
466
this.cities = [];
467
this.hasSearched = true;
468
469
} finally {
470
this.isLoading = false;
471
}
472
},
473
474
retryLastSearch() {
475
if (this.lastSearchText) {
476
this.onSearch(this.lastSearchText);
477
}
478
}
479
}
480
};
481
</script>
482
483
<style scoped>
484
.no-results {
485
padding: 20px;
486
text-align: center;
487
}
488
.error-message {
489
color: #dc3545;
490
}
491
.error-message button {
492
margin-top: 10px;
493
padding: 5px 10px;
494
border: 1px solid #dc3545;
495
background: white;
496
color: #dc3545;
497
border-radius: 4px;
498
cursor: pointer;
499
}
500
.error-banner {
501
margin-top: 10px;
502
padding: 10px;
503
background: #f8d7da;
504
color: #721c24;
505
border: 1px solid #f5c6cb;
506
border-radius: 4px;
507
}
508
</style>
509
```
510
511
### Multiple AJAX Sources
512
513
```vue
514
<template>
515
<v-select
516
v-model="selectedResult"
517
:options="allResults"
518
:loading="isLoading"
519
@search="onSearch"
520
label="title"
521
placeholder="Search across multiple sources..."
522
>
523
<template #option="result">
524
<div class="search-result">
525
<div class="result-title">{{ result.title }}</div>
526
<div class="result-source">{{ result.source }}</div>
527
<div class="result-description">{{ result.description }}</div>
528
</div>
529
</template>
530
</v-select>
531
</template>
532
533
<script>
534
import axios from 'axios';
535
536
export default {
537
data() {
538
return {
539
selectedResult: null,
540
allResults: [],
541
isLoading: false
542
};
543
},
544
methods: {
545
async onSearch(searchText, toggleLoading) {
546
if (searchText.length < 2) {
547
this.allResults = [];
548
return;
549
}
550
551
this.isLoading = true;
552
553
try {
554
// Search multiple sources in parallel
555
const [usersResponse, postsResponse, productsResponse] = await Promise.allSettled([
556
axios.get('/api/users/search', { params: { q: searchText } }),
557
axios.get('/api/posts/search', { params: { q: searchText } }),
558
axios.get('/api/products/search', { params: { q: searchText } })
559
]);
560
561
const results = [];
562
563
// Process users
564
if (usersResponse.status === 'fulfilled') {
565
results.push(...usersResponse.value.data.map(user => ({
566
id: `user-${user.id}`,
567
title: user.name,
568
description: user.email,
569
source: 'Users',
570
data: user
571
})));
572
}
573
574
// Process posts
575
if (postsResponse.status === 'fulfilled') {
576
results.push(...postsResponse.value.data.map(post => ({
577
id: `post-${post.id}`,
578
title: post.title,
579
description: post.excerpt,
580
source: 'Posts',
581
data: post
582
})));
583
}
584
585
// Process products
586
if (productsResponse.status === 'fulfilled') {
587
results.push(...productsResponse.value.data.map(product => ({
588
id: `product-${product.id}`,
589
title: product.name,
590
description: `$${product.price}`,
591
source: 'Products',
592
data: product
593
})));
594
}
595
596
this.allResults = results;
597
598
} catch (error) {
599
console.error('Multi-source search failed:', error);
600
this.allResults = [];
601
} finally {
602
this.isLoading = false;
603
}
604
}
605
}
606
};
607
</script>
608
609
<style scoped>
610
.search-result {
611
padding: 5px 0;
612
}
613
.result-title {
614
font-weight: bold;
615
color: #333;
616
}
617
.result-source {
618
font-size: 0.8em;
619
color: #007bff;
620
text-transform: uppercase;
621
}
622
.result-description {
623
font-size: 0.9em;
624
color: #666;
625
}
626
</style>
627
```
628
629
### AJAX with Progressive Enhancement
630
631
```vue
632
<template>
633
<v-select
634
v-model="selectedOption"
635
:options="displayOptions"
636
:loading="isLoading"
637
@search="onSearch"
638
@open="onOpen"
639
label="name"
640
placeholder="Progressive loading..."
641
/>
642
</template>
643
644
<script>
645
import axios from 'axios';
646
647
export default {
648
data() {
649
return {
650
selectedOption: null,
651
staticOptions: [
652
{ id: 1, name: 'Popular Option 1', type: 'static' },
653
{ id: 2, name: 'Popular Option 2', type: 'static' },
654
{ id: 3, name: 'Popular Option 3', type: 'static' }
655
],
656
dynamicOptions: [],
657
isLoading: false,
658
hasLoadedDynamic: false
659
};
660
},
661
computed: {
662
displayOptions() {
663
return [...this.staticOptions, ...this.dynamicOptions];
664
}
665
},
666
methods: {
667
async onOpen() {
668
// Load additional options when dropdown opens
669
if (!this.hasLoadedDynamic) {
670
await this.loadMoreOptions();
671
}
672
},
673
674
async onSearch(searchText) {
675
if (searchText.length < 2) {
676
// Reset to static + any previously loaded dynamic options
677
return;
678
}
679
680
await this.searchDynamicOptions(searchText);
681
},
682
683
async loadMoreOptions() {
684
this.isLoading = true;
685
686
try {
687
const response = await axios.get('/api/options/popular');
688
this.dynamicOptions = response.data.map(option => ({
689
...option,
690
type: 'dynamic'
691
}));
692
this.hasLoadedDynamic = true;
693
} catch (error) {
694
console.error('Failed to load additional options:', error);
695
} finally {
696
this.isLoading = false;
697
}
698
},
699
700
async searchDynamicOptions(searchText) {
701
this.isLoading = true;
702
703
try {
704
const response = await axios.get('/api/options/search', {
705
params: { q: searchText }
706
});
707
708
// Replace dynamic options with search results
709
this.dynamicOptions = response.data.map(option => ({
710
...option,
711
type: 'search'
712
}));
713
714
} catch (error) {
715
console.error('Search failed:', error);
716
// Keep existing dynamic options on search failure
717
} finally {
718
this.isLoading = false;
719
}
720
}
721
}
722
};
723
</script>
724
```