1.0.6 • Published 3 years ago

svelte-selecta v1.0.6

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

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) }} />
1.0.6

3 years ago

1.0.5

3 years ago

1.0.4

3 years ago

1.0.3

3 years ago

1.0.2

3 years ago

1.0.0

3 years ago