Skip to content
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

GitHub Feature Request: Add Searchable Dropdown to Form Field #1592

Closed
1 task done
good-dev-student opened this issue Jan 1, 2025 · 2 comments
Closed
1 task done
Labels
type: feature Introduction of new functionality to the application

Comments

@good-dev-student
Copy link

Prerequisites

  • This feature already exists in shadcn/ui - if not, it won't be considered here so don't continue with your issue.

Describe the feature

Prerequisites
Ensure the feature is not already implemented in shadcn/ui. If it is, this feature request will not be considered.
This feature is intended to remain aligned with the design and philosophy of shadcn/ui. If it is outside the scope of that project, it may not be accepted.


Description
Feature: This feature adds a searchable dropdown to the form field component, improving the user experience when selecting from large lists. The searchable dropdown will allow users to filter through options by typing into a search bar within the dropdown. The dropdown will show filtered results based on the user's input and allow them to select an item from the list.

Key Features:
Searchable Input: Users can type into a search bar to filter options based on the name or code properties of items.
Clear Search: A button to clear the search input will be available to reset the filter.
Flexible Integration: This feature can be integrated into existing forms with minimal changes.
Customizable: The search functionality can be customized to search different fields based on the structure of the list passed to the component.


Code Implementation
The feature can be implemented in Svelte using the following code:

Copy code :

<script lang="ts">
	import { capitalizeFirstLetter } from "$lib/app/utils/text/capitalizeFirstLetter";
	import * as Form from "$lib/components/ui/form";
	import * as Select from "$lib/components/ui/select";
	import { Input } from "$lib/components/ui/input";
	import { Button } from "$lib/components/ui/button"; 
	import { cn } from "$lib/utils";
	import type { SuperForm } from "sveltekit-superforms";

	type Props = {
		formData: any;
		form: SuperForm<any, any>;
		name: string;
		label: string;
		description?: string;
		list: any[]; // List of options to choose from
		class?: string; // Optional class for styling
	};

	let { form, formData, name, label, list, description, class: klass }: Props = $props();

	let searchTerm = $state(""); // Search term to filter options
	let filteredList = $derived(
		list.filter((item) => 
			item.name?.toLowerCase().includes(searchTerm.toLowerCase()) || 
			item.code?.toLowerCase().includes(searchTerm.toLowerCase())
		)
	);

	// Function to clear the search term
	function clearSearch() {
		searchTerm = "";
	}
</script>

<Form.Field {form} {name}>
	<Form.Control>
		{#snippet children({ props })}

			<Form.Label class="cursor-pointer select-none">{capitalizeFirstLetter(label)}</Form.Label>

			<Select.Root type="single" bind:value={$formData[name]} {name}>
				<Select.Trigger class={cn("min-w-40 w-full", klass)} {...props}>
					{list.find((item) => item.code === $formData[name])?.name ?? "Select an option"}
				</Select.Trigger>

				<Select.Content>
					<div class="sticky top-0 p-2 bg-popover border-b border-border">
						<div class="relative">
							<Input
								type="search"
								placeholder="Search..."
								bind:value={searchTerm}
								class="h-8 pr-8"
							/>
							{#if searchTerm}
								<Button
									variant="ghost"
									size="sm"
									class="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
									onclick={clearSearch}
								>
									×
								</Button>
							{/if}
						</div>
					</div>

					<Select.Group class="h-[300px] overflow-y-auto">
						{#if filteredList.length === 0}
							<div class="flex items-center justify-center h-full text-muted-foreground">
								No results found
							</div>
						{:else}
							{#each filteredList as item}
								<Select.Item value={item.code} label={item.name}>
									{item.name}
								</Select.Item>
							{/each}
						{/if}
					</Select.Group>
				</Select.Content>
			</Select.Root>

		{/snippet}
	</Form.Control>

	{#if description}
		<Form.Description>{description}</Form.Description>
	{/if}
	<Form.FieldErrors />
</Form.Field>

How It Works:
Search Input: A search bar is added at the top of the dropdown. When the user types, the list of items will be filtered based on the search term.
Clear Search: If the user enters a search term, a clear button (×) will appear to reset the search and show all available options again.
Displaying Items: The filtered list of items will be displayed inside the dropdown, and users can select from those options.
Benefits:
Enhanced User Experience: With large lists, users often find it difficult to locate the option they need. The searchable dropdown reduces this friction.
Customizable: The code allows customization of the search criteria, making it adaptable to various data structures.
Improved Accessibility: By supporting search functionality, users with specific preferences or needs will find it easier to navigate options.

Contribution
If you'd like to contribute to this feature, feel free to submit a pull request! Here's how you can implement it:

Clone the repository.
Add the provided code to the form component.
Test the feature with different lists and verify it works as expected.
Update any necessary documentation.
Thanks for your interest in improving this project!

@good-dev-student good-dev-student added the type: feature Introduction of new functionality to the application label Jan 1, 2025
@good-dev-student
Copy link
Author

new version with focus:

<script lang="ts">
	import { capitalizeFirstLetter } from "$lib/app/utils/text/capitalizeFirstLetter";
	import * as Form from "$lib/components/ui/form";
	import * as Select from "$lib/components/ui/select";
	import { Input } from "$lib/components/ui/input";
	import { Button } from "$lib/components/ui/button";
	import { cn } from "$lib/utils";
	import type { SuperForm } from "sveltekit-superforms";

	type Props = {
		formData: any;
		form: SuperForm<any, any>;
		name: string;
		label: string;
		description?: string;
		list: any[];
		class?: string;
	};

	let { form, formData, name, label, list, description, class: klass }: Props = $props();

	let open = $state(false);

	let searchTerm = $state("");
	let filteredList = $derived(
		list.filter(
			(item) =>
				item.name?.toLowerCase().includes(searchTerm.toLowerCase()) ||
				item.code?.toLowerCase().includes(searchTerm.toLowerCase())
		)
	);

	function clearSearch() {
		searchTerm = "";
	}

	let searchInput: any = $state(null);

	$effect(() => {
        // Focus the search input when the dropdown is opened
        if (open && searchInput) {
            setTimeout(() => searchInput.focus(), 50);
        }
    });
</script>

<Form.Field {form} {name}>
	<Form.Control>
		{#snippet children({ props })}
			<Form.Label class="cursor-pointer select-none">{capitalizeFirstLetter(label)}</Form.Label>
			<Select.Root type="single" bind:value={$formData[name]} {name} bind:open>
				<Select.Trigger class={cn("w-full min-w-40", klass)} {...props}>
					{list.find((item) => item.code === $formData[name])?.name ?? "Select an option"}
				</Select.Trigger>
				<Select.Content>
					<div class="sticky top-0 border-b border-border bg-popover p-2">
						<div class="relative">
							<Input
								bind:ref={searchInput}
								id="fuck"
								type="search"
								placeholder="Search..."
								bind:value={searchTerm}
								class="h-8 pr-8"
								autofocus={true}
								tabindex={0}
							/>
							{#if searchTerm}
								<Button
									variant="ghost"
									size="sm"
									class="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
									onclick={clearSearch}
								>
									×
								</Button>
							{/if}
						</div>
					</div>
					<Select.Group class="h-[300px] overflow-y-auto">
						{#if filteredList.length === 0}
							<div class="flex h-full items-center justify-center text-muted-foreground">
								No results found
							</div>
						{:else}
							{#each filteredList as item}
								<Select.Item value={item.code} label={item.name}>
									{item.name}
								</Select.Item>
							{/each}
						{/if}
					</Select.Group>
				</Select.Content>
			</Select.Root>
		{/snippet}
	</Form.Control>

	{#if description}
		<Form.Description>{description}</Form.Description>
	{/if}
	<Form.FieldErrors />
</Form.Field>

@good-dev-student
Copy link
Author

idk if this will be good option i will close the make @huntabyte concentrate on more important thinks though

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: feature Introduction of new functionality to the application
Projects
None yet
Development

No branches or pull requests

1 participant