Dynamic
Dynamic render tags inside an input, followed by an actual text input. By default, tags are added when text is typed in the input field and the
EnterorCommakey is pressed. Throughout the interaction, DOM focus remains on the input element.
Features
Install
Install the component from your command line.
Anatomy
Import all parts and piece them together.
<script setup lang="ts">import * as dynamic from '@destyler/dynamic'import { normalizeProps, useMachine } from '@destyler/vue'import { computed, useId } from 'vue'
const [state, send] = useMachine( dynamic.machine({ id: useId(), value: ['React', 'Vue'], }),)const api = computed(() => dynamic.connect(state.value, send, normalizeProps),)</script>
<template> <div v-bind="api.getRootProps()" > <span v-for="(value, index) in api.value" :key="index" v-bind="api.getItemProps({ index, value })" > <div v-bind="api.getItemPreviewProps({ index, value })"> <button v-bind="api.getItemDeleteTriggerProps({ index, value })" /> </div> <input v-bind="api.getItemInputProps({ index, value })"> </span> <input v-bind="api.getInputProps()"> </div></template>import * as dynamic from '@destyler/dynamic'import { normalizeProps, useMachine } from '@destyler/react'import { useId } from 'react'
export default function DynamicPage() { const [state, send] = useMachine( dynamic.machine({ id: useId(), value: ['React', 'Vue'], }), )
const api = dynamic.connect(state, send, normalizeProps)
return ( <> <div {...api.getRootProps()}> {api.value.map((value, index) => ( <span key={index} {...api.getItemProps({ index, value })} > <div {...api.getItemPreviewProps({ index, value })} > <button {...api.getItemDeleteTriggerProps({ index, value })} /> </div> <input {...api.getItemInputProps({ index, value })}/> </span> ))} <input {...api.getInputProps()} /> </div> </> )}<script lang="ts"> import * as dynamic from "@destyler/dynamic"; import { normalizeProps, useMachine } from "@destyler/svelte";
const [state, send] = useMachine( dynamic.machine({ id: crypto.randomUUID(), value: ["Svelte", "Vue"], }), );
const api = $derived(dynamic.connect(state, send, normalizeProps));</script>
<div {...api.getRootProps()}> {#each api.value as value, index (index)} <span {...api.getItemProps({ index, value })}> <div {...api.getItemPreviewProps({ index, value })} > <button {...api.getItemDeleteTriggerProps({ index, value })}></button> </div> <input {...api.getItemInputProps({ index, value })} /> </span> {/each} <input {...api.getInputProps()}/></div>import * as dynamic from '@destyler/dynamic'import { normalizeProps, useMachine } from '@destyler/solid'import { createMemo, createUniqueId, For } from 'solid-js'
export default function DynamicPage() { const [state, send] = useMachine( dynamic.machine({ id: createUniqueId(), value: ['Solid', 'Vue'], }), )
const api = createMemo(() => dynamic.connect(state, send, normalizeProps))
return ( <> <div {...api().getRootProps()}> <For each={api().value}> {(value, index) => ( <span {...api().getItemProps({ index: index(), value })}> <div {...api().getItemPreviewProps({ index: index(), value })}> <button {...api().getItemDeleteTriggerProps({ index: index(), value })} /> </div> <input {...api().getItemInputProps({ index: index(), value })} /> </span> )} </For> <input {...api().getInputProps()} /> </div> </> )}Removing all tags
The tags input will remove all tags when the clear button is clicked.
To remove all tags, use the provided clearButtonProps function from the api.
<template> <!-- ... --> <div v-bind="api.getControlProps()"> <input v-bind="api.getInputProps()" /> <button v-bind="api.getClearButtonProps()" /> </div> <!-- ... --></template>//...<div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getClearButtonProps()} /></div>//...<!-- ... --><div {...api.getControlProps()}> <input {...api.getInputProps()} /> <button {...api.getClearButtonProps()} ></button></div><!-- ... -->//...<div {...api().getControlProps()}> <input {...api().getInputProps()} /> <button {...api().getClearButtonProps()} /></div>//...To programmatically remove all tags, use the api.clearAll() method that’s available in the connect.
Usage within forms
The tags input works when placed within a form and the form is submitted. We achieve this by:
-
ensuring we emit the input event as the value changes.
-
adding a
nameandvalueattribute to a hidden input so the tags can be accessed in theFormData.
To get this feature working you need to pass a name option to the context and render the hiddenInput element.
const [state, send] = useMachine( tagsInput.machine({ name: "tags", }),)Limiting the number of tags
To limit the number of tags within the component,
you can set the max property to the limit you want.
The default value is Infinity.
When the tag reaches the limit,
new tags cannot be added except the allowOverflow option is passed to the context.
const [state, send] = useMachine( tagsInput.machine({ max: 10, allowOverflow: true, }),)Validating Tags
Before a tag is added, the machine provides a validate function you can use to determine whether to accept or reject a tag.
A common use-case for validating tags is preventing duplicates or validating the data type.
const [state, send] = useMachine( tagsInput.machine({ validate(details) { return !details.values.includes(details.inputValue) }, }),)Blur behavior
When the tags input is blurred, you can configure the action the machine should take by passing the blurBehavior option to the context.
-
"add"— Adds the tag to the list of tags. -
"clear"— Clears the tags input value.
const [state, send] = useMachine( tagsInput.machine({ blurBehavior: "add", }),)Paste behavior
To add a tag when a arbitrary value is pasted in the input element,
pass the addOnPaste option.
When a value is pasted, the machine will:
-
check if the value is a valid tag based on the
validateoption -
split the value by the
delimiteroption passed
const [state, send] = useMachine( tagsInput.machine({ addOnPaste: true, }),)Disable tag editing
by default the tags can be edited by double clicking on the tag or focusing on them and pressing Enter.
To disable this behavior, pass the allowEditTag: false.
const [state, send] = useMachine( tagsInput.machine({ allowEditTag: false, }),)Listening for events
During the lifetime of the tags input interaction, here’s a list of events we emit:
-
onValueChange— invoked when the tag value changes. -
onHighlightChange— invoked when a tag has visual focus. -
onValueInvalid— invoked when the max tag count is reached or thevalidatefunction returnsfalse.
const [state, send] = useMachine( tagsInput.machine({ onValueChange(details) { // details => { value: string[] } console.log("tags changed to:", details.value) }, onHighlightChange(details) { // details => { value: string } console.log("highlighted tag:", details.value) }, onValueInvalid(details) { console.log("Invalid!", details.reason) }, }),)Styling guide
Earlier, we mentioned that each Dynamic part has a
data-partattribute added to them to select and style them in the DOM.
Focused state
The combobox input is focused when the user clicks on the input element. In this focused state, the root, label, input.
[data-part="root"][data-focus] { /* styles for root focus state */}
[data-part="label"][data-focus] { /* styles for label focus state */}
[data-part="input"]:focus { /* styles for input focus state */}Invalid state
When the tags input is invalid by setting the invalid: true in the destyler’s context, the data-invalid attribute is set on the root, input, control, and label.
[data-part="root"][data-invalid] { /* styles for invalid state for root */}
[data-part="label"][data-invalid] { /* styles for invalid state for label */}
[data-part="input"][data-invalid] { /* styles for invalid state for input */}Disabled state
When the tags input is disabled by setting the disabled: true in the destyler’s context,
the data-disabled attribute is set on the root, input, control and label.
[data-part="root"][data-disabled] { /* styles for disabled state for root */}
[data-part="label"][data-disabled] { /* styles for disabled state for label */}
[data-part="input"][data-disabled] { /* styles for disabled state for input */}
[data-part="control"][data-disabled] { /* styles for disabled state for control */}When a tag is disabled, the data-disabled attribute is set on the tag.
[data-part="item-preview"][data-disabled] { /* styles for disabled tag */}Highlighted state
When a tag is highlighted via the keyboard navigation or pointer hover,
a data-highlighted attribute is set on the tag.
[data-part="item-preview"][data-highlighted] { /* styles for visual focus */}Readonly state
When the tags input is in its readonly state,
the data-readonly attribute is set on the root, label, input and control.
[data-part="root"][data-readonly] { /* styles for readonly for root */}
[data-part="control"][data-readonly] { /* styles for readonly for control */}
[data-part="input"][data-readonly] { /* styles for readonly for input */}
[data-part="label"][data-readonly] { /* styles for readonly for label */}Methods and Properties
Machine Context
The dynamic machine exposes the following context properties:
Partial<{ root: string; input: string; hiddenInput: string; clearBtn: string; label: string; control: string; item: (opts: ItemProps) => string; itemDeleteTrigger: (opts: ItemProps) => string; itemInput: (opts: ItemProps) => string; }>IntlTranslationsnumberstring | RegExpbooleanbooleanbooleanbooleanbooleanbooleanstringstring[](details: ValueChangeDetails) => void(details: InputValueChangeDetails) => void(details: HighlightChangeDetails) => void(details: ValidityChangeDetails) => void(details: ValidateArgs) => boolean"clear" | "add"booleannumberbooleanstringstring"ltr" | "rtl"string() => ShadowRoot | Node | Document(event: PointerDownOutsideEvent) => void(event: FocusOutsideEvent) => void(event: InteractOutsideEvent) => voidMachine API
The dynamic api exposes the following methods:
booleanstringstring[]stringnumberboolean(value: string[]) => void(id?: string) => void(value: string) => void(index: number, value: string) => void(value: string) => void() => void() => void(props: ItemProps) => ItemStateData Attributes
Root
data-scopedata-partdata-invaliddata-readonlydata-disableddata-focusdata-emptyLabel
data-scopedata-partdata-disableddata-invaliddata-readonlyControl
data-scopedata-partdata-disableddata-readonlydata-invaliddata-focusInput
data-scopedata-partdata-invaliddata-readonlyItem
data-scopedata-partdata-valuedata-disabledItemPreview
data-scopedata-partdata-valuedata-disableddata-highlightedItemText
data-scopedata-partdata-disableddata-highlightedClearTrigger
data-scopedata-partdata-readonlyAccessibility
Keyboard Interaction
ArrowLeftArrowRightBackspaceEnterDeleteControl + V