In modern WordPress development, enhancing user experience (UX) for custom components often involves handling data from custom post types and ensuring a smooth and responsive interface. In this technical tutorial, we’ll explore how to create a custom autocomplete dropdown using WordPress’s FormTokenField for a tag-like movie selection with added “Load More” functionality.
We’ll be focusing on a custom post type rt-movie, and demonstrate how to integrate dynamic search, single selection, and pagination for a great UX. We’ll also tackle common challenges like loading states and error handling.
Problem Overview
We needed to implement a movie selection dropdown for a custom post type (rt-movie) in WordPress, ensuring that users can:
- Search for a movie dynamically from the database.
- Select only one movie at a time.
- Implement debouncing to avoid excessive API calls during search.
To achieve this, we decided to utilize WordPress’s native FormTokenField component, which allows for searchable tag-like inputs, and enhance it with pagination and a “Load More” button.
Solution Breakdown
Here’s how we approached the problem:
Step 1: Fetching Movies Using useSelect
We use the useSelect hook from @wordpress/data to dynamically fetch movies from our custom post type. The hook will monitor the searchTerm and page states to trigger a new query each time the search input changes or the user requests more results.
import { useSelect } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { FormTokenField } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
const CustomAutocompleteField = ({ label, value, onChange, postType = 'rt-movie' }) => {
const [searchTerm, setSearchTerm] = useState('');
const [selectedToken, setSelectedToken] = useState(value ? [value] : []);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Fetch movies using useSelect hook
const movies = useSelect(
(select) => {
setIsLoading(true);
try {
const result = select('core').getEntityRecords('postType', postType, {
search: searchTerm,
per_page: 20,
page,
});
setIsLoading(false);
return result || [];
} catch (err) {
setError(__('Unable to fetch movies', 'movie-library'));
setIsLoading(false);
return [];
}
},
[postType, searchTerm, page]
);
In this snippet:
useSelectretrieves movies from thert-moviepost type by searching for thesearchTermand paginating results by thepagenumber.- We manage loading and error states for better UX, showing users when the results are loading or if an error occurred.
Step 2: Using FormTokenField for Autocomplete
FormTokenField is a versatile component that provides a tag-like selection interface. It’s typically used for multi-select scenarios, but we adapted it for single selection by restricting the tokens.
// Options for the FormTokenField
const movieSuggestions = movies?.map((movie) => movie.title.rendered) || [];
// Handle input change in the FormTokenField
const handleInputChange = (newSearchTerm) => {
setSearchTerm(newSearchTerm);
setPage(1); // Reset page on new search
};
// Handle token change (selection of movies)
const handleTokenChange = (selectedTokens) => {
if (selectedTokens.length > 0) {
const selectedMovie = movies.find(
(movie) => movie.title.rendered === selectedTokens[0]
);
setSelectedToken([selectedTokens[0]]);
onChange(selectedMovie?.id); // Pass movie ID to parent
} else {
setSelectedToken([]); // Clear selection
onChange(null);
}
};
<FormTokenField
label={__(label || 'Search and select a movie', 'movie-library')}
value={selectedToken} // The currently selected token (single movie)
suggestions={movieSuggestions} // Suggestions from the fetched movies
onInputChange={(newSearchTerm) => handleInputChange(newSearchTerm)}
onChange={(newTokens) => handleTokenChange(newTokens)}
maxSuggestions={20} // Limit to 20 suggestions per search
placeholder={__('Start typing movie name...', 'movie-library')}
aria-live="polite"
aria-busy={isLoading ? 'true' : 'false'}
aria-describedby={error ? 'error-message' : undefined}
/>
Key Points:
suggestions: We pass in the list of movie titles fetched from the database.onInputChange: Triggers a search request when the user types in the input field.onChange: Handles the selection and passes the selected movie ID back to the parent component.value: This holds the currently selected movie.
Step 3: Adding Debounce for Better Performance
To prevent excessive API calls when the user types rapidly, we implemented a debounce function that delays the search request. This ensures the API is only called after the user has paused typing.
import { useEffect } from 'react';
const useDebouncedEffect = (effect, deps, delay) => {
useEffect(() => {
const handler = setTimeout(() => effect(), delay);
return () => clearTimeout(handler);
}, [...(deps || []), delay]);
};
// Debounce the searchTerm input
useDebouncedEffect(() => {
setSearchTerm(searchTerm);
}, [searchTerm], 500); // Delay of 500ms
Step 4: Handling Loading and Error States
Good UX also requires feedback to users. We handled loading and error states to ensure the user knows when the results are being fetched and if there was any issue retrieving the data.
{isLoading && <p>{__('Loading movies...', 'movie-library')}</p>}
{error && <p id="error-message" className="error-message">{error}</p>}
Final Thoughts
By combining WordPress’s native FormTokenField, useSelect, and some custom pagination logic, we built a robust and responsive autocomplete dropdown with dynamic search and load-more functionality. This approach can be further extended for other custom post types or use cases where you need a searchable and paginated dropdown.
The key aspects of this solution include:
- Debounced Input: To avoid unnecessary API calls.
- Error and Loading States: For a polished user experience.
Thank you for reading through the post… More will be added.
by ~Leaveitblank (Mayank Tripathi)