@plantae-tech/plantae-filter
v0.3.4
Published
Customizable and performant dropdown component with search, multi-select, and virtual scrolling for native <select> elements.
Keywords
Readme
Plantae Filter
A lightweight JavaScript plugin to transform native <select> elements into custom dropdown components with search, multi-select, and virtual list rendering (Clusterize.js).
Installation
NPM
npm install @plantae-tech/plantae-filterCDN (UMD)
<script src="https://plantae-tecnologies.github.io/plantae-filter/plantae-filter.umd.js"></script>CDN (ES Module)
<script type="module" src="https://plantae-tecnologies.github.io/plantae-filter/plantae-filter.es.js"></script>Usage
1. Using with UMD (CDN + Vanilla JS)
<script src="https://plantae-tecnologies.github.io/plantae-filter/plantae-filter.umd.js"></script>
<select id="mySelect" data-pl-label="Products">
<optgroup label="Fruits">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</optgroup>
<option value="other">Other</option>
</select>
<script>
const select = document.getElementById('mySelect');
const pf = new PlantaeFilter(select);
// API Example
pf.addOption({ value: 'grape', text: 'Grape', group: 'Fruits' });
pf.selectOptions(['grape']);
</script>2. Using with Bundler (Vite, Webpack, Rollup)
import PlantaeFilter from '@plantae-tech/plantae-filter';
const select = document.querySelector('select');
const filter = new PlantaeFilter(select, {
label: "Products",
allText: "All",
emptyText: "Select options",
groupSelectedLabel: "Selected",
applyButtonText: "Apply",
searchPlaceholder: "Search.."
});
// API Example
filter.addOption({ value: 'grape', text: 'Grape', group: 'Fruits' });
filter.selectOptions(['grape']);3. Initializing select with custom element (example with UMD)
<script src="https://plantae-tecnologies.github.io/plantae-filter/plantae-filter.umd.js"></script>
<plantae-filter label="Products" empty-text="Select..">
<select id="mySelect">
<optgroup label="Fruits">
<option value="apple">Apple</option>
<option value="banana">Banana</option>
</optgroup>
<option value="other">Other</option>
</select>
</plantae-filter>
<script>
const pf = document.querySelector('plantae-filter');
// API Example
pf.addOption({ value: 'grape', text: 'Grape', group: 'Fruits' });
pf.selectOptions(['grape']);
</script>Data Attributes
Attributes must be informed in camelCase when informed by the class constructor.
Thedata-pl-*attributes are alternatives that can be used inside the<select>element if you prefer to configure it via the native<select>.
If both are provided, the values from<plantae-filter>will take priority.
| Attribute (constructor class) | Description | Example (<plantae-filter>) | Example (<select>) |
| --------------------------------------- | ------------------------------------------ | --------------------------------------------------------------- | -------------------------------------------------------------- |
| label | Label shown on the filter | label="Products" | data-pl-label="Products" |
| allText | Text when all items are selected | all-text="All" | data-pl-all-text="All" |
| emptyText | Text when no item is selected | empty-text="Select" | data-pl-empty-text="Select" |
| groupSelectedLabel | Label shown on the "Selected" group | group-selected-label="Selected" | data-pl-group-selected-label="Selected" |
| applyButtonText | Label shown on the apply button | apply-button-text="Apply" | data-pl-apply-button-text="Apply" |
| searchPlaceholder | Placeholder shown on the search input | search-placeholder="Search.." | data-pl-search-placeholder="Search.." |
| searchDebounceDelay | Search engine response time | search-debounce-delay="100" | data-pl-search-debounce-delay="100" |
| searchEngineMode | Search engine mode ('fuse' or 'fuse-worker') | search-engine-mode="fuse" | data-pl-search-engine-mode="fuse" |
| fuseOptions | JSON string with Fuse.js custom options | fuse-options='{"threshold": 0.2, "distance": 100}' | data-pl-fuse-options='{"threshold": 0.2}' |
| clusterizeOptions | JSON string with Clusterize.js options | clusterize-options='{"rows_in_block": 25}' | data-pl-clusterize-options='{"no_data_text": "No data"}' |
⚠️ Important Notice for Vite Users
If you are using Vite as your bundler and installing @plantae-tech/plantae-filter via NPM, you MUST configure your vite.config.ts if you are using searchEngineMode='fuse-worker'.
Why?
When the library is configured with searchEngineMode set to 'fuse-worker', it uses import.meta.url combined with ?worker to load a background thread (via Web Worker) for search operations. Vite’s dependency optimizer (optimizeDeps) might break the dynamic new URL() resolution during pre-bundling of node_modules, causing errors like:
The file does not exist at ".../node_modules/.vite/deps/assets/search-worker-xxxxx.js"Solution: Add this to your vite.config.ts
export default defineConfig({
optimizeDeps: {
exclude: ["@plantae-tech/plantae-filter"]
}
});Public API
| Method | Description |
|-----------------------------|--------------------------------------------------------|
| addOption(option) | Add a single option |
| addOptions(options) | Add multiple options |
| clearSelection() | Deselect all selected options |
| deselectOptions(values) | Deselect specific values |
| disableOptions(values) | Disable options by value |
| enableOptions(values) | Enable options by value |
| getAllOptions() | Get all available options |
| getSelected() | Get all selected options as objects |
| getValue() | Get the values of selected options (array of strings) |
| removeAllOptions() | Remove all options |
| removeOptions(values) | Remove options by value |
| selectOptions(values) | Select multiple values by value |
| setDataSource(config) | Load options from a paginated remote API |
| setValue(values) | Replace all selected options with new values |
Customization
1. Using Bootstrap 5 Theme
You can apply the official Bootstrap 5 theme by including the stylesheet:
<link rel="stylesheet" href="https://plantae-tecnologies.github.io/plantae-filter/theme/bootstrap5-theme.css">2. Customizing with ::part()
Plantae Filter exposes the following parts for styling via Shadow DOM ::part() selectors:
| Part Name | Description |
| ---------------------- | -------------------------------------------------------- |
| filter | The clickable filter input area |
| filter-text | The text container inside the filter area |
| clear-button | The clear (reset) button inside the filter |
| counter-filter | The badge with applied itens counter |
| dropdown | The dropdown container with the options list |
| dropdown-item | A single option (<li>) inside the dropdown |
| dropdown-item selected | A selected option inside the dropdown |
| dropdown-item focused | The item currently focused via keyboard navigation |
| highlight | The <mark> used to highlight search matches |
| optgroup | The group label (<li> separator) inside the dropdown |
| apply-button | The "Apply" button inside the dropdown footer |
| search-input | The search <input> inside the dropdown |
Example usage:
plantae-filter::part(filter) {
background: #f1f1f1;
border-radius: 4px;
}
plantae-filter::part(dropdown-item focused) {
ackground: #e0e0e0;
}
plantae-filter::part(highlight) {
background: yellow;
}Feel free to adapt styles to match your design system.
Advanced Features
Custom render Function (Option HTML override)
You can pass a custom render function via the class constructor to control how each option is rendered inside the dropdown.
This function receives an object containing the full OptionItem, including:
text(with applied<mark>highlights)valuegroupdisabledselecteddata(custom metadata fromdata-*attributes)
Example:
new PlantaeFilter(selectElement, {
render: ({ text, value, selected, data }) => {
return `
<div><strong>${text}</strong></div>
<small>${data?.description ?? 'no description'}</small>
`;
}
});The text field will already contain search highlights (wrapped in <mark part="highlight">), so you don’t need to reapply them manually.
Option data object (metadata from data-*)
You can add data-* attributes to your <option> elements. All of them will be automatically extracted and available inside the data property of each option.
Example:
<option value="1" data-code="A1" data-description="Imported item">Apple</option>In the render function or API, you'll get:
{
value: "1",
text: "Apple",
data: {
code: "A1",
description: "Imported item"
}
}You can also include those fields in the search by configuring fuseOptions.keys:
new PlantaeFilter(select, {
fuseOptions: {
keys: ['text', 'value', 'data.description', 'data.code'],
includeMatches: true
}
});Paginated API / Remote Data Source
Plantae Filter supports loading options from paginated APIs. Pages are fetched in background, and items become available (and searchable) as each page loads. With the concurrency option, multiple pages can be fetched in parallel for faster loading.
This is configured via the dataSource option in the constructor, or dynamically via setDataSource().
DataSource Config
| Property | Type | Required | Description |
|-------------------|-------------------------------------------------------------|----------|-----------------------------------------------------------------------------|
| url | string | Yes | API endpoint URL |
| method | 'GET' \| 'POST' | No | HTTP method (default: 'GET') |
| headers | Record<string, string> | No | Custom HTTP headers (e.g. Authorization) |
| params | Record<string, string> | No | Static query parameters merged into every request |
| pageSize | number | No | Items per page (default: 50) |
| concurrency | number | No | Max parallel requests (default: 1). Requires numeric pagination and totalItems in mapResponse. |
| mapResponse | (body: any, cursor?: string \| number) => DataSourcePage | Yes | Transforms API response into the standard page format |
| buildPageParams | (cursor, pageSize) => Record<string, string> | No | Customizes pagination parameters (default: page / per_page) |
| onComplete | () => void | No | Callback invoked when all pages have been loaded successfully |
| onError | (error: Error) => void | No | Callback invoked when an error occurs during loading |
| onLoadingChange | (isLoading: boolean) => void | No | Callback invoked whenever the loading state changes |
The mapResponse function must return a DataSourcePage object:
// Cursor-based pagination (total unknown)
type DataSourcePage =
| { items: OptionItem[]; hasMore: boolean; nextCursor?: string | number }
// Parallel-capable pagination (total known — enables concurrency)
| { items: OptionItem[]; totalItems: number };Use the
hasMorevariant for cursor or sequential pagination. Use thetotalItemsvariant when the API returns a total count, which enables parallel fetching withconcurrency > 1.
Example: Page-based pagination
new PlantaeFilter(select, {
label: 'Products',
dataSource: {
url: 'https://api.example.com/products',
pageSize: 50,
headers: { Authorization: 'Bearer token' },
mapResponse: (res) => ({
items: res.data.map(p => ({
value: p.id,
text: p.name,
group: p.category
})),
hasMore: res.meta.current_page < res.meta.last_page,
nextCursor: res.meta.current_page + 1
})
}
});Example: Parallel fetching
When the API returns the total item count, you can enable parallel page fetching for significantly faster loading:
new PlantaeFilter(select, {
label: 'Products',
dataSource: {
url: 'https://api.example.com/products',
pageSize: 50,
concurrency: 4, // fetch up to 4 pages simultaneously
mapResponse: (res) => ({
items: res.data.map(p => ({
value: p.id,
text: p.name
})),
hasMore: res.meta.current_page < res.meta.last_page,
nextCursor: res.meta.current_page + 1,
totalItems: res.meta.total // required for parallel fetching
})
}
});Note: Parallel fetching requires numeric page-based pagination (
nextCursormust be a number) andtotalItemsin themapResponsereturn. When these conditions are not met, fetching falls back to sequential mode automatically.
Example: Cursor-based pagination
new PlantaeFilter(select, {
dataSource: {
url: 'https://api.example.com/items',
mapResponse: (res) => ({
items: res.results.map(i => ({ value: i.id, text: i.label })),
hasMore: !!res.next_cursor,
nextCursor: res.next_cursor
}),
buildPageParams: (cursor, pageSize) => ({
cursor: cursor?.toString() ?? '',
limit: pageSize.toString()
})
}
});Example: Offset/limit pagination
new PlantaeFilter(select, {
dataSource: {
url: '/api/users',
pageSize: 100,
mapResponse: (res, cursor) => {
const offset = (cursor as number) ?? 0;
return {
items: res.users.map(u => ({ value: u.id, text: u.name })),
hasMore: offset + res.users.length < res.total,
nextCursor: offset + res.users.length
};
},
buildPageParams: (cursor, pageSize) => ({
offset: (cursor ?? 0).toString(),
limit: pageSize.toString()
})
}
});Example: Loading and error callbacks
new PlantaeFilter(select, {
dataSource: {
url: '/api/products',
pageSize: 50,
mapResponse: (res) => ({
items: res.data.map(p => ({ value: p.id, text: p.name })),
hasMore: res.page < res.totalPages,
nextCursor: res.page + 1
}),
onLoadingChange: (isLoading) => {
document.getElementById('spinner')!.hidden = !isLoading;
},
onError: (error) => {
console.error('Failed to load options:', error.message);
},
onComplete: () => {
console.log('All pages loaded.');
}
}
});Dynamic usage with setDataSource()
You can also set or replace the data source after initialization:
const filter = new PlantaeFilter(select, { label: 'Users' });
// Later, load from API
filter.setDataSource({
url: '/api/users',
pageSize: 50,
mapResponse: (res) => ({
items: res.data.map(u => ({ value: u.id, text: u.name })),
hasMore: res.page < res.totalPages,
nextCursor: res.page + 1
})
});Browser Support
Plantae Filter leverages modern browser APIs, including the use of Web Workers to offload the search functionality (powered by Fuse.js) to a separate thread, ensuring a smoother experience when filtering large datasets.
Notes:
- The Web Worker is automatically used for all search operations.
- As of today, all modern browsers fully support Web Workers (Chrome, Firefox, Edge, Safari, etc.).
- Internet Explorer and very old browsers do not support Web Workers and are not compatible with this plugin.
- There is no fallback implementation if Web Workers are not available.
Recommendation:
Ensure your application targets modern browsers or environments that provide full support for Web Workers.
Powered by Plantae Gestão Agrícola
