List field
Search a list and add entries from one field. Paste a column to add several entries, and recover your text if adding fails. Built on List item.
<script setup lang="ts">import { computed, ref } from 'vue'import { UiListField, UiListItem, UiBadge, UiSwitch } from 'grani' const names = ref<string[]>([ 'Fruit', 'Dairy', 'Bakery', 'Vegetables', 'Meat', 'Fish', 'Frozen food',]) const query = ref('') const fails = ref(false) const lit = ref<string[]>([]) let dim: ReturnType<typeof setTimeout> | undefined const matching = computed(() => { const q = query.value.trim().toLowerCase() return q ? names.value.filter((n) => n.toLowerCase().includes(q)) : names.value}) // What was added here stands on top, marked New, until the list is left.const ordered = (added: string[]) => [ ...matching.value.filter((n) => added.includes(n)), ...matching.value.filter((n) => !added.includes(n)),] // A row lights for a moment when it comes, or when a repeat points at it.function light(items: string[]) { clearTimeout(dim) lit.value = items dim = setTimeout(() => (lit.value = []), 1200)} // Each item is a request; the promise holds the field, a rejection gives the text back.const add = (items: string[]) => new Promise<void>((resolve, reject) => setTimeout(() => { if (fails.value) return reject(new Error('offline')) names.value.push(...items) light(items) resolve() }, 600), )</script> <template> <div class="flex w-full max-w-sm flex-col gap-4"> <UiListField v-slot="{ added }" v-model:query="query" placeholder="Find or add a subcategory" :existing="names" @add="add" @taken="(name) => light([name])" > <UiListItem v-for="name in ordered(added)" :key="name" as="button" size="sm" step :title="name" :highlight="query" :lit="lit.includes(name)" > <template v-if="added.includes(name)" #actions> <UiBadge label="New" tone="success" size="sm" /> </template> </UiListItem> </UiListField> <UiSwitch v-model="fails">The server fails</UiSwitch> </div></template>
Also called tag input, token input, chips input, multi-value field.
Usage
<UiListField>…</UiListField> With GraniResolver in the Vite config, a template needs no import: set it up once.
API
| Name · type | Default | Description |
|---|---|---|
| Value | ||
| querystring= "" | "" | The text in the field (v-model:query): what the list is filtered by, and what “Add …” adds. |
| State | ||
| emptystring= "" | "" | The line of an empty list; defaults to "Nothing here yet: type a name and press Enter". |
| Behavior | ||
| searchboolean= true | true | Typing filters the list and Enter opens the first match. Off, the field only adds: a short list inside a form, where every row is in view. |
| existingstring[]= [] | [] | The names in the list: an exact one, in any case, is found, not offered again. |
| splitlines | lists= "lines" | "lines" | lines: a pasted list splits by line. lists: also by commas and semicolons, typed or pasted. |
| fix((item: string) => string) | Tidies an item on its way in: (s) => s.toLowerCase(). A change is named under the field. | |
| check((item: string) => string | undefined) | Why an item cannot be, in the product's words, or nothing when it can. | |
| limitnumber | The most the list holds, counted from existing: full, the field is gone. | |
| onAdd((items: string[]) => unknown) | “Add …”, Enter or a pasted list (@add): the items, tidied, checked, without repeats. Return the request's promise to hold the field; a rejection gives the text back. Nothing returned, the field is not held. Put the new names into existing before the promise settles: repeats and limit are counted from it. | |
| Look | ||
| placeholderstring= "" | "" | The hint in the empty field: "Find or add a subcategory". Defaults to "Find or add…". |
| sizexs | sm | md | Step of the field's row, as a list row's: 44 px at xs and sm, 56 at md. Unset, the app's size (createGrani({ size })), sm by default. | |
| Form and access | ||
| ariaLabelstring= "" | "" | The field's name for a screen reader; defaults to the placeholder. |
| disabledboolean= false | false | Nothing can be typed, added or cleared: the field, Add and the cross are held, on the disabled plate. |
| idstring | Own id of the input when standalone. Inside UiField the field's id takes precedence. | |
Attributes: class and style stay on the outer element; the rest (native attributes, aria-*, data-testid) reach the control inside it.
When to use another component
| When you need | Take |
|---|---|
| A list with a toolbar and pages | Data table |
| Picks from a fixed set | Select multiple |
| One text value | Input |
| Rows that nobody adds here | List item rows in a List |