Skip to content

Commit 43f9f56

Browse files
feat: tags input (#328)
* feat: tags input * chore: add `tags-input` to sidebar links * chore: update * chore: add combobox demo * chore: improve tag highlight * chore: update * chore: rename title * chore: add static width to `TagsInputCombo` example --------- Co-authored-by: zernonia <zernonia@gmail.com>
1 parent 60fbe49 commit 43f9f56

File tree

23 files changed

+527
-1
lines changed

23 files changed

+527
-1
lines changed

apps/www/.vitepress/theme/config/docs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,12 @@ export const docsConfig: DocsConfig = {
326326
href: '/docs/components/tabs',
327327
items: [],
328328
},
329+
{
330+
title: 'Tags Input',
331+
href: '/docs/components/tags-input',
332+
label: 'New',
333+
items: [],
334+
},
329335
{
330336
title: 'Textarea',
331337
href: '/docs/components/textarea',

apps/www/__registry__/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,20 @@ export const Index = {
639639
component: () => import('../src/lib/registry/default/example/TabsDemo.vue').then(m => m.default),
640640
files: ['../src/lib/registry/default/example/TabsDemo.vue'],
641641
},
642+
TagsInputComboboxDemo: {
643+
name: 'TagsInputComboboxDemo',
644+
type: 'components:example',
645+
registryDependencies: ['command', 'tags-input'],
646+
component: () => import('../src/lib/registry/default/example/TagsInputComboboxDemo.vue').then(m => m.default),
647+
files: ['../src/lib/registry/default/example/TagsInputComboboxDemo.vue'],
648+
},
649+
TagsInputDemo: {
650+
name: 'TagsInputDemo',
651+
type: 'components:example',
652+
registryDependencies: ['tags-input'],
653+
component: () => import('../src/lib/registry/default/example/TagsInputDemo.vue').then(m => m.default),
654+
files: ['../src/lib/registry/default/example/TagsInputDemo.vue'],
655+
},
642656
TextareaDemo: {
643657
name: 'TextareaDemo',
644658
type: 'components:example',
@@ -1565,6 +1579,20 @@ export const Index = {
15651579
component: () => import('../src/lib/registry/new-york/example/TabsDemo.vue').then(m => m.default),
15661580
files: ['../src/lib/registry/new-york/example/TabsDemo.vue'],
15671581
},
1582+
TagsInputComboboxDemo: {
1583+
name: 'TagsInputComboboxDemo',
1584+
type: 'components:example',
1585+
registryDependencies: ['command', 'tags-input'],
1586+
component: () => import('../src/lib/registry/new-york/example/TagsInputComboboxDemo.vue').then(m => m.default),
1587+
files: ['../src/lib/registry/new-york/example/TagsInputComboboxDemo.vue'],
1588+
},
1589+
TagsInputDemo: {
1590+
name: 'TagsInputDemo',
1591+
type: 'components:example',
1592+
registryDependencies: ['tags-input'],
1593+
component: () => import('../src/lib/registry/new-york/example/TagsInputDemo.vue').then(m => m.default),
1594+
files: ['../src/lib/registry/new-york/example/TagsInputDemo.vue'],
1595+
},
15681596
TextareaDemo: {
15691597
name: 'TextareaDemo',
15701598
type: 'components:example',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: Tags Input
3+
description: Tag inputs render tags inside an input, followed by an actual text input.
4+
source: apps/www/src/lib/registry/default/ui/tags-input
5+
primitive: https://www.radix-vue.com/components/tags-input.html
6+
---
7+
8+
<ComponentPreview name="TagsInputDemo" />
9+
10+
## Installation
11+
12+
```bash
13+
npx shadcn-vue@latest add tags-input
14+
```
15+
16+
17+
## Usage
18+
19+
### Tags with Combobox
20+
21+
<ComponentPreview name="TagsInputComboboxDemo" />

apps/www/src/examples/tasks/components/DataTableFacetedFilter.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ const selectedValues = computed(() => new Set(props.column?.getFilterValue() as
8484
v-for="option in options"
8585
:key="option.value"
8686
:value="option"
87-
@select="() => {
87+
@select="(e) => {
88+
console.log(e.detail.value)
8889
const isSelected = selectedValues.has(option.value)
8990
if (isSelected) {
9091
selectedValues.delete(option.value)
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<script setup lang="ts">
2+
import { computed, ref } from 'vue'
3+
import { ComboboxAnchor, ComboboxInput, ComboboxPortal, ComboboxRoot } from 'radix-vue'
4+
import { CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/lib/registry/default/ui/command'
5+
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/default/ui/tags-input'
6+
7+
const frameworks = [
8+
{ value: 'next.js', label: 'Next.js' },
9+
{ value: 'sveltekit', label: 'SvelteKit' },
10+
{ value: 'nuxt.js', label: 'Nuxt.js' },
11+
{ value: 'remix', label: 'Remix' },
12+
{ value: 'astro', label: 'Astro' },
13+
]
14+
15+
const modelValue = ref<string[]>([])
16+
const open = ref(false)
17+
const searchTerm = ref('')
18+
19+
const filteredFrameworks = computed(() => frameworks.filter(i => !modelValue.value.includes(i.label)))
20+
</script>
21+
22+
<template>
23+
<TagsInput class="px-0 gap-0 w-80" :model-value="modelValue">
24+
<div class="flex gap-2 flex-wrap items-center px-3">
25+
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
26+
<TagsInputItemText />
27+
<TagsInputItemDelete />
28+
</TagsInputItem>
29+
</div>
30+
31+
<ComboboxRoot v-model="modelValue" v-model:open="open" v-model:searchTerm="searchTerm" class="w-full">
32+
<ComboboxAnchor as-child>
33+
<ComboboxInput placeholder="Framework..." as-child>
34+
<TagsInputInput class="w-full px-3" :class="modelValue.length > 0 ? 'mt-2' : ''" @keydown.enter.prevent />
35+
</ComboboxInput>
36+
</ComboboxAnchor>
37+
38+
<ComboboxPortal>
39+
<CommandList
40+
position="popper"
41+
class="w-[--radix-popper-anchor-width] rounded-md mt-2 border bg-popover text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2"
42+
>
43+
<CommandEmpty />
44+
<CommandGroup>
45+
<CommandItem
46+
v-for="framework in filteredFrameworks" :key="framework.value" :value="framework.label"
47+
@select.prevent="(ev) => {
48+
if (typeof ev.detail.value === 'string') {
49+
searchTerm = ''
50+
modelValue.push(ev.detail.value)
51+
}
52+
53+
if (filteredFrameworks.length === 0) {
54+
open = false
55+
}
56+
}"
57+
>
58+
{{ framework.label }}
59+
</CommandItem>
60+
</CommandGroup>
61+
</CommandList>
62+
</ComboboxPortal>
63+
</ComboboxRoot>
64+
</TagsInput>
65+
</template>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/lib/registry/default/ui/tags-input'
4+
5+
const modelValue = ref(['Apple', 'Banana'])
6+
</script>
7+
8+
<template>
9+
<TagsInput v-model="modelValue">
10+
<TagsInputItem v-for="item in modelValue" :key="item" :value="item">
11+
<TagsInputItemText />
12+
<TagsInputItemDelete />
13+
</TagsInputItem>
14+
15+
<TagsInputInput placeholder="Fruits..." />
16+
</TagsInput>
17+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { TagsInputRoot, type TagsInputRootEmits, type TagsInputRootProps, useForwardPropsEmits } from 'radix-vue'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<TagsInputRootProps & { class?: HTMLAttributes['class'] }>()
7+
const emits = defineEmits<TagsInputRootEmits>()
8+
9+
const delegatedProps = computed(() => {
10+
const { class: _, ...delegated } = props
11+
12+
return delegated
13+
})
14+
15+
const forwarded = useForwardPropsEmits(delegatedProps, emits)
16+
</script>
17+
18+
<template>
19+
<TagsInputRoot v-bind="forwarded" :class="cn('flex flex-wrap gap-2 items-center rounded-md border border-input bg-background px-3 py-2 text-sm', props.class)">
20+
<slot />
21+
</TagsInputRoot>
22+
</template>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { TagsInputInput, type TagsInputInputProps, useForwardProps } from 'radix-vue'
4+
import { cn } from '@/lib/utils'
5+
6+
const props = defineProps<TagsInputInputProps & { class?: HTMLAttributes['class'] }>()
7+
8+
const delegatedProps = computed(() => {
9+
const { class: _, ...delegated } = props
10+
11+
return delegated
12+
})
13+
14+
const forwardedProps = useForwardProps(delegatedProps)
15+
</script>
16+
17+
<template>
18+
<TagsInputInput v-bind="forwardedProps" :class="cn('text-sm min-h-6 focus:outline-none flex-1 bg-transparent px-1', props.class)" />
19+
</template>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { TagsInputItem, type TagsInputItemProps, useForwardProps } from 'radix-vue'
4+
5+
import { cn } from '@/lib/utils'
6+
7+
const props = defineProps<TagsInputItemProps & { class?: HTMLAttributes['class'] }>()
8+
9+
const delegatedProps = computed(() => {
10+
const { class: _, ...delegated } = props
11+
12+
return delegated
13+
})
14+
15+
const forwardedProps = useForwardProps(delegatedProps)
16+
</script>
17+
18+
<template>
19+
<TagsInputItem v-bind="forwardedProps" :class="cn('flex h-6 items-center rounded bg-secondary data-[state=active]:ring-ring data-[state=active]:ring-2 data-[state=active]:ring-offset-2 ring-offset-background', props.class)">
20+
<slot />
21+
</TagsInputItem>
22+
</template>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script setup lang="ts">
2+
import { type HTMLAttributes, computed } from 'vue'
3+
import { TagsInputItemDelete, type TagsInputItemDeleteProps, useForwardProps } from 'radix-vue'
4+
import { X } from 'lucide-vue-next'
5+
import { cn } from '@/lib/utils'
6+
7+
const props = defineProps<TagsInputItemDeleteProps & { class?: HTMLAttributes['class'] }>()
8+
9+
const delegatedProps = computed(() => {
10+
const { class: _, ...delegated } = props
11+
12+
return delegated
13+
})
14+
15+
const forwardedProps = useForwardProps(delegatedProps)
16+
</script>
17+
18+
<template>
19+
<TagsInputItemDelete v-bind="forwardedProps" :class="cn('flex rounded bg-transparent mr-1', props.class)">
20+
<slot>
21+
<X class="w-4 h-4" />
22+
</slot>
23+
</TagsInputItemDelete>
24+
</template>

0 commit comments

Comments
 (0)