feat(ui): Pills list done 💊💊

This commit is contained in:
upsiflu 2025-03-12 20:18:49 +01:00
parent 9b02f1840e
commit d0f42437ae
4 changed files with 234 additions and 210 deletions

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, watch, nextTick, computed } from 'vue' import { ref, watch, nextTick, computed, onMounted } from 'vue'
import { type ColorProps, type PastelProps, type VariantProps, type RaisedProps, color } from '~/composables/color' import { type ColorProps, type PastelProps, type VariantProps, type RaisedProps, color } from '~/composables/color'
import Layout from './Layout.vue' import Layout from './Layout.vue'
@ -11,13 +11,16 @@ import PopoverItem from './popover/PopoverItem.vue'
/* Event */ /* Event */
const emit = defineEmits<{ const emit = defineEmits<{
changed: [] confirmed: [],
closed: []
}>() }>()
/* Model */ /* Model */
const props = defineProps<{ const props = defineProps<{
noUnderline?:true noUnderline?: true,
cancel?: string,
autofocus?: boolean
} & (PastelProps | ColorProps) } & (PastelProps | ColorProps)
& VariantProps & VariantProps
& RaisedProps & RaisedProps
@ -25,25 +28,34 @@ const props = defineProps<{
type Item = { type: 'custom' | 'preset', label: string } type Item = { type: 'custom' | 'preset', label: string }
const model = defineModel<{ const currentItem = defineModel<Item>('current'),
current: Item, otherItems = defineModel<Item[]>('others')
others: Item[]
}>()
const isEditing = ref<boolean>(false) const isEditing = ref<boolean>(false)
/* Lifecycle */
onMounted(() => {
if (props.autofocus) {
nextTick(() => {
if (!currentItem.value || !otherItems.value) return
clicked()
})
}
})
let previousValue: Item | undefined let previousValue: Item | undefined
let previouslyFocusedElement: Element | null let previouslyFocusedElement: Element | null
watch(isEditing, (isTrue, wasTrue) => { watch(isEditing, (isTrue, wasTrue) => {
if (!model.value) return if (!currentItem.value || !otherItems.value) return
// Cache the previous value, in case the user cancels later // Cache the previous value, in case the user cancels later
if (isTrue && !wasTrue) { if (isTrue && !wasTrue) {
previousValue = { ...model.value.current } previousValue = { ...currentItem.value }
if (model.value.current.type === 'preset') { if (currentItem.value.type === 'preset') {
model.value.others.push({...model.value.current}) otherItems.value.push({...currentItem.value})
model.value.current.type = 'custom' currentItem.value.type = 'custom'
} }
// Shift focus between the input and the previously focused element // Shift focus between the input and the previously focused element
previouslyFocusedElement = document.activeElement previouslyFocusedElement = document.activeElement
@ -51,29 +63,28 @@ watch(isEditing, (isTrue, wasTrue) => {
nextTick(() => (previouslyFocusedElement as HTMLElement)?.focus()) nextTick(() => (previouslyFocusedElement as HTMLElement)?.focus())
const matchInOthers const matchInOthers
= model.value?.others.find(({ label })=>label === model.value?.current.label.trim()) = otherItems.value.find(({ label }) => label === currentItem.value?.label.trim())
if (matchInOthers) { if (matchInOthers) {
model.value.current = { ...matchInOthers } currentItem.value = { ...matchInOthers }
} }
model.value.others = model.value.others.filter(({ label }) => label !== model.value?.current.label) otherItems.value = otherItems.value.filter(({ label }) => label !== currentItem.value?.label)
emit('changed') emit('closed')
} }
}) })
/* Update */ /* Update */
const clicked = () => { const clicked = () => {
if (!model.value) return if (!currentItem.value || !otherItems.value) return
if (!isEditing.value) { if (!isEditing.value) {
isEditing.value = true isEditing.value = true
} }
} }
const pressedKey = (e: KeyboardEvent) => { const pressedKey = (e: KeyboardEvent) => {
if (!model.value) return if (!currentItem.value || !otherItems.value) return
// confirm or cancel // confirm or cancel
switch (e.key) { switch (e.key) {
@ -90,14 +101,19 @@ const pressedKey = (e: KeyboardEvent) => {
} }
} }
const releasedKey = () => {
if (!otherItems.value || !currentItem.value) return
currentItem.value.label = currentItem.value.label.replace(',', '').replace(' ', '').trim()
}
const canceled = () => { const canceled = () => {
if (!previousValue || !model.value) return if (!previousValue || !currentItem.value || !otherItems.value) return
const matchInOthers const matchInOthers
= model.value?.others.find(({ label })=>label === model.value?.current.label.trim()) = otherItems.value?.find(({ label })=>label === currentItem.value?.label.trim())
// Reset current label // Reset current label
model.value.current currentItem.value
= matchInOthers = matchInOthers
|| {...previousValue} || {...previousValue}
@ -106,35 +122,37 @@ const canceled = () => {
} }
const confirmed = () => { const confirmed = () => {
if (!previousValue || !model.value) return; if (!previousValue || !currentItem.value || !otherItems.value) return
const matchInOthers // Sanitize label
= model.value?.others.find(({ label })=>label === model.value?.current.label.trim()) currentItem.value.label = currentItem.value.label.replace(',', '').replace(' ', '').trim()
// Use the best match; otherwise the current input, sanitized // Apply the identical, otherwise the best match, if available
model.value.current currentItem.value
= matchInOthers = otherItems.value?.find(({ label })=>label === currentItem.value?.label.trim())
|| match.value || match.value
|| { ...model.value.current, || currentItem.value
label : model.value.current.label.replace(',', '').replace(' ', '').trim()
}
// Close dropdown // Close dropdown
isEditing.value = false isEditing.value = false
// Tell parent component
if (previousValue !== currentItem.value)
emit('confirmed')
} }
const sortedOthers = computed(()=> const sortedOthers = computed(()=>
model.value otherItems.value && currentItem.value
? model.value.others.map((item) => ? otherItems.value.map((item) =>
item.label.toLowerCase().includes(model.value?.current.label.toLowerCase() || '') item.label.toLowerCase().includes(currentItem.value?.label.toLowerCase() || '')
? [item.label.length - (model.value?.current.label.length || 0), item] as const ? [item.label.length - (currentItem.value?.label.length || 0), item] as const /* TODO: Use a more sophisticated algorithm for suggestions */
: [99, item] as const : [99, item] as const
) )
.sort(([deltaA, a], [deltaB, b]) => .sort(([deltaA, a], [deltaB, b]) =>
deltaA - deltaB deltaA - deltaB
) )
.map(([delta, item], index) => .map(([delta, item], index) =>
index===0 && delta < 99 && model.value && model.value.current.label.length>0 && model.value.current.label !== previousValue?.label index===0 && delta < 99 && currentItem.value && currentItem.value.label.length>0 && currentItem.value.label !== previousValue?.label
? [-1, item] as const /* It's a match */ ? [-1, item] as const /* It's a match */
: [delta, item] as const /* It's not a match */ : [delta, item] as const /* It's not a match */
) )
@ -147,85 +165,73 @@ const match = computed(()=>
: undefined : undefined
) )
const other = computed(() => (option: Item) => ( const other = computed(() => (option: Item) => ({
{
item: { item: {
onClick: () => { onClick: () => {
if (!model.value) return; if (!currentItem.value || !otherItems.value) return;
model.value = { currentItem.value = { ...option, type: 'custom' }
current: { ...option, type: 'custom' }, otherItems.value = [...(
others: [...( currentItem.value.label.trim() === '' || otherItems.value.find(({ label }) => label === currentItem.value?.label.trim())
model.value.current.label.trim() === '' || model.value.others.find(({ label })=>label === model.value?.current.label.trim())
? [] ? []
: [{ ...model.value.current }] : [{ ...currentItem.value }]
), ...model.value.others.filter( ), ...otherItems.value.filter(
({ label, type }) => label !== option.label || type === 'preset' ({ label, type }) => label !== option.label || type === 'preset'
)] )]
}
isEditing.value = false isEditing.value = false
}, },
isMatch: match.value?.label === option.label, isMatch: match.value?.label === option.label,
isSame: option.label === model.value?.current.label isSame: option.label === currentItem.value?.label
}, },
action: option.type === 'custom' action: option.type === 'custom'
? { ? {
title: 'Delete custom', title: 'Delete custom',
icon: 'bi-trash', icon: 'bi-trash',
onClick: () => { onClick: () => {
if (!model.value) return; if (!currentItem.value || !otherItems.value) return;
model.value = { otherItems.value = otherItems.value.filter(({ label }) => label !== option.label)
...model.value,
others: model.value.others.filter(({ label }) => label !== option.label)
}
} }
} }
: undefined : undefined
} as const } as const))
))
const current = computed(() => ( const current = computed(() => (
!model.value !currentItem.value || !otherItems.value
? undefined ? undefined
: model.value.current.label === '' && previousValue?.label !== '' : currentItem.value.label === '' && previousValue?.label !== ''
? { ? {
attributes: { attributes: {
title: `Reset to ${previousValue?.label || model.value.current}`, title: `Reset to ${previousValue?.label || currentItem.value}`,
icon: 'bi-arrow-counterclockwise' icon: 'bi-arrow-counterclockwise'
}, },
onClick: () => { onClick: () => {
if (!model.value) return; if (!currentItem.value || !otherItems.value) return;
model.value = { currentItem.value = previousValue || currentItem.value
...model.value,
current: previousValue || model.value.current
}
} }
} as const } as const
: model.value.current.label === previousValue?.label && model.value.current.type==='custom' && !model.value?.others.find(({ label })=>label === model.value?.current.label.trim()) && model.value.current.label !== '' : currentItem.value.label === previousValue?.label && currentItem.value.type==='custom' && !otherItems.value?.find(({ label })=>label === currentItem.value?.label.trim()) && currentItem.value.label !== ''
? { ? {
attributes: { attributes: {
title: `Delete ${model.value.current.label}`, title: `Delete ${currentItem.value.label}`,
icon: 'bi-trash', icon: 'bi-trash',
destructive: true destructive: true
}, },
onClick: () => { onClick: () => {
if (!model.value) return; if (!currentItem.value || !otherItems.value) return;
model.value.current.label = '' currentItem.value.label = ''
isEditing.value = false isEditing.value = false
} }
} as const } as const
: model.value.current.label !== match.value?.label && model.value.current.type === 'custom' && model.value.current.label.trim() !== '' && !model.value?.others.find(({ label })=>label === model.value?.current.label.trim()) : currentItem.value.label !== match.value?.label && currentItem.value.type === 'custom' && currentItem.value.label.trim() !== '' && !otherItems.value?.find(({ label })=>label === currentItem.value?.label.trim())
? { ? {
attributes: { attributes: {
title: `Add ${model.value.current.label}`, title: `Add ${currentItem.value.label}`,
icon: 'bi-plus', icon: 'bi-plus',
'aria-pressed': !match.value, 'aria-pressed': !match.value,
primary: true primary: true
}, },
onClick: () => { onClick: () => {
if (!model.value if (!otherItems.value || !currentItem.value || otherItems.value.find(({ label })=>label === currentItem.value?.label.trim())) return
|| model.value.others.find(({ label })=>label === model.value?.current.label.trim()) otherItems.value.push({...currentItem.value})
) return
model.value.others.push({...model.value.current})
} }
} as const } as const
: undefined : undefined
@ -235,7 +241,7 @@ const current = computed(() => (
<template> <template>
<button <button
:class="['funkwhale', $style.pill, props.noUnderline && $style['no-underline']]" :class="['funkwhale', $style.pill, (props.noUnderline || currentItem) && $style['no-underline']]"
type="button" type="button"
@click="clicked" @click="clicked"
> >
@ -257,9 +263,9 @@ const current = computed(() => (
<!-- Preset content --> <!-- Preset content -->
<div :class="$style['pill-content']"> <div :class="$style['pill-content']">
<slot /> <slot />
{{ model?.current?.label }} &ZeroWidthSpace; {{ currentItem?.label }} {{ `&ZeroWidthSpace;${''}` }}
<Popover <Popover
v-if="model" v-if="currentItem && otherItems"
v-model="isEditing" v-model="isEditing"
> >
<div /> <div />
@ -268,12 +274,13 @@ const current = computed(() => (
<PopoverItem> <PopoverItem>
<Input <Input
v-model="model.current.label" v-model="currentItem.label"
autofocus autofocus
low-height low-height
:class="$style.input" :class="$style.input"
@keydown.enter.stop.prevent="pressedKey" @keydown.enter.stop.prevent="pressedKey"
@keydown="pressedKey" @keydown="pressedKey"
@keyup="releasedKey"
/> />
<template #after> <template #after>
<Button <Button
@ -317,9 +324,10 @@ const current = computed(() => (
<hr> <hr>
<PopoverItem <PopoverItem
v-if="cancel"
@click.stop.prevent="canceled" @click.stop.prevent="canceled"
> >
Cancel {{ cancel }}
</PopoverItem> </PopoverItem>
</template> </template>
</Popover> </Popover>

View File

@ -1,27 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch } from 'vue' import { computed, nextTick, ref, watch } from 'vue'
import { color } from '~/composables/color' import { color } from '~/composables/color'
import Pill from './Pill.vue' import Pill from './Pill.vue'
import Layout from './Layout.vue' import Layout from './Layout.vue'
import Button from './Button.vue'
const props = defineProps<{ const props = defineProps<{
icon?: string, icon?: string,
placeholder?: string, placeholder?: string,
label?: string, label?: string,
cancel?: string,
}>() }>()
const model = defineModel<{ const model = defineModel<Model>({ required: true })
currents: Item[],
others?: Item[],
}>({ required: true })
type Item = { type: 'custom' | 'preset', label: string } type Item = { type: 'custom' | 'preset', label: string }
type Model = { currents: Item[], others?: Item[] }
// Manually trigger rerendering
const componentKey = ref(0)
const forceRerender = () => componentKey.value++
const isStatic = computed(() => const isStatic = computed(() =>
!model.value.others !model.value.others
@ -31,47 +27,20 @@ const emptyItem = {
label: '', type: 'custom' label: '', type: 'custom'
} as const } as const
const pills = computed({ const nextIndex = ref<number | undefined>(undefined)
get: () => { const sanitize = () => {
console.log("OTHERS", model.value.others) if (model.value.others)
return [...model.value.currents, { ...emptyItem }].map( model.value.currents = [...model.value.currents.filter(({ label }) => label !== ''), { ...emptyItem }]
(item) => ({ current: { ...item }, others: model.value.others ? model.value.others.map(item => ({ ...item })) : [] })
)
},
set: (pills) => {
console.log("SETTING PILLS", pills)
model.value.currents = pills
.filter(({ current }) => current.label !== '')
.map(({ current }) => ({ ...current }))
}
})
const changed = (index: number, pill: (typeof pills.value)[number]) => {
/*reduce<U>(
callbackfn: (previousValue: U, currentValue: Item, currentIndex: number, array: Item[]) => U,
initialValue: U): U */
console.log("NEW: #", index, "=", pills.value[index].current)
console.log("OLD: #", index, "=", pill.current)
// model.value.currents.push({ ...emptyItem })
// console.log(model.value.currents.length)
model.value.currents[index] = { ...pills.value[index].current }
model.value.others = { ...pills.value[index].others }
forceRerender()
} }
// Add empty current item if none is inside const next = (index: number) => nextTick(() => { nextIndex.value = index + 1 })
watch(model, () => { watch(model, () => {
console.log("MODEL CHANGED", model.value) sanitize()
}) })
// Remove duplicates on each CONFIRM sanitize();
// const unique = (list:Item[]) =>
// list.reduce((acc, a) => ([a, ...acc]), [])
</script> </script>
<template> <template>
@ -94,8 +63,6 @@ watch( model, () => {
:class="$style.label" :class="$style.label"
> >
{{ props.label }} {{ props.label }}
<div>PILLS       {{ pills.map(({ current })=>current.label) }}</div>
<div>MODEL {{ model.currents.map(({ label })=>label) }}</div>
</span> </span>
<!-- List of Pills --> <!-- List of Pills -->
@ -107,19 +74,46 @@ watch( model, () => {
:class="$style.list" :class="$style.list"
> >
<Pill <Pill
v-for="(pill, index) in pills" v-for="(_, index) in model.currents"
:key="index*1000+componentKey" :key="index+1000*(nextIndex || 0)"
v-model="pills[index]" v-model:current="model.currents[index]"
v-model:others="model.others"
:cancel="cancel"
:autofocus="index === nextIndex && nextIndex < model.currents.length"
outline outline
no-underline no-underline
:class="[$style.pill, $style[isStatic ? 'static' : pill.current.label === '' ? 'empty' : pill.current.type === 'custom' ? 'custom' : 'preset']]" :class="[$style.pill, $style[
@changed="() => { console.log('CCCCCC', index); changed(index, pill) }" isStatic
? 'static'
: model.currents[index].label === ''
? 'empty'
: model.currents[index].type
]]"
@closed="() => { sanitize() }"
@confirmed="() => { next(index) }"
> >
<span <span
v-if="isStatic" v-if="isStatic"
:class="$style['pill-content']" :class="$style['pill-content']"
>{{ pill.current.label }}</span> >{{ model.currents[index].label }}</span>
{{ `${index} ${componentKey}` }} <template
v-if="model.others && model.currents[index].label !== ''"
#action
>
<Button
ghost
primary
round
icon="bi-x"
title="Deselect"
@click.stop.prevent="() => {
if (!model.others) return
model.others.push({...model.currents[index]});
model.currents[index] = {label: '', type: 'custom'}
sanitize()
}"
/>
</template>
</Pill> </Pill>
</Layout> </Layout>
</Layout> </Layout>

View File

@ -4,14 +4,12 @@ import { computed, ref } from 'vue'
import Pill from '~/components/ui/Pill.vue' import Pill from '~/components/ui/Pill.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
const customTag = ref({ const current = ref({ type: 'custom', label: 'I-am-custom.-Change-me!' })
current: { type: 'custom', label: 'I-am-custom.-Change-me!' }, const others = ref([
others: [
{ type: 'preset', label: 'Preset-1' }, { type: 'preset', label: 'Preset-1' },
{ type: 'preset', label: 'Preset-2' }, { type: 'preset', label: 'Preset-2' },
{ type: 'preset', label: 'Preset-3' }, { type: 'preset', label: 'Preset-3' },
] ])
})
</script> </script>
```ts ```ts
@ -170,28 +168,35 @@ Add `v-model="..."` to link the pill content to a `ref` with one `current` item
```ts ```ts
import { ref } from "vue" import { ref } from "vue"
const customTag = ref({ const current = ref({ type: 'custom', label: 'I-am-custom.-Change-me!' })
current: { type: 'custom', label: 'I-am-custom.-Change-me!' }, const others = ref([
others: [
{ type: 'preset', label: 'Preset-1' }, { type: 'preset', label: 'Preset-1' },
{ type: 'preset', label: 'Preset-2' }, { type: 'preset', label: 'Preset-2' },
{ type: 'preset', label: 'Preset-3' }, { type: 'preset', label: 'Preset-3' },
] ])
})
``` ```
```vue-html ```vue-html
<Pill v-model="customTag" /> <Pill
v-model:current="current"
v-model:others="others"
/>
``` ```
<Pill no-underline v-model="customTag" /> <Pill
v-model:current="current"
v-model:others="others"
/>
## Add an action ## Add an action
<Button primary ghost icon="bi-trash"/> <Button primary ghost icon="bi-trash"/>
```vue-html ```vue-html
<Pill v-model="customTag"> <Pill
v-model:current="current"
v-model:others="others"
>
<template #action> <template #action>
<Button ghost primary round icon="bi-x" <Button ghost primary round icon="bi-x"
title="Deselect" title="Deselect"
@ -207,19 +212,22 @@ const customTag = ref({
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<Pill v-model="customTag"> <Pill
v-model:current="current"
v-model:others="others"
>
<template #action> <template #action>
<Button ghost primary round icon="bi-x" <Button ghost primary round icon="bi-x"
title="Deselect" title="Deselect"
@click.stop.prevent="() => { @click.stop.prevent="() => {
if (customTag.current.type === 'custom') if (current.type === 'custom')
customTag.others.push({...customTag.current}); others.push({...current});
customTag.current = {label: '', type: 'custom'} current = {label: '', type: 'custom'}
}" }"
/> />
</template> </template>
</Pill> </Pill>
{{ customTag }} {{ current }} (+ {{ others.length }} other options)
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -2,6 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import Pills from '~/components/ui/Pills.vue'; import Pills from '~/components/ui/Pills.vue';
import Spacer from '~/components/ui/Spacer.vue';
type Item = { type: 'custom' | 'preset', label: string } type Item = { type: 'custom' | 'preset', label: string }
type Model = { type Model = {
@ -15,25 +16,22 @@ const nullModel = ref({
const staticModel = ref({ const staticModel = ref({
currents: [ currents: [
{ label: "#Noise", type: 'preset' }, { label: "#noise", type: 'preset' },
{ label: "#FieldRecording", type: 'preset' }, { label: "#fieldRecording", type: 'preset' },
{ label: "#Experiment", type: 'preset' } { label: "#experiment", type: 'preset' }
] ]
} satisfies Model); } satisfies Model);
const interactiveModel = ref({ const simpleCustomModel = ref({
...staticModel.value, currents: [],
others: [ others: []
{ label: "#Melody", type: 'preset' }, })
{ label: "#Rhythm", type: 'preset' }
]
} satisfies Model);
const customModel = ref({ const customModel = ref({
...staticModel.value, ...staticModel.value,
others: [ others: [
{ label: "#MyTag1", type: 'custom' }, { label: "#myTag1", type: 'custom' },
{ label: "#MyTag2", type: 'custom' } { label: "#myTag2", type: 'custom' }
] ]
} satisfies Model); } satisfies Model);
</script> </script>
@ -57,12 +55,20 @@ Each item has a `label` of type `string` as well as a `type` of either:
- `custom`: the user can edit its label or - `custom`: the user can edit its label or
- `preset`: the user cannot edit its label - `preset`: the user cannot edit its label
```ts
type Item = { type: 'custom' | 'preset', label: string }
type Model = {
currents: Item[],
others?: Item[],
}
```
## No pills ## No pills
```ts ```ts
const nullModel = ref({ const nullModel = ref({
currents: [] currents: []
}) as { currents: Item[] }; }) satisfies Model;
``` ```
```vue-html ```vue-html
@ -71,48 +77,46 @@ const nullModel = ref({
<Pills v-model="nullModel" /> <Pills v-model="nullModel" />
## Predefined list of pills ## Static list of pills
```ts ```ts
const staticModel = ref({ const staticModel = ref({
currents: [ currents: [
{ label: "#Noise", type: 'preset' }, { label: "#noise", type: 'preset' },
{ label: "#FieldRecording", type: 'preset' }, { label: "#fieldRecording", type: 'preset' },
{ label: "#Experiment", type: 'preset' } { label: "#experiment", type: 'preset' }
] ]
}); } satisfies Model);
``` ```
```vue-html ```vue-html
<Pills v-model="staticModel" label="Static Tags" /> <Pills v-model="staticModel"/>
``` ```
<Pills v-model="staticModel" label="Static Tags" /> <Pills v-model="staticModel"/>
## Let users select and unselect pills
Select a set of pills from presets, and add and remove custom ones
```ts
const interactiveModel = ref({
...staticModel,
others: [
{ label: "#Melody", type: 'preset' },
{ label: "#Rhythm", type: 'preset' }
]
});
```
```vue-html
<Pills v-model="interactiveModel" label="Interactive Tags" />
```
<Pills v-model="interactiveModel" label="Interactive Tags" />
## Let users add, remove and edit custom pills ## Let users add, remove and edit custom pills
Use [reactive](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-variables-with-ref) methods [such as `computed(...)`](https://vuejs.org/guide/essentials/computed.html) and `watch(...)` to query the model. By adding `custom` options, you make the `Pills` instance interactive. Use [reactive](https://vuejs.org/guide/essentials/reactivity-fundamentals.html#reactive-variables-with-ref) methods [such as `computed(...)`](https://vuejs.org/guide/essentials/computed.html) and `watch(...)` to bind the model.
Note that this component will automatically add an empty pill to the end of the model because it made the implementation more straightforward. Use `filter(({ label }) => label !== '') to ignore it when reading the model.
### Minimal example
```ts
const simpleCustomModel = ref({
currents: [],
others: []
})
```
```vue-html
<Pills v-model="simpleCustomModel"/>
```
<Pills v-model="simpleCustomModel"/>
### Complex example
```ts ```ts
const customModel = ref({ const customModel = ref({
@ -121,11 +125,21 @@ const customModel = ref({
{ label: "#MyTag1", type: 'custom' }, { label: "#MyTag1", type: 'custom' },
{ label: "#MyTag2", type: 'custom' } { label: "#MyTag2", type: 'custom' }
] ]
}); } satisfies Model);
``` ```
```vue-html ```vue-html
<Pills v-model="customModel" label="Custom Tags" /> <Pills
v-model="customModel"
label="Custom Tags"
cancel="Cancel"
/>
``` ```
<Pills v-model="customModel" label="Custom Tags" /> <Spacer />
<Pills
v-model="customModel"
label="Custom Tags"
cancel="Cancel"
/>