Tag List ​
A dynamic list of tags shows a remaining counter if all tags cannot be shown in 1 line
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HTagList } from '@holistics/design-system'
const TAGS = [
'Holistics',
'Design System',
'is',
'simply',
'the best', '!!!',
] as const
const tags = ref<string[]>(TAGS.slice())
const tagToAdd = ref<string>('')
function addTag () {
tags.value.push(tagToAdd.value)
tagToAdd.value = ''
}
function addMany () {
tags.value.push(...Array.from({ length: 10000 }, (_, i) => (i + 1).toString()))
}
function removeMany () {
const i = tags.value.findIndex((t) => t === '1')
tags.value.splice(i, 10000)
}
</script>
<template>
<div class="w-80">
<div class="rounded border border-solid px-[7px] py-[5px]">
<HTagList v-model="tags" />
</div>
<div class="mt-4 flex items-center justify-center space-x-4">
<input
v-model="tagToAdd"
type="text"
class="h-8 min-w-0 flex-1 rounded border border-solid border-gray-300 px-2 py-1 text-sm focus:ring-1 focus:ring-blue-200"
placeholder="Add more tag..."
@keydown.enter="addTag"
>
<HButton
type="secondary-default"
label="Refresh"
icon="refresh"
@click="tags = TAGS.slice()"
/>
</div>
<div class="mt-2 flex items-center justify-center space-x-4">
<HButton
type="outline-success"
label="Add many"
icon="add"
class="flex-1"
@click="addMany"
/>
<HButton
type="outline-danger"
label="Remove many"
icon="cancel"
class="flex-1"
@click="removeMany"
/>
</div>
</div>
</template>Examples ​
Closable ​
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HTagList } from '@holistics/design-system'
const TAGS = [
'Holistics',
'Design System',
'is',
'simply',
'the best', '!!!',
] as const
const tags = ref<string[]>(TAGS.slice())
const tagToAdd = ref<string>('')
function addTag () {
tags.value.push(tagToAdd.value)
tagToAdd.value = ''
}
function addManyTags () {
tags.value.push(...Array.from({ length: 10000 }, (_, i) => (i + 1).toString()))
}
function removeManyTags () {
const i = tags.value.findIndex((t) => t === '1')
tags.value.splice(i, 10000)
}
</script>
<template>
<div class="w-80">
<div class="rounded border border-solid px-[7px] py-[5px]">
<HTagList
v-model="tags"
closeable
/>
</div>
<div class="mt-4 flex items-center justify-center space-x-4">
<input
v-model="tagToAdd"
type="text"
class="h-8 min-w-0 flex-1 rounded border border-solid border-gray-300 px-2 py-1 text-sm focus:ring-1 focus:ring-blue-200"
placeholder="Add more tag..."
@keydown.enter="addTag"
>
<HButton
type="secondary-default"
label="Refresh"
icon="refresh"
@click="tags = TAGS.slice()"
/>
</div>
<div class="mt-2 flex items-center justify-center space-x-4">
<HButton
type="outline-success"
label="Add many"
icon="add"
class="flex-1"
@click="addManyTags"
/>
<HButton
type="outline-danger"
label="Remove many"
icon="cancel"
class="flex-1"
@click="removeManyTags"
/>
</div>
</div>
</template>Non-remaining Append ​
You can provide an arbitrary slot to render at the append position of the TagList. This will be shown under one of these conditions:
- When there is no remaining counter tag AND placing it at the append position won't make the
TagListoverflow. - When Popover is shown, it will be placed at the append position in the list.
<script setup lang="ts">
import { HButton, HTagList } from '@holistics/design-system'
import { ref } from 'vue'
const TAGS = [
'Holistics',
'Design System',
'is',
'simply',
'the best', '!!!',
] as const
const tags = ref<string[]>(TAGS.slice())
function onClick () {
alert(`Current tags: [${tags.value.join(', ')}]`)
}
const tagToAdd = ref('')
function addTag () {
tags.value.push(tagToAdd.value)
tagToAdd.value = ''
}
</script>
<template>
<div
class="space-y-4 text-xs"
style="width: 448px"
>
<div class="flex items-center">
<div class="w-32">
Popover <em class="italic">wrapped</em>:
</div>
<HTagList
v-model="tags"
closeable
class="ml-1 flex-1"
>
<template #non-remaining-append="{ forwardRef }">
<HButton
:ref="forwardRef"
type="clear-default"
icon="add"
unified
@click="onClick"
/>
</template>
</HTagList>
</div>
<div class="flex items-center">
<div class="w-32">
Popover <em class="italic">at counter</em>:
</div>
<HTagList
v-model="tags"
closeable
popover="at-counter"
class="ml-1 flex-1"
>
<template #non-remaining-append="{ forwardRef }">
<HButton
:ref="forwardRef"
type="clear-default"
icon="add"
unified
@click="onClick"
/>
</template>
</HTagList>
</div>
<div class="mt-4 flex items-center justify-center space-x-4">
<input
v-model="tagToAdd"
type="text"
class="h-8 min-w-0 flex-1 rounded border border-solid border-gray-300 px-2 py-1 text-sm focus:ring-1 focus:ring-blue-200"
placeholder="Add more tag..."
@keydown.enter="addTag()"
>
<HButton
type="secondary-default"
label="Refresh"
icon="refresh"
@click="tags = TAGS.slice()"
/>
</div>
</div>
</template>Generics ​
TagList supports generic types by looking at the tags prop.
By default, the component assumes the passed tags is a list of strings. When passing other types, you must either specify the getLabel prop or provide the #tag slot to help the component render the correct labels.
<script setup lang="ts">
import { ref, type Ref } from 'vue'
import {
HButton,
HTagList,
HTag,
type IconName,
} from '@holistics/design-system'
interface MyTag {
id: number
label: string
icon: IconName
}
const TAGS = [
{ id: 1, label: 'Pinned', icon: 'pin' },
{ id: 2, label: 'Verified', icon: 'check-circle-solid' },
{ id: 3, label: 'Archived', icon: 'archived' },
{ id: 4, label: 'Deleted', icon: 'delete' },
] as const satisfies MyTag[]
const tags = ref(TAGS.slice()) as Ref<MyTag[]>
function getTagTheme (tag: MyTag) {
switch (tag.id) {
case 2: return 'green'
case 3: return 'gray'
case 4: return 'red'
default: return undefined
}
}
</script>
<template>
<div class="w-80 rounded border border-solid px-[7px] py-[5px]">
<HTagList
v-model="tags"
:get-label="(tag) => tag.label"
>
<template #tag="{ tag, index, forwardRef }">
<HTag
:ref="forwardRef ? ((ref) => forwardRef(ref, index)) : undefined"
:tag="tag.label"
:icon="tag.icon"
:theme="getTagTheme(tag)"
closeable
@close="tags.splice(index, 1)"
/>
</template>
</HTagList>
</div>
<div class="mt-4 flex justify-center">
<HButton
type="secondary-default"
label="Refresh"
icon="refresh"
@click="tags = TAGS.slice()"
/>
</div>
</template>API ​
Pass-through ​
- If
popoveriswrapped(default):<HPopover> - Otherwise:
<div>
What does this mean?
All props, events, and attrs that are not specified in the tables below will be passed to the element/component described above.
Props ​
| Name | Type | Description |
|---|---|---|
modelValue * | unknown[] | |
getLabel | TagListGetLabel<unknown> | A function to get the tag label from the passed generic type |
gap | number= 4 | The gap (px) between each tag. |
minTagSize | number= (p) => {
const base = 2 + 16;
if (p.closeable)
return base + 2 + 16;
return base;
} | The minimal size (px) of a tag. By default, it's the width of the |
counterTagSize | TagListCounterTagSizeFn | A function to calculate the size (px) of the remaining-counter tag. Since the counter could be By default, it's the width of the If you use a different font-family or font-size, you should determine the maximum rendered width of |
theme | "blue" | "green" | "red" | "orange" | "purple" | "gray" | "white" | `#${string}` | |
closeable | boolean |
Events ​
| Name | Parameters | Description |
|---|---|---|
@close | [event: MouseEvent | KeyboardEvent, tag: unknown, index: number] | |
@update:modelValue | [tags: unknown[]] |
Slots ​
| Name | Scoped | Description |
|---|---|---|
#tag | { tag: unknown; index: number; visible?: boolean | undefined; forwardRef?: ((element: HTMLElement, index: number) => void) | undefined; } | |
#counter-tag | { remainingCount: number; counterTagSize: TagListCounterTagSizeFn; } |