Skip to content
Destyler UI Destyler UI Destyler UI

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 or Comma key is pressed. Throughout the interaction, DOM focus remains on the input element.

React
Vue
React
Vue
Svelte
Vue
Solid
Vue

Features

Install

Install the component from your command line.

Terminal window
      
        
npm install @destyler/{dynamic,vue}
Terminal window
      
        
npm install @destyler/{dynamic,react}
Terminal window
      
        
npm install @destyler/{dynamic,svelte}
Terminal window
      
        
npm install @destyler/{dynamic,solid}

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 and value attribute to a hidden input so the tags can be accessed in the FormData.

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 the validate function returns false.

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:

ids
Partial<{ root: string; input: string; hiddenInput: string; clearBtn: string; label: string; control: string; item(opts: ItemProps): string; }>
The ids of the elements in the dynamic. Useful for composition.
translations
IntlTranslations
Specifies the localized strings that identifies the accessibility elements and their states
maxLength
number
The max length of the input.
delimiter
string | RegExp
The character that serves has: - event key to trigger the addition of a new tag - character used to split tags when pasting into the input
autoFocus
boolean
Whether the input should be auto-focused
disabled
boolean
Whether the dynamic should be disabled
readOnly
boolean
Whether the dynamic should be read-only
invalid
boolean
Whether the dynamic is invalid
required
boolean
Whether the dynamic is required
editable
boolean
Whether a tag can be edited after creation, by pressing `Enter` or double clicking.
inputValue
string
The tag input's value
value
string[]
The tag values
onValueChange
(details: ValueChangeDetails) => void
Callback fired when the tag values is updated
onInputValueChange
(details: InputValueChangeDetails) => void
Callback fired when the input value is updated
onHighlightChange
(details: HighlightChangeDetails) => void
Callback fired when a tag is highlighted by pointer or keyboard navigation
onValueInvalid
(details: ValidityChangeDetails) => void
Callback fired when the max tag count is reached or the `validateTag` function returns `false`
validate
(details: ValidateArgs) => boolean
Returns a boolean that determines whether a tag can be added. Useful for preventing duplicates or invalid tag values.
blurBehavior
"clear" | "add"
The behavior of the dynamic when the input is blurred - `"add"`: add the input value as a new tag - `"clear"`: clear the input value
addOnPaste
boolean
Whether to add a tag when you paste values into the tag input
max
number
The max number of tags
allowOverflow
boolean
Whether to allow tags to exceed max. In this case, we'll attach `data-invalid` to the root
name
string
The name attribute for the input. Useful for form submissions
form
string
The associate form of the underlying input element.
dir
"ltr" | "rtl"
The document's text/writing direction.
id
string
The unique identifier of the machine.
getRootNode
() => ShadowRoot | Node | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the component
onFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the component
onInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component

Machine API

The dynamic api exposes the following methods:

empty
boolean
Whether the tags are empty
inputValue
string
The value of the tags entry input.
value
string[]
The value of the tags as an array of strings.
valueAsString
string
The value of the tags as a string.
count
number
The number of the tags.
atMax
boolean
Whether the tags have reached the max limit.
setValue
(value: string[]) => void
Function to set the value of the tags.
clearValue
(id?: string) => void
Function to clear the value of the tags.
addValue
(value: string) => void
Function to add a tag to the tags.
setValueAtIndex
(index: number, value: string) => void
Function to set the value of a tag at the given index.
setInputValue
(value: string) => void
Function to set the value of the tags entry input.
clearInputValue
() => void
Function to clear the value of the tags entry input.
focus
() => void
Function to focus the tags entry input.
getItemState
(props: ItemProps) => ItemState
Returns the state of a tag

Data Attributes

Root

name
desc
data-scope
tags-input
data-part
root
data-invalid
Present when invalid
data-readonly
Present when read-only
data-disabled
Present when disabled
data-focus
Present when focused

Label

name
desc
data-scope
tags-input
data-part
label
data-disabled
Present when disabled
data-invalid
Present when invalid
data-readonly
Present when read-only

Control

name
desc
data-scope
tags-input
data-part
control
data-disabled
Present when disabled
data-readonly
Present when read-only
data-invalid
Present when invalid
data-focus
Present when focused

Input

name
desc
data-scope
tags-input
data-part
input
data-invalid
Present when invalid
data-readonly
Present when read-only

Item

name
desc
data-scope
tags-input
data-part
item
data-value
The value of the item
data-disabled
Present when disabled

ItemPreview

name
desc
data-scope
tags-input
data-part
item-preview
data-value
The value of the item
data-disabled
Present when disabled
data-highlighted
Present when highlighted

ItemText

name
desc
data-scope
tags-input
data-part
item-text
data-disabled
Present when disabled
data-highlighted
Present when highlighted

ClearTrigger

name
desc
data-scope
tags-input
data-part
clear-trigger
data-readonly
Present when read-only

Accessibility

Keyboard Interaction

name
desc
ArrowLeft
Moves focus to the previous tag item
ArrowRight
Moves focus to the next tag item
Backspace
Deletes the tag item that has visual focus or the last tag item
Enter
When a tag item has visual focus, it puts the tag in edit mode. When the input has focus, it adds the value to the list of tags
Delete
Deletes the tag item that has visual focus
Control + V
When `addOnPaste` is set. Adds the pasted value as a tags