CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vue-select

Everything you wish the HTML select element could do, wrapped up into a lightweight, extensible Vue component.

Pending
Overview
Eval results
Files

ajax-loading.mddocs/

AJAX and Loading States

Built-in support for asynchronous data loading with loading state management, search event handling, and server-side filtering integration.

Capabilities

Loading State Management

Properties and methods for controlling loading indicators and asynchronous operations.

/**
 * Show loading state indicator
 * Toggles adding 'loading' class to main wrapper
 * Useful for controlling UI state during AJAX operations
 */
loading: Boolean  // default: false

// Data properties (from ajax mixin)
data: {
  /**
   * Internal mutable loading state
   * Synced with loading prop but can be controlled independently
   */
  mutableLoading: Boolean  // default: false
}

// Methods (from ajax mixin)
methods: {
  /**
   * Toggle loading state programmatically
   * Can be called without parameters to toggle current state
   * @param toggle - Optional boolean to set specific state
   * @returns Current loading state after toggle
   */
  toggleLoading(toggle?: Boolean): Boolean
}

Search Event Integration

Event system specifically designed for AJAX search implementation.

/**
 * Emitted whenever search text changes
 * Provides search text and loading toggle function for AJAX integration
 * @param searchText - Current search input value
 * @param toggleLoading - Function to control loading state
 */
'search': (searchText: String, toggleLoading: Function) => void

AJAX-Related Configuration

Properties that commonly work together with AJAX functionality.

/**
 * When false, updating options will not reset selected value
 * Useful when options are updated via AJAX without losing selection
 */
resetOnOptionsChange: Boolean | Function  // default: false

/**
 * Disable filtering when using server-side search
 * Options should be pre-filtered by server response
 */
filterable: Boolean  // default: true (set to false for server-side filtering)

Usage Examples

Basic AJAX Search

<template>
  <v-select 
    v-model="selectedUser"
    :options="searchResults"
    :loading="isLoading"
    @search="onSearch"
    label="name"
    placeholder="Search users..."
  />
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedUser: null,
      searchResults: [],
      isLoading: false
    };
  },
  methods: {
    async onSearch(search, toggleLoading) {
      if (search.length < 2) {
        this.searchResults = [];
        return;
      }
      
      toggleLoading(true);
      
      try {
        const response = await axios.get('/api/users/search', {
          params: { q: search }
        });
        this.searchResults = response.data;
      } catch (error) {
        console.error('Search failed:', error);
        this.searchResults = [];
      } finally {
        toggleLoading(false);
      }
    }
  }
};
</script>

Debounced AJAX Search

<template>
  <v-select 
    v-model="selectedProduct"
    :options="products"
    :loading="isSearching"
    :filterable="false"
    @search="debouncedSearch"
    label="name"
    placeholder="Search products..."
  />
</template>

<script>
import axios from 'axios';
import { debounce } from 'lodash';

export default {
  data() {
    return {
      selectedProduct: null,
      products: [],
      isSearching: false
    };
  },
  created() {
    // Create debounced search function
    this.debouncedSearch = debounce(this.performSearch, 300);
  },
  methods: {
    async performSearch(searchText, toggleLoading) {
      if (searchText.length < 2) {
        this.products = [];
        return;
      }
      
      this.isSearching = true;
      
      try {
        const response = await axios.get('/api/products', {
          params: { 
            search: searchText,
            limit: 20
          }
        });
        this.products = response.data.products;
      } catch (error) {
        console.error('Product search failed:', error);
        this.products = [];
      } finally {
        this.isSearching = false;
      }
    }
  }
};
</script>

AJAX with Caching

<template>
  <v-select 
    v-model="selectedRepository"
    :options="repositories"
    :loading="isLoading"
    @search="onSearch"
    label="full_name"
    placeholder="Search GitHub repositories..."
  />
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedRepository: null,
      repositories: [],
      isLoading: false,
      searchCache: new Map(),
      lastSearch: ''
    };
  },
  methods: {
    async onSearch(searchText, toggleLoading) {
      if (searchText.length < 2) {
        this.repositories = [];
        return;
      }
      
      // Check cache first
      if (this.searchCache.has(searchText)) {
        this.repositories = this.searchCache.get(searchText);
        return;
      }
      
      this.isLoading = true;
      this.lastSearch = searchText;
      
      try {
        const response = await axios.get('https://api.github.com/search/repositories', {
          params: { 
            q: searchText,
            sort: 'stars',
            order: 'desc',
            per_page: 10
          }
        });
        
        const results = response.data.items;
        
        // Only update if this is still the latest search
        if (searchText === this.lastSearch) {
          this.repositories = results;
          // Cache results
          this.searchCache.set(searchText, results);
        }
        
      } catch (error) {
        console.error('Repository search failed:', error);
        if (searchText === this.lastSearch) {
          this.repositories = [];
        }
      } finally {
        if (searchText === this.lastSearch) {
          this.isLoading = false;
        }
      }
    }
  }
};
</script>

Server-Side Filtering with Pagination

<template>
  <v-select 
    v-model="selectedItem"
    :options="items"
    :loading="isLoading"
    :filterable="false"
    @search="onSearch"
    @open="loadInitialData"
    label="title"
    placeholder="Search with pagination..."
  >
    <template #list-footer v-if="hasMoreResults">
      <div class="load-more">
        <button @click="loadMore" :disabled="isLoadingMore">
          {{ isLoadingMore ? 'Loading...' : 'Load More' }}
        </button>
      </div>
    </template>
  </v-select>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedItem: null,
      items: [],
      isLoading: false,
      isLoadingMore: false,
      currentSearch: '',
      currentPage: 1,
      hasMoreResults: false
    };
  },
  methods: {
    async loadInitialData() {
      if (this.items.length === 0) {
        await this.performSearch('', 1);
      }
    },
    
    async onSearch(searchText) {
      this.currentSearch = searchText;
      this.currentPage = 1;
      await this.performSearch(searchText, 1, true);
    },
    
    async loadMore() {
      this.currentPage++;
      await this.performSearch(this.currentSearch, this.currentPage, false);
    },
    
    async performSearch(searchText, page, replace = true) {
      const isFirstPage = page === 1;
      
      if (replace) {
        this.isLoading = true;
      } else {
        this.isLoadingMore = true;
      }
      
      try {
        const response = await axios.get('/api/items', {
          params: {
            q: searchText,
            page: page,
            per_page: 20
          }
        });
        
        const newItems = response.data.items;
        
        if (replace) {
          this.items = newItems;
        } else {
          this.items = [...this.items, ...newItems];
        }
        
        this.hasMoreResults = response.data.has_more;
        
      } catch (error) {
        console.error('Search failed:', error);
        if (replace) {
          this.items = [];
        }
      } finally {
        this.isLoading = false;
        this.isLoadingMore = false;
      }
    }
  }
};
</script>

<style scoped>
.load-more {
  padding: 10px;
  text-align: center;
  border-top: 1px solid #e9ecef;
}
.load-more button {
  padding: 5px 15px;
  border: 1px solid #007bff;
  background: #007bff;
  color: white;
  border-radius: 4px;
  cursor: pointer;
}
.load-more button:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>

AJAX with Error Handling

<template>
  <div>
    <v-select 
      v-model="selectedCity"
      :options="cities"
      :loading="isLoading"
      @search="onSearch"
      label="name"
      placeholder="Search cities..."
    >
      <template #no-options>
        <div class="no-results">
          <div v-if="searchError" class="error-message">
            <p>❌ Search failed: {{ searchError }}</p>
            <button @click="retryLastSearch">Retry</button>
          </div>
          <div v-else-if="hasSearched && cities.length === 0">
            No cities found for your search.
          </div>
          <div v-else>
            Start typing to search cities...
          </div>
        </div>
      </template>
    </v-select>
    
    <div v-if="searchError" class="error-banner">
      Connection issues detected. Please check your internet connection.
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedCity: null,
      cities: [],
      isLoading: false,
      searchError: null,
      hasSearched: false,
      lastSearchText: ''
    };
  },
  methods: {
    async onSearch(searchText, toggleLoading) {
      if (searchText.length < 2) {
        this.cities = [];
        this.searchError = null;
        this.hasSearched = false;
        return;
      }
      
      this.lastSearchText = searchText;
      this.searchError = null;
      this.isLoading = true;
      
      try {
        const response = await axios.get('/api/cities', {
          params: { q: searchText },
          timeout: 5000 // 5 second timeout
        });
        
        this.cities = response.data;
        this.hasSearched = true;
        
      } catch (error) {
        console.error('City search failed:', error);
        
        if (error.code === 'ECONNABORTED') {
          this.searchError = 'Request timed out';
        } else if (error.response) {
          this.searchError = `Server error: ${error.response.status}`;
        } else if (error.request) {
          this.searchError = 'Network error';
        } else {
          this.searchError = 'Unknown error occurred';
        }
        
        this.cities = [];
        this.hasSearched = true;
        
      } finally {
        this.isLoading = false;
      }
    },
    
    retryLastSearch() {
      if (this.lastSearchText) {
        this.onSearch(this.lastSearchText);
      }
    }
  }
};
</script>

<style scoped>
.no-results {
  padding: 20px;
  text-align: center;
}
.error-message {
  color: #dc3545;
}
.error-message button {
  margin-top: 10px;
  padding: 5px 10px;
  border: 1px solid #dc3545;
  background: white;
  color: #dc3545;
  border-radius: 4px;
  cursor: pointer;
}
.error-banner {
  margin-top: 10px;
  padding: 10px;
  background: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
  border-radius: 4px;
}
</style>

Multiple AJAX Sources

<template>
  <v-select 
    v-model="selectedResult"
    :options="allResults"
    :loading="isLoading"
    @search="onSearch"
    label="title"
    placeholder="Search across multiple sources..."
  >
    <template #option="result">
      <div class="search-result">
        <div class="result-title">{{ result.title }}</div>
        <div class="result-source">{{ result.source }}</div>
        <div class="result-description">{{ result.description }}</div>
      </div>
    </template>
  </v-select>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedResult: null,
      allResults: [],
      isLoading: false
    };
  },
  methods: {
    async onSearch(searchText, toggleLoading) {
      if (searchText.length < 2) {
        this.allResults = [];
        return;
      }
      
      this.isLoading = true;
      
      try {
        // Search multiple sources in parallel
        const [usersResponse, postsResponse, productsResponse] = await Promise.allSettled([
          axios.get('/api/users/search', { params: { q: searchText } }),
          axios.get('/api/posts/search', { params: { q: searchText } }),
          axios.get('/api/products/search', { params: { q: searchText } })
        ]);
        
        const results = [];
        
        // Process users
        if (usersResponse.status === 'fulfilled') {
          results.push(...usersResponse.value.data.map(user => ({
            id: `user-${user.id}`,
            title: user.name,
            description: user.email,
            source: 'Users',
            data: user
          })));
        }
        
        // Process posts
        if (postsResponse.status === 'fulfilled') {
          results.push(...postsResponse.value.data.map(post => ({
            id: `post-${post.id}`,
            title: post.title,
            description: post.excerpt,
            source: 'Posts',
            data: post
          })));
        }
        
        // Process products
        if (productsResponse.status === 'fulfilled') {
          results.push(...productsResponse.value.data.map(product => ({
            id: `product-${product.id}`,
            title: product.name,
            description: `$${product.price}`,
            source: 'Products',
            data: product
          })));
        }
        
        this.allResults = results;
        
      } catch (error) {
        console.error('Multi-source search failed:', error);
        this.allResults = [];
      } finally {
        this.isLoading = false;
      }
    }
  }
};
</script>

<style scoped>
.search-result {
  padding: 5px 0;
}
.result-title {
  font-weight: bold;
  color: #333;
}
.result-source {
  font-size: 0.8em;
  color: #007bff;
  text-transform: uppercase;
}
.result-description {
  font-size: 0.9em;
  color: #666;
}
</style>

AJAX with Progressive Enhancement

<template>
  <v-select 
    v-model="selectedOption"
    :options="displayOptions"
    :loading="isLoading"
    @search="onSearch"
    @open="onOpen"
    label="name"
    placeholder="Progressive loading..."
  />
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      selectedOption: null,
      staticOptions: [
        { id: 1, name: 'Popular Option 1', type: 'static' },
        { id: 2, name: 'Popular Option 2', type: 'static' },
        { id: 3, name: 'Popular Option 3', type: 'static' }
      ],
      dynamicOptions: [],
      isLoading: false,
      hasLoadedDynamic: false
    };
  },
  computed: {
    displayOptions() {
      return [...this.staticOptions, ...this.dynamicOptions];
    }
  },
  methods: {
    async onOpen() {
      // Load additional options when dropdown opens
      if (!this.hasLoadedDynamic) {
        await this.loadMoreOptions();
      }
    },
    
    async onSearch(searchText) {
      if (searchText.length < 2) {
        // Reset to static + any previously loaded dynamic options
        return;
      }
      
      await this.searchDynamicOptions(searchText);
    },
    
    async loadMoreOptions() {
      this.isLoading = true;
      
      try {
        const response = await axios.get('/api/options/popular');
        this.dynamicOptions = response.data.map(option => ({
          ...option,
          type: 'dynamic'
        }));
        this.hasLoadedDynamic = true;
      } catch (error) {
        console.error('Failed to load additional options:', error);
      } finally {
        this.isLoading = false;
      }
    },
    
    async searchDynamicOptions(searchText) {
      this.isLoading = true;
      
      try {
        const response = await axios.get('/api/options/search', {
          params: { q: searchText }
        });
        
        // Replace dynamic options with search results
        this.dynamicOptions = response.data.map(option => ({
          ...option,
          type: 'search'
        }));
        
      } catch (error) {
        console.error('Search failed:', error);
        // Keep existing dynamic options on search failure
      } finally {
        this.isLoading = false;
      }
    }
  }
};
</script>

Install with Tessl CLI

npx tessl i tessl/npm-vue-select

docs

ajax-loading.md

customization.md

index.md

keyboard-navigation.md

search-filtering.md

selection.md

tagging.md

tile.json