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
Enter
orComma
key 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
name
andvalue
attribute 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
validate
option -
split the value by the
delimiter
option 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 thevalidate
function 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-part
attribute 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; }>
IntlTranslations
number
string | RegExp
boolean
boolean
boolean
boolean
boolean
boolean
string
string[]
(details: ValueChangeDetails) => void
(details: InputValueChangeDetails) => void
(details: HighlightChangeDetails) => void
(details: ValidityChangeDetails) => void
(details: ValidateArgs) => boolean
"clear" | "add"
boolean
number
boolean
string
string
"ltr" | "rtl"
string
() => ShadowRoot | Node | Document
(event: PointerDownOutsideEvent) => void
(event: FocusOutsideEvent) => void
(event: InteractOutsideEvent) => void
Machine API
The dynamic api
exposes the following methods:
boolean
string
string[]
string
number
boolean
(value: string[]) => void
(id?: string) => void
(value: string) => void
(index: number, value: string) => void
(value: string) => void
() => void
() => void
(props: ItemProps) => ItemState
Data Attributes
Root
data-scope
data-part
data-invalid
data-readonly
data-disabled
data-focus
Label
data-scope
data-part
data-disabled
data-invalid
data-readonly
Control
data-scope
data-part
data-disabled
data-readonly
data-invalid
data-focus
Input
data-scope
data-part
data-invalid
data-readonly
Item
data-scope
data-part
data-value
data-disabled
ItemPreview
data-scope
data-part
data-value
data-disabled
data-highlighted
ItemText
data-scope
data-part
data-disabled
data-highlighted
ClearTrigger
data-scope
data-part
data-readonly
Accessibility
Keyboard Interaction
ArrowLeft
ArrowRight
Backspace
Enter
Delete
Control + V