-
-
Notifications
You must be signed in to change notification settings - Fork 359
feat: add disableAutoFilter prop to Combobox for custom filtering #3615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add disableAutoFilter prop to Combobox for custom filtering #3615
Conversation
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
@Yeison07 just confirming I see this is ready for review. I've been away the last week so I'm just getting back on my feet here. It may take me a bit longer to get a proper review in, but I will asap. |
@Yeison07 so reviewing this, I actually like your alternative idea, as we've had had a pending request for custom filtering for a while now: We must preserve the default behavior for backwards compatibility (to avoid a breaking change), so here's my suggestion:
Per the |
zagProps.onInputValueChange?.(event); | ||
} | ||
})); | ||
|
||
if (disableAutoFilter) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of:
let options = $state.raw(data);
if (disableAutoFilter) {
$effect(() => {
options = data;
});
}
We should do:
let options = $derived(data);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented $derived for options, as you suggested, to avoid using $state.raw and $effect, making the logic more declarative and reactive. Instead of using disableAutoFilter, I introduced a prop filter that allows the user to pass a custom filter function or null. If filter is null, options directly uses data with $derived(filter === null ? data : internalOptions). If filter is provided, I maintain an internal state (internalOptions) that is updated in onInputValueChange with the custom filtering. This eliminates the need for $effect and allows for a more flexible API to control filtering from the parent component.
@@ -65,11 +66,19 @@ | |||
zagProps.onOpenChange?.(event); | |||
}, | |||
onInputValueChange(event) { | |||
const filtered = data.filter((item) => item.label.toLowerCase().includes(event.inputValue.toLowerCase())); | |||
options = filtered; | |||
if (!disableAutoFilter) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should remove the if check and do:
options = data.filter(filter);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, the default filter right now should be the default value for the filter in our props, so :
filter = (item, value) => item.label.toLowerCase().includes(value);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The prop filter now uses the suggested default filter, (item, value) => item.label.toLowerCase().includes(value.toLowerCase()), maintaining compatibility with the previous behavior. Instead of options = data.filter(filter), I use an internal state internalOptions and $derived(filter === null ? data : internalOptions) to give the parent component full control over filtering. If filter is null, the component uses data directly, which allows to handle complex cases such as input length checks or asynchronous requests without interference from internal filtering.
@Yeison07 just checking on the status of this and the requested changes. |
hi @endigo9740 , checking this out, been a bit busy and couldn't, hope to finish it tonight. |
…ovide a custom filtering function for items based on the input value.
🦋 Changeset detectedLatest commit: 2ca02d3 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Hey @endigo9740 @Hugos68, I tested my code under three scenarios I think cover the main use cases for the Combobox:
Overall, I think the filter prop with null support and internalOptions makes the component flexible, especially for dynamic cases like async fetches or custom preconditions. Since this is my first time working with Zag, I might've missed some edge cases. Let me know what you think about my approach in the latest commit and if the onOpenChange behavior is okay or needs a fix. Thanks for the feedback! 👍 |
@Yeison07 so first things first, this looks much better. Reviewing the original Zag implementation - which we strive to stick as close to as possible - I see filtering only occurs in onInputValueChange({ inputValue }) {
const filtered = comboboxData.filter((item) =>
item.label.toLowerCase().includes(inputValue.toLowerCase()),
)
const newOptions = filtered.length > 0 ? filtered : comboboxData
options = newOptions
}, But I would expect we could perhaps remove the need for let {
// ...
filter = (item: T, value: string) => item.label.toLowerCase().includes(value.toLowerCase()),
// ...
}: ComboboxProps<T> = $props(); let options = $derived(data); onInputValueChange({ inputValue }) {
const filtered = $state.snapshot(options).filter((item) => filter(item, inputValue));
options = filtered.length > 0 ? filtered : options;
}, This would:
To be honest I haven't used I'll also again ask @Hugos68 to chime in if he has any input on your code or my suggestion here. One final thing @Yeison07 - when we do settle on the custom filter solution. We'll need to document this alongside the Combobox itself. Likely as a small paragraph + code snippet that explains that you can replace the filtering option, and showcasing a simple use case. Let me know if you need help with this. Also make sure to update the changset to represent the shift to the customizable |
Linked Issue
Closes #3600
Description
This PR addresses the issues reported in #3600, where the Combobox component experiences desynchronization between the provided data and the filtered options. The root cause is the internal "contains" filter logic, which always executes before any external handlers (e.g., onInputValueChange). This results in the data being one step behind, causing lag in scenarios involving dynamic filtering, such as minimum length checks or asynchronous API fetches.
To resolve this, I've introduced an optional boolean prop disableAutoFilter (default: false). When set to true, the internal filtering is skipped, allowing users full control over data updates via their own onInputValueChange handler. This ensures seamless handling of custom filtering logic without interference from the component's built-in behavior.
Additionally, a reactive $effect has been added to synchronize the options with the data prop when auto-filtering is disabled, maintaining reactivity.
This approach preserves backward compatibility, as the default behavior remains unchanged.
Alternatives Considered
I evaluated a more flexible option, such as introducing a filterFn prop to allow custom filter callbacks. However, the disableAutoFilter prop provides a simpler and more accessible solution for users, especially beginners, while still enabling advanced customization.
Checklist
Please read and apply all contribution requirements.
docs/
,feature/
,chore/
,bugfix/
main
branchpnpm check
in the root of the monorepopnpm format
in the root of the monorepopnpm lint
in the root of the monorepopnpm test
in the root of the monorepo/package
projects, please supply a ChangesetChangesets
View our documentation to learn more about Changesets. To create a Changeset:
pnpm changeset
and follow the prompts