1.0.6 • Published 3 years ago
svelte-selecta v1.0.6
Svelte Selecta
npm install svelte-selecta # or yarn add svelte-selecta
<script lang="ts">
import clsx from 'clsx';
import { onDestroy, onMount, tick } from 'svelte';
import { copyArray, filterOptions, float, outside } from 'svelte-selecta';
// generals
export let options: any[] = [];
export let search = '';
export let selectedObject: any = undefined;
// generals
// option: keys
export let keyOptionLabel: string;
export let keyOptionValue: string;
// option: keys
// select: decorators
export let label: string;
export let placeholder: string | undefined = undefined;
// select: decorators
// select: styles
export let fullWidth: boolean;
// select: styles
// callbacks
export let getOptionLabel: ((option: any) => string) | undefined = undefined;
export let onSelect: ((option: any) => void) | undefined = undefined;
// callbacks
// svelte-forma props, npm i svelte-forma
export let name: string;
export let validators: any = () => {};
export let rules: ((value: any) => boolean | string)[] | undefined = undefined;
// svelte-forma props
onMount(() => {
if (getOptionLabel) {
options = options.map((option) => {
option.label = getOptionLabel && getOptionLabel(option);
return option;
});
}
});
// vars
let defaultInputLogic = true;
let inputEl: HTMLInputElement | undefined;
let menuEl: HTMLElement | undefined;
let cleanup: any;
let open = false;
// vars
// reactive
$: if (selectedObject === null) {
// remote cleanup
search = '';
}
$: filteredOptions = [...options];
$: if (inputEl && menuEl) {
cleanup = float(inputEl, menuEl);
}
// reactive
const onInput = (e: Event) => {
const target = e.target as HTMLInputElement;
if (defaultInputLogic) {
// onOptionClick dispatch native event
open = true;
search = target.value;
filteredOptions = filterOptions(search, copyArray(options), keyOptionLabel);
} else {
// default logic
defaultInputLogic = true;
search = target.value;
}
};
const onOptionClick = async (option: any) => {
selectedObject = option;
search = option?.[keyOptionLabel];
open = false;
filteredOptions = copyArray(options);
inputEl?.focus(); // good ux
// run native validation
await tick();
const e = new Event('input', { bubbles: true });
defaultInputLogic = false;
inputEl?.dispatchEvent(e);
// run native validation
onSelect && onSelect(option);
};
const onInputClick = () => {
// normal behavior
open = true;
};
const onOutsideClick = () => {
open = false;
if (search !== selectedObject?.[keyOptionLabel]) {
// cancel search
search = '';
selectedObject = undefined;
filteredOptions = copyArray(options);
}
};
onDestroy(() => {
cleanup && cleanup();
});
</script>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class={clsx('flex flex-col', fullWidth && 'w-full')}>
{#if label}
<span class=" text-sm mb-2 font-medium">{label}</span>
{/if}
<div
class={clsx('relative', fullWidth && 'w-full')}
use:outside
on:click:outside={onOutsideClick}
>
<input
use:validators={rules}
{name}
tabindex="0"
value={search}
bind:this={inputEl}
autocomplete="off"
{placeholder}
on:click|preventDefault|stopPropagation={onInputClick}
on:input={onInput}
class={clsx(
' outline-none border-2 border-blue-500 mb-1 px-2 py-1 focus:ring-2 focus:ring-blue-500 rounded-md',
fullWidth && 'w-full'
)}
/>
{#if open && filteredOptions.length > 0}
<div
bind:this={menuEl}
class="menu absolute max-h-[20rem] overflow-auto w-full border border-gray-200 bg-white shadow-md py-1 rounded-md z-[9999]"
>
{#each filteredOptions as option}
<div
tabindex="0"
on:click|preventDefault|stopPropagation={() => onOptionClick(option)}
class={clsx(
' cursor-pointer px-4 py-2 text-sm hover:bg-gray-100',
selectedObject?.[keyOptionValue] === option?.[keyOptionValue] && 'bg-blue-100'
)}
>
{#if option.highlight}
<span>{@html option.highlight}</span>
{:else}
<span>{@html option?.[keyOptionLabel]}</span>
{/if}
</div>
{/each}
</div>
{/if}
</div>
</label>
Usage
<Autocomplete bind:selectedObject={selectedObject} bind:search={search}
getOptionLabel={(option) => `${option.prop1} ${option.prop2}`} // optional
options={options} keyOptionLabel="label" keyOptionValue="id"
placeholder="Choose..." name="name" // svelte-forma (optional) {validators} //
svelte-forma (optional) rules={[(value) => options.some((a) => a.label ===
value) || 'Field is required']} // svelte-forma (optional) label="Label"
fullWidth onSelect={(option) => { console.log(option) }} />