-
-
Notifications
You must be signed in to change notification settings - Fork 52
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
Improve search functionality in SelectField
and MultiSelect
#577
Improve search functionality in SelectField
and MultiSelect
#577
Conversation
|
🦋 Changeset detectedLatest commit: 1b17d69 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 |
searchLabel
property in SelectField
searchLabel
property in SelectField
and MultiSelect
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
commit: |
I'm not sure it's worth increasing the API surface area, but it looks like we need to refine the contract of export let search = async (text: string) => {
logger.debug('search', { text, open });
if (text === '') {
// Reset options
filteredOptions = options;
} else {
const words = text?.toLowerCase().split(' ') ?? [];
filteredOptions = options.filter((option) => {
return words.every((word) => option.label.toLowerCase().includes(word));
});
}
}; to instead return the filtered options... export let search = async (text: string) => {
logger.debug('search', { text, open });
if (text === '') {
// Reset options
return options;
} else {
const words = text?.toLowerCase().split(' ') ?? [];
return options.filter((option) => {
return words.every((word) => option.label.toLowerCase().includes(word));
});
}
}; and then change where we are handling the results to apply them from... search(searchText).then(() => {
...
}) to... search(searchText).then((options) => {
filteredOptions = options;
...
}) This should give you the flexibility you need. I thought if it would be worth passing options into search as well Note: you should be able to accomplish this now by tracking |
@techniq Yes, I went the route of tracking the three search texts and three filtered objects outside of the The idea you mentioned above would definitely be a better DX than the current What you've proposed seems like a big and flexible win, requiring only a minimal amount of work (this way, we could avoid maintaining the search state and separate filtered objects outside the component usage). Thanks! |
I think this change can be considered a You might also be able to change this while maintaining full backwards compatible by checking if search(searchText).then((_options) => {
filteredOptions = _options ?? options;
...
}) |
@techniq I'm currently working to get us on the latest versions of The current API for this change would be something like this: <SelectField
options={users.map((u) => ({
...o,
value: u.id,
label: u.name,
searchLabel: [u.name, u.email]
}))}
>
<svelte:fragment slot="option" let:option={user}>
<MenuItem>
<div data-id={user.id}>{user.name} ({user.email})</div>
</MenuItem>
</svelte:fragment>
</SelectField>
/> I don't like that my initial solution here requires a property to be added on the I wonder if there's a better fix that would exist as a hybrid between this and the existing <SelectField
options={users.map((u) => ({
...o,
value: u.id,
label: u.name,
}))}
search={async (user, searchText) => {
const words = searchText?.toLowerCase().split(' ') ?? [];
const label = [user.name, user.email].filter(Boolean).join(' ');
return words.every((word) => label.toLowerCase().includes(word));
}}
>
<svelte:fragment slot="option" let:option={user}>
<MenuItem>
<div data-id={user.id}>{user.name} ({user.email})</div>
</MenuItem>
</svelte:fragment>
</SelectField>
/> This does feel much cleaner in my opinion. I think this could be a good fix to the Something like this could work: <SelectField
options={users.map((u) => ({
...o,
value: u.id,
label: u.name,
}))}
search={searchFor((user) => [user.name, user.email])}
>
<svelte:fragment slot="option" let:option={user}>
<MenuItem>
<div data-id={user.id}>{user.name} ({user.email})</div>
</MenuItem>
</svelte:fragment>
</SelectField>
/> In this case, the <!-- SelectField.svelte -->
<script lang="ts" context="module">
export function searchFor<T extends unknown>(fn: (option: T) => string | string[]) {
return async (option: T, searchText: string) => {
const words = searchText?.toLowerCase().split(' ') ?? [];
const searchData = fn(option);
const label = String(
Array.isArray(searchData)
? searchData.filter(Boolean).join(' ')
: searchData
).trim().toLowerCase();
return words.every((word) => label.includes(word));
} <!-- SelectField_Consumer.svelte -->
<script lang="ts">
import { default as SelectField, searchFor } from './SelectField.svelte';
</script> Although, those imports might look prettier if you move that into a util somewhere. After all, that pattern is common and could be useful in plenty of other search or filtering contexts. |
I think the best solution is for For your example it would look like: <SelectField
options={users.map((u) => ({
...o,
value: u.id,
label: u.name,
}))}
search={async (searchText) => {
const words = searchText?.toLowerCase().split(' ') ?? [];
return users.filter(user => {
const label = [user.name, user.email].filter(Boolean).join(' ');
return words.every((word) => label.toLowerCase().includes(word));
}
}}
>
<svelte:fragment slot="option" let:option={user}>
<MenuItem>
<div data-id={user.id}>{user.name} ({user.email})</div>
</MenuItem>
</svelte:fragment>
</SelectField>
/> and an API search could look like <SelectField
options={users.map((u) => ({
...o,
value: u.id,
label: u.name,
}))}
search={async (searchText) => {
// Example API call but can be structured many ways
const results = await api('/search', { searchText });
// You probably need to `results.map(...)` to `MenuOption` `{ label, value }` depending on the response
return results;
}}
>
<svelte:fragment slot="option" let:option={user}>
<MenuItem>
<div data-id={user.id}>{user.name} ({user.email})</div>
</MenuItem>
</svelte:fragment>
</SelectField>
/> |
@techniq I think that makes a lot of sense. I think it could be useful to include the current list of options as the second arg in that callback, for the same reason that most This way, if the options array is constructed inline (as shown in my users example above), the final constructed object would also be available for use as the second parameter. What did you think about my idea for abstracting away that common search-filter pattern to a helper function util? That would look like this: <SelectField
options={users.map((u) => ({
...o,
value: u.id,
label: u.name,
}))}
search={searchFor((user) => [user.name, user.email])}
search={async (searchText, users) => {
const words = searchText?.toLowerCase().split(' ') ?? [];
/* Option 1️⃣: `searchFor` returns the filter callback */
return users.filter(searchFor((user) => [user.name, user.email]));
/* Option 2️⃣: `searchFor` explicitly works inside the filter callback */
return users.filter((user) => searchFor([user.name, user.email]));
}}
>
<svelte:fragment slot="option" let:option={user}>
<MenuItem>
<div data-id={user.id}>{user.name} ({user.email})</div>
</MenuItem>
</svelte:fragment>
</SelectField> I think could that whichever one of these two options yields the most reusable value could be worth including in utils, even in |
Passing |
I dig it. I'll update my PR per that spec. |
- update `search` prop logic - remove `searchLabel` - rename `inlineSearch` -> `search` - add support in `MultiSelect` for passing custom search function into `search` prop
Made some updates to align with that new vision
|
searchLabel
property in SelectField
and MultiSelect
SelectField
and MultiSelect
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.
Looks good, thanks @brandonmcconnell!
Thanks, @techniq! |
This change adds for overriding searchable text in
SelectField
andMultiSelect
components with newsearchLabel
property on theMenuOption
type