2025-03-12 13:58:07 +00:00
< script setup lang = "ts" >
import { ref } from 'vue'
2025-04-01 07:30:11 +00:00
import { unionBy, union } from 'lodash-es'
2025-01-04 11:01:02 +00:00
import Pills from '~/components/ui/Pills.vue';
2025-04-01 07:30:11 +00:00
import Button from '~/components/ui/Button.vue';
2025-03-12 19:18:49 +00:00
import Spacer from '~/components/ui/Spacer.vue';
2025-04-01 07:30:11 +00:00
import Layout from '~/components/ui/Layout.vue';
2025-03-12 13:58:07 +00:00
type Item = { type: 'custom' | 'preset', label: string }
type Model = {
currents: Item[],
others?: Item[],
}
2025-01-04 13:34:49 +00:00
const nullModel = ref({
2025-03-12 13:58:07 +00:00
currents: []
} satisfies Model)
2025-01-04 11:01:02 +00:00
2025-04-01 07:30:11 +00:00
const staticModel = ref< Model > ({
2025-03-12 13:58:07 +00:00
currents: [
2025-03-12 19:18:49 +00:00
{ label: "#noise", type: 'preset' },
{ label: "#fieldRecording", type: 'preset' },
{ label: "#experiment", type: 'preset' }
2025-03-12 13:58:07 +00:00
]
2025-04-01 07:30:11 +00:00
});
2025-01-04 11:01:02 +00:00
2025-04-01 07:30:11 +00:00
const simpleCustomModel = ref< Model > ({
2025-03-12 19:18:49 +00:00
currents: [],
others: []
})
2025-01-29 08:52:52 +00:00
2025-04-01 07:30:11 +00:00
const customModel = ref< Model > ({
2025-03-12 13:58:07 +00:00
...staticModel.value,
others: [
2025-03-12 19:18:49 +00:00
{ label: "#myTag1", type: 'custom' },
{ label: "#myTag2", type: 'custom' }
2025-03-12 13:58:07 +00:00
]
2025-04-01 07:30:11 +00:00
});
const sharedOthers = ref< Model [ ' others ' ] > (customModel.value.others)
const currentA = ref< Model [ ' currents ' ] > ([{ label: 'A', type: 'preset' }])
const currentB = ref< Model [ ' currents ' ] > ([])
const updateSharedOthers = (others: Item[]) => {
sharedOthers.value
= unionBy(sharedOthers.value, others, 'label')
.filter(item => [...currentA.value, ...currentB.value].every(({ label }) => item.label !== label ))
}
const tags = ref< string [ ] > (['1', '2'])
const sharedTags = ref< string [ ] > (['3'])
const setTags = (v: string[]) => {
sharedTags.value
= [...tags.value, ...sharedTags.value].filter(tag => !(v.includes(tag)))
tags.value
= v
}
2025-01-04 11:01:02 +00:00
< / script >
2025-01-10 00:13:17 +00:00
```ts
2025-02-22 16:19:57 +00:00
import Pills from "~/components/ui/Pills.vue"
2025-01-10 00:13:17 +00:00
```
2025-01-04 11:01:02 +00:00
# Pills
2025-01-29 08:52:52 +00:00
Show a dense list of pills representing tags, categories or options.
Users can select a subset of given options and create new ones.
The model you provide will be mutated by this component:
2025-03-12 13:58:07 +00:00
- `currents` : these items are currently selected
- `others` : these items are currently not selected (but can be selected by the user). This prop is optional. By adding it, you allow users to change the selection.
2025-03-09 12:19:05 +00:00
2025-03-12 13:58:07 +00:00
Each item has a `label` of type `string` as well as a `type` of either:
2025-03-09 12:19:05 +00:00
2025-03-12 13:58:07 +00:00
- `custom` : the user can edit its label or
- `preset` : the user cannot edit its label
2025-03-09 12:19:05 +00:00
2025-03-12 19:18:49 +00:00
```ts
type Item = { type: 'custom' | 'preset', label: string }
type Model = {
currents: Item[],
others?: Item[],
}
```
2025-01-29 08:52:52 +00:00
## No pills
```ts
const nullModel = ref({
2025-03-12 13:58:07 +00:00
currents: []
2025-03-12 19:18:49 +00:00
}) satisfies Model;
2025-01-29 08:52:52 +00:00
```
```vue-html
< Pills v-model = "nullModel" / >
```
2025-04-01 07:30:11 +00:00
< Pills
:get="(v) => { return }"
:set="() => nullModel"
/>
2025-01-29 08:52:52 +00:00
2025-03-12 19:18:49 +00:00
## Static list of pills
2025-01-29 08:52:52 +00:00
```ts
const staticModel = ref({
2025-03-12 13:58:07 +00:00
currents: [
2025-03-12 19:18:49 +00:00
{ label: "#noise", type: 'preset' },
{ label: "#fieldRecording", type: 'preset' },
{ label: "#experiment", type: 'preset' }
2025-03-12 13:58:07 +00:00
]
2025-03-12 19:18:49 +00:00
} satisfies Model);
2025-01-29 08:52:52 +00:00
```
2025-01-15 09:35:43 +00:00
```vue-html
2025-04-01 07:30:11 +00:00
< Pills
:get="(v) => { return }"
:set="() => staticModel"
/>
2025-01-15 09:35:43 +00:00
```
2025-04-01 07:30:11 +00:00
< Pills
:get="(v) => { return }"
:set="() => staticModel"
/>
2025-01-04 11:01:02 +00:00
2025-03-12 19:18:49 +00:00
## Let users add, remove and edit custom pills
2025-01-29 08:52:52 +00:00
2025-03-12 19:18:49 +00:00
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.
2025-01-04 11:01:02 +00:00
2025-03-12 19:18:49 +00:00
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.
2025-03-12 13:58:07 +00:00
2025-03-12 19:18:49 +00:00
### Minimal example
```ts
const simpleCustomModel = ref({
currents: [],
others: []
})
2025-01-29 08:52:52 +00:00
```
2025-01-15 09:35:43 +00:00
```vue-html
2025-04-01 07:30:11 +00:00
< Pills
:get="(v) => { simpleCustomModel = v }"
:set="() => staticModel"
/>
2025-01-15 09:35:43 +00:00
```
2025-04-01 07:30:11 +00:00
< Pills
:get="(v) => { simpleCustomModel = v }"
:set="() => staticModel"
/>
2025-01-29 08:52:52 +00:00
2025-03-12 19:18:49 +00:00
### Complex example
2025-01-30 22:27:05 +00:00
2025-01-29 08:52:52 +00:00
```ts
const customModel = ref({
2025-03-12 13:58:07 +00:00
...staticModel,
others: [
{ label: "#MyTag1", type: 'custom' },
{ label: "#MyTag2", type: 'custom' }
]
2025-03-12 19:18:49 +00:00
} satisfies Model);
2025-01-29 08:52:52 +00:00
```
2025-01-04 13:34:49 +00:00
2025-01-15 09:35:43 +00:00
```vue-html
2025-03-12 19:18:49 +00:00
< Pills
2025-04-01 07:30:11 +00:00
:get="(v) => { customModel = v }"
:set="() => customModel"
2025-03-12 19:18:49 +00:00
label="Custom Tags"
cancel="Cancel"
/>
2025-01-15 09:35:43 +00:00
```
2025-03-12 19:18:49 +00:00
< Spacer / >
< Pills
2025-04-01 07:30:11 +00:00
:get="(v) => { customModel = v }"
:set="() => customModel"
2025-03-12 19:18:49 +00:00
label="Custom Tags"
cancel="Cancel"
/>
2025-04-01 07:30:11 +00:00
## Bind data with an external sink
In the following example, `others` are shared among two `Pills` lists.
```ts
const sharedOthers = ref< Model [ ' others ' ] > (customModel.value.others)
const currentA = ref< Model [ ' currents ' ] > ([{ label: 'A', type: 'preset' }])
const currentB = ref< Model [ ' currents ' ] > ([])
const updateSharedOthers = (others: Item[]) => {
sharedOthers.value
= unionBy(sharedOthers.value, others, 'label')
.filter(item =>
[...currentA.value, ...currentB.value].every(({ label }) =>
item.label !== label
))
}
```
<!-- prettier - ignore - start -->
< Spacer / >
< Pills
:get="({ currents, others }) => {
currentA = currents;
updateSharedOthers(others || []);
}"
:set="({ currents, others }) => ({ currents: currentA, others: unionBy(sharedOthers, others, 'label') })"
label="A"
cancel="Cancel"
/>
< Spacer / >
< Pills
:get="({ currents, others }) => {
currentB = currents;
updateSharedOthers(others || []);
}"
:set="({ currents, others }) => ({ currents: currentB, others: unionBy(sharedOthers, others, 'label') })"
label="B"
cancel="Cancel"
/>
< template
v-for="_ in [1]"
:key="[...sharedOthers].join(',')"
>
< pre > Shared among A and B:
{{ (sharedOthers || []).map(({ label }) => label).join(', ') }}
< / pre >
< / template >
<!-- prettier - ignore - end -->
## Bind data with an external source
You can use the same pattern to influence the model from an outside source:
```ts
const tags = ref< string [ ] > (['1', '2'])
const sharedTags = ref< string [ ] > (['3'])
const setTags = (v: string[]) => {
sharedTags.value
= [...tags.value, ...sharedTags.value].filter(tag => !(v.includes(tag)))
tags.value
= v
}
```
<!-- prettier - ignore - start -->
< Layout
flex
gap-8
>
< Button
secondary
@click ="setTags([])"
>
Set tags=[]
< / Button >
< Button
secondary
@click ="setTags(['1', '2', '3'])"
>
Set tags=['1', '2', '3']
< / Button >
< / Layout >
< hr / >
< template
v-for="_ in [1]"
:key="[...tags, '*', ...sharedTags].join(',')"
>
< Layout flex >
< pre > {{ tags.join(', ') }}< / pre >
< Spacer grow / >
< pre > {{ sharedTags.join(', ') }}< / pre >
< / Layout >
< hr / >
< Spacer / >
< Pills
:get="({ currents, others }) => {
setTags(currents.map(({ label }) => label));
sharedTags
= union(sharedTags, (others || []).map(({ label }) => label))
}"
:set="({ currents, others }) => ({
currents: tags.map(l => ({ type: 'custom', label: l})),
others: sharedTags
.filter(l => currents.every(({ label }) => label !== l))
.map(l => ({ type: 'custom', label: l}))
})"
label="Two-way binding with an array of strings"
cancel="Cancel"
/>
< / template >
<!-- prettier - ignore - end -->