Skip to content

Commit

Permalink
feat: added support for object in tags input
Browse files Browse the repository at this point in the history
  • Loading branch information
onmax committed Apr 20, 2024
1 parent 594da83 commit c3b4390
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 18 deletions.
12 changes: 12 additions & 0 deletions docs/content/meta/TabsRoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@
'type': '\'vertical\' | \'horizontal\'',
'required': false,
'default': '\'horizontal\''
},
{
'name': 'convertValue',
'description': '<p>Convert the input value to the desired type. Mandatory when using objects as values and using <code>TagsInputInput</code></p>\n',
'type': '(value: string) => AcceptableInputValue',
'required': false
},
{
'name': 'displayValue',
'description': '<p>Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix or when using object as values</p>\n',
'type': '(value: AcceptableInputValue) => value',
'required': false
}
]" />

Expand Down
10 changes: 6 additions & 4 deletions packages/radix-vue/src/TagsInput/TagsInputItem.vue
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
<script lang="ts">
import type { PrimitiveProps } from '@/Primitive'
import { createContext, useForwardExpose } from '@/shared'
import { type Ref, computed, toRefs } from 'vue'
import { injectTagsInputRootContext } from './TagsInputRoot.vue'
import { type ComputedRef, type Ref, computed, toRefs } from 'vue'
import { type AcceptableInputValue, injectTagsInputRootContext } from './TagsInputRoot.vue'
export interface TagsInputItemProps extends PrimitiveProps {
/** Value associated with the tags */
value: string
value: AcceptableInputValue
/** When `true`, prevents the user from interacting with the tags input. */
disabled?: boolean
}
export interface TagsInputItemContext {
value: Ref<string>
value: Ref<AcceptableInputValue>
displayValue: ComputedRef<string>
isSelected: Ref<boolean>
disabled?: Ref<boolean>
textId: string
Expand Down Expand Up @@ -40,6 +41,7 @@ const itemContext = provideTagsInputItemContext({
isSelected,
disabled,
textId: '',
displayValue: computed(() => context.displayValue(value.value)),
})
</script>

Expand Down
2 changes: 1 addition & 1 deletion packages/radix-vue/src/TagsInput/TagsInputItemText.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ itemContext.textId ||= useId(undefined, 'radix-vue-tags-input-item-text')

<template>
<Primitive v-bind="props" :id="itemContext.textId">
<slot>{{ itemContext.value.value }}</slot>
<slot>{{ itemContext.displayValue.value }}</slot>
</Primitive>
</template>
41 changes: 28 additions & 13 deletions packages/radix-vue/src/TagsInput/TagsInputRoot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { createContext, useArrowNavigation, useDirection, useFormControl, useFor
import type { Direction } from '@/shared/types'
import { type Ref, ref, toRefs } from 'vue'
export interface TagsInputRootProps extends PrimitiveProps {
export type AcceptableInputValue = string | Record<string, any>
export interface TagsInputRootProps<T = AcceptableInputValue> extends PrimitiveProps {
/** The controlled value of the tags input. Can be bind as `v-model`. */
modelValue?: Array<string>
modelValue?: Array<T>
/** The value of the tags that should be added. Use when you do not need to control the state of the tags input */
defaultValue?: Array<string>
defaultValue?: Array<T>
/** When `true`, allow adding tags on paste. Work in conjunction with delimiter prop. */
addOnPaste?: boolean
/** When `true` allow adding tags on tab keydown */
Expand All @@ -30,17 +32,21 @@ export interface TagsInputRootProps extends PrimitiveProps {
/** The name of the tags input submitted with its owning form as part of a name/value pair. */
name?: string
id?: string
/** Convert the input value to the desired type. Mandatory when using objects as values and using `TagsInputInput` */
convertValue?: (value: string) => T
/** Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix or when using object as values */
displayValue?: (value: T) => string
}
export type TagsInputRootEmits = {
export type TagsInputRootEmits<T = AcceptableInputValue> = {
/** Event handler called when the value changes */
'update:modelValue': [payload: Array<string>]
'update:modelValue': [payload: Array<T>]
/** Event handler called when the value is invalid */
'invalid': [payload: string]
'invalid': [payload: T]
}
export interface TagsInputRootContext {
modelValue: Ref<Array<string>>
export interface TagsInputRootContext<T = AcceptableInputValue> {
modelValue: Ref<Array<T>>
onAddValue: (payload: string) => boolean
onRemoveValue: (index: number) => void
onInputKeydown: (event: KeyboardEvent) => void
Expand All @@ -54,24 +60,26 @@ export interface TagsInputRootContext {
dir: Ref<Direction>
max: Ref<number>
id: Ref<string | undefined> | undefined
displayValue: (value: T) => string
}
export const [injectTagsInputRootContext, provideTagsInputRootContext]
= createContext<TagsInputRootContext>('TagsInputRoot')
</script>

<script setup lang="ts">
<script setup lang="ts" generic="T extends AcceptableInputValue = string">
import { Primitive } from '@/Primitive'
import { CollectionSlot, createCollection } from '@/Collection'
import { useFocusWithin, useVModel } from '@vueuse/core'
import { VisuallyHiddenInput } from '@/VisuallyHidden'
const props = withDefaults(defineProps<TagsInputRootProps>(), {
const props = withDefaults(defineProps<TagsInputRootProps<T>>(), {
defaultValue: () => [],
delimiter: ',',
max: 0,
displayValue: (value: T) => value.toString(),
})
const emits = defineEmits<TagsInputRootEmits>()
const emits = defineEmits<TagsInputRootEmits<T>>()
defineSlots<{
default(props: {
Expand All @@ -87,7 +95,7 @@ const modelValue = useVModel(props, 'modelValue', emits, {
defaultValue: props.defaultValue,
passive: true,
deep: true,
}) as Ref<Array<string>>
}) as Ref<Array<AcceptableInputValue>>
const { forwardRef, currentElement } = useForwardExpose()
const { focused } = useFocusWithin(currentElement)
Expand All @@ -100,7 +108,13 @@ const isInvalidInput = ref(false)
provideTagsInputRootContext({
modelValue,
onAddValue: (payload) => {
onAddValue: (_payload) => {
// Check if the value is an object and if the convertValue function is provided. We don't check this a type level because the use
// of `TagsInputInput` is optional.
if ((typeof modelValue.value === 'object' || typeof props.defaultValue === 'object') && typeof props.convertValue === 'function')
throw new Error('You must provide a `convertValue` function when using objects as values.')
const payload = props.convertValue ? props.convertValue(_payload) : _payload as T
if ((modelValue.value.length >= max.value) && !!max.value) {
emits('invalid', payload)
return false
Expand Down Expand Up @@ -204,6 +218,7 @@ provideTagsInputRootContext({
delimiter,
max,
id,
displayValue: props.displayValue as (value: AcceptableInputValue) => string,
})
</script>

Expand Down
42 changes: 42 additions & 0 deletions packages/radix-vue/src/TagsInput/story/TagsInputObject.story.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<script setup lang="ts">
import { ref } from 'vue'
import { TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText, TagsInputRoot } from '..'
import { Icon } from '@iconify/vue'
let id = 1
type Item = { id: number; label: string }
const modelValue = ref<Item[]>([{ id, label: 'Test' }])
function convertValue(label: string) {
id++
return { id, label } satisfies Item
}
function displayValue(item: Item) {
return item.label
}
</script>

<template>
<Story title="TagsInput/Object" :layout="{ type: 'single', iframe: false }">
<Variant title="default">
{{ JSON.stringify(modelValue) }}
<TagsInputRoot
v-model="modelValue"
:convert-value="convertValue"
:display-value="displayValue"
class="flex gap-2 items-center border p-2 rounded-lg bg-blackA7 w-[300px] flex-wrap border-blackA7 mt-6"
>
<TagsInputItem
v-for="item in modelValue" :key="item.id" :value="item"
class=" data-[disabled]:opacity-50 flex items-center justify-center gap-2 bg-green8 aria-[current=true]:bg-green9 rounded px-2 py-1"
>
<TagsInputItemText class="text-sm" />
<TagsInputItemDelete>
<Icon icon="lucide:x" />
</TagsInputItemDelete>
</TagsInputItem>

<TagsInputInput placeholder="Anything..." class="focus:outline-none flex-1 rounded bg-transparent text-white placeholder:text-mauve10 px-1" />
</TagsInputRoot>
</Variant>
</Story>
</template>

0 comments on commit c3b4390

Please sign in to comment.