Filter Components Refactoring
Overview
Filter components have been refactored to use modular, reusable components. This reduces code duplication and makes it easier to create new filter components in the future.
Generic Components
1. FilterContainer
A wrapper component that provides consistent layout and styling for filter sections.
Props:
title(String): Title displayed at the top (default: "Filter")show-clear-button(Boolean): Whether to show the clear filters buttonclear-button-text(String): Text for the clear button (default: "Clear Filters")
Events:
@clear: Emitted when the clear button is clicked
Usage:
<filter-container
title="Filter"
:show-clear-button="hasActiveFilters"
@clear="clearFilters"
>
<!-- Filter components go here -->
</filter-container>
2. FilterInput
A generic text input component for filters.
Props:
id(String, required): HTML id for the input elementlabel(String): Label text displayed above the inputmodel-value(String/Number): The input value (use with v-model)placeholder(String): Placeholder text for the inputtype(String): Input type (default: "text")
Events:
@update:model-value: Emitted when input value changes (v-model compatible)
Usage:
<filter-input
id="search-filter"
label="Search"
v-model="searchQuery"
placeholder="Search games..."
/>
3. FilterDropdown
A generic dropdown/select component for filters.
Props:
id(String, required): HTML id for the select elementlabel(String): Label text displayed above the dropdownmodel-value(String/Number): The selected value (use with v-model)options(Array, required): Array of options (strings or objects)placeholder(String): Placeholder text for empty optionvalue-key(String): For object arrays, which property to use as valuelabel-key(String): For object arrays, which property to use as label
Events:
@update:model-value: Emitted when selection changes (v-model compatible)
Usage:
<!-- Simple string array -->
<FilterDropdown
id="letter-filter"
label="Letter"
v-model="selectedLetter"
:options="['A', 'B', 'C']"
placeholder="All"
/>
<!-- Object array -->
<filter-dropdown
id="sort-filter"
label="Sort By"
v-model="selectedSort"
:options="[
{ value: 'date', label: 'Date' },
{ value: 'name', label: 'Name' },
]"
value-key="value"
label-key="label"
></filter-dropdown>
4. SearchableMultiSelect
A complex component for searchable multi-select with chips/tags display.
Props:
id(String, required): HTML id for the input elementlabel(String): Label text displayed above the inputplaceholder(String): Placeholder text for the search inputselected-items(Array): Array of selected items (use with v-model:selected-items)search-results(Array): Array of search results to displayis-searching(Boolean): Whether a search is in progresshas-more-results(Boolean): Whether there are more results to loadis-loading-more(Boolean): Whether more results are being loadedmin-search-length(Number): Minimum characters before searching (default: 2)debounce-ms(Number): Debounce delay in milliseconds (default: 400)show-match-all(Boolean): Whether to show "match all" checkboxrequire-all(Boolean): Value for "match all" checkbox (use with v-model:require-all)match-all-text(String): Text for the "match all" checkbox labelload-more-text(String): Text for the load more buttonno-results-text(String): Text shown when no results founditem-key(String): For object arrays, which property to use as unique keyitem-label(String): For object arrays, which property to use as display label
Events:
@update:selected-items: Emitted when selected items change@update:require-all: Emitted when "match all" checkbox changes@search: Emitted when user types in search (debounced)@loadMore: Emitted when load more button clicked@select: Emitted when an item is selected@remove: Emitted when an item is removed
Slots:
result-item: Custom template for search result itemsresult-content: Custom template for the content within a result item
Usage:
<searchable-multi-select
id="search-filter"
label="Search Items"
v-model:selected-items="selectedItems"
v-model:require-all="requireAll"
:search-results="searchResults"
:is-searching="isSearching"
:has-more-results="hasMoreResults"
:is-loading-more="isLoadingMore"
:show-match-all="true"
placeholder="Search..."
item-key="id"
item-label="name"
@search="handleSearch"
@load-more="handleLoadMore"
>
<!-- Custom result template -->
<template #result-item="{ results, selectItem }">
<div
v-for="item in results"
:key="item.id"
@mousedown="selectItem(item)"
>
{{ item.name }}
</div>
</template>
</searchable-multi-select>
Composables
useFilters(eventName, initialFilters)
Shared logic for managing filter state and URL synchronization.
Parameters:
eventName(String): Name of the custom event to dispatchinitialFilters(Object): Default filter values
Returns:
filters(Ref): Reactive filter state objectapplyFilters(customFilters): Function to apply filters and update URLclearFilters(defaultFilters): Function to clear filtersloadFiltersFromUrl(): Function to load filters from URL parameters
Usage:
const { applyFilters, clearFilters } = useFilters("my-filter-changed", {
search: "",
sort: "date",
});
// Apply filters
applyFilters({
search: "test",
sort: "name",
});
// Clear filters
clearFilters({ search: "", sort: "date" });
useSearch(searchFunction, options)
Shared logic for search functionality with debouncing and pagination.
Parameters:
searchFunction(Function): Async function that performs the search- Should accept
(query, page)and return{ success, items, currentPage, totalPages, hasMore }
- Should accept
options(Object):debounceMs(Number): Debounce delay (default: 400)minSearchLength(Number): Minimum search length (default: 2)
Returns:
searchResults(Ref): Array of search resultsisSearching(Ref): Whether a search is in progresshasMoreResults(Ref): Whether there are more results to loadisLoadingMore(Ref): Whether more results are being loadedcurrentSearchPage(Ref): Current page numbertotalPages(Ref): Total number of pagessearch(query, page, append): Function to perform searchdebouncedSearch(query, page): Debounced search functionloadMore(query): Function to load next pagereset(): Function to reset search state
Usage:
const searchApi = async (query, page) => {
const response = await fetch(`/api/search?q=${query}&page=${page}`);
const data = await response.json();
return {
success: data.success,
items: data.results,
currentPage: data.page,
totalPages: data.total_pages,
hasMore: data.has_more,
};
};
const {
searchResults,
isSearching,
hasMoreResults,
debouncedSearch,
loadMore,
} = useSearch(searchApi);
// Perform a search
debouncedSearch("search term");
// Load more results
loadMore("search term");
Benefits
- Reduced Code Duplication: Common functionality is now in reusable components
- Easier Maintenance: Changes to filter UI/behavior can be made in one place
- Consistent UI: All filters will have the same look and feel
- Faster Development: New filter components can be created quickly using generic components
- Better Separation of Concerns: Filter logic is separated from presentation
- Type Safety: Generic components handle different data types (strings, objects, arrays)
Migration Notes
The refactored PlatformFilter and ReleaseFilter components maintain the same external API (props, events) so no changes are needed to the PHP templates or parent components.
Creating New Filter Components
To create a new filter component:
- Use
FilterContaineras the wrapper - Add
FilterInputcomponents for text/search inputs - Add
FilterDropdowncomponents for select/dropdown filters - Use
SearchableMultiSelectfor searchable multi-select with chips - Use
useFilterscomposable for filter state management - Use
useSearchcomposable for search functionality
Example:
<template>
<filter-container
title="My Filters"
:show-clear-button="hasActiveFilters"
@clear="clearFilters"
>
<filter-input
id="search-filter"
label="Search"
v-model="searchQuery"
placeholder="Search..."
@update:model-value="onFilterChange"
></filter-input>
<filter-dropdown
id="category-filter"
label="Category"
v-model="selectedCategory"
:options="categories"
@update:model-value="onFilterChange"
></filter-dropdown>
</filter-container>
</template>
<script>
const { ref, computed } = require("vue");
const FilterContainer = require("./FilterContainer.vue");
const FilterInput = require("./FilterInput.vue");
const FilterDropdown = require("./FilterDropdown.vue");
const { useFilters } = require("../composables/useFilters.js");
module.exports = exports = {
name: "MyFilter",
components: { FilterContainer, FilterInput, FilterDropdown },
setup() {
const searchQuery = ref("");
const selectedCategory = ref("");
const categories = ["A", "B", "C"];
const { applyFilters, clearFilters } = useFilters("my-filter-changed", {
search: "",
category: "",
});
const hasActiveFilters = computed(
() => searchQuery.value !== "" || selectedCategory.value !== "",
);
const onFilterChange = () => {
applyFilters({
search: searchQuery.value,
category: selectedCategory.value,
});
};
return {
searchQuery,
selectedCategory,
categories,
hasActiveFilters,
onFilterChange,
clearFilters,
};
},
};
</script>