or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

ajax-loading.mdcustomization.mdindex.mdkeyboard-navigation.mdsearch-filtering.mdselection.mdtagging.md

ajax-loading.mddocs/

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

```