Skip to content

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 · typeDefaultDescription
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= truetrueTyping 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.
limitnumberThe 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 | mdStep 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= falsefalseNothing can be typed, added or cleared: the field, Add and the cross are held, on the disabled plate.
idstringOwn 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 needTake
A list with a toolbar and pagesData table
Picks from a fixed setSelect multiple
One text valueInput
Rows that nobody adds hereList item rows in a List