Skip to content

Commit

Permalink
fix: improve autocomplete, listbox, radio typing
Browse files Browse the repository at this point in the history
  • Loading branch information
stafyniaksacha committed Oct 14, 2023
1 parent 1b20a39 commit b18930d
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 106 deletions.
20 changes: 10 additions & 10 deletions .playground/pages/tests/form/input-models.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ definePageMeta({
section: 'form',
})
const autocomplete1 = ref()
const autocomplete2 = ref()
const autocomplete1 = ref<string>()
const autocomplete2 = ref<{ name: string }>()
const checkbox1 = ref()
const checkbox2 = ref()
const checkbox3 = ref([])
const radio1 = ref()
const radio2 = ref()
const radio3 = ref()
const radio1 = ref<boolean>()
const radio2 = ref<string>()
const radio3 = ref<{ [key: string]: number }>()
const checkboxCustom1 = ref()
const radioCustom1 = ref()
const radioCustom1 = ref<'yes' | 'no'>()
const animatedCheckbox1 = ref()
const animatedCheckbox2 = ref([])
const switchBall = ref()
Expand All @@ -26,8 +26,8 @@ const input3 = ref()
const input4 = ref()
const inputFile1 = ref<FileList | null>(null)
const inputFileCustom1 = ref<FileList | null>(null)
const listbox1 = ref()
const listbox2 = ref()
const listbox1 = ref<string>()
const listbox2 = ref<{ name: string }>()
const select1 = ref()
const select2 = ref()
const textarea1 = ref()
Expand All @@ -49,7 +49,7 @@ const textarea2 = ref()
v-model="autocomplete1"
:items="['tete', 'hello', 'test', 'tast', 'tutu', 'holla']"
:filter-items="
(query?: string, items?: string[]) =>
(query, items) =>
items?.filter((item) => {
return query
? item?.toLowerCase().startsWith(query.toLowerCase())
Expand Down Expand Up @@ -82,7 +82,7 @@ autocomplete1: {{ autocomplete1 }}({{ typeof autocomplete1 }})</pre
<div class="col-span-2">
<BaseAutocomplete
v-model="autocomplete2"
:display-value="(item: any) => item.name"
:display-value="(item) => item.name"
clearable
:items="[
{
Expand Down
22 changes: 9 additions & 13 deletions components/form/BaseAutocomplete.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends any = string">
import {
Combobox,
ComboboxButton,
Expand All @@ -11,19 +11,15 @@ import { Float, FloatReference, FloatContent } from '@headlessui-float/vue'
const props = withDefaults(
defineProps<{
// Temporary fix to allow attributes inheritance with generic components
// @see https://github.com/vuejs/core/issues/8372
[attrs: string]: any
/**
* The model value of the component.
*/
modelValue?: any | any[]
modelValue?: T | T[]
/**
* The items to display in the component.
*/
items?: any[]
items?: T[]
/**
* The shape of the component.
Expand Down Expand Up @@ -121,7 +117,7 @@ const props = withDefaults(
/**
* A function used to render the items as strings in either the input or the tag when multiple is true.
*/
displayValue?: (item: any) => string
displayValue?: (item: T) => string
/**
* The debounce time for the filterItems method.
Expand All @@ -133,7 +129,7 @@ const props = withDefaults(
*
* You can use this method to implement your own filtering logic or to fetch items from an API.
*/
filterItems?: (query?: string, items?: any[]) => Promise<any[]> | any[]
filterItems?: (query?: string, items?: T[]) => Promise<T[]> | T[]
/**
* Optional CSS classes to apply to the wrapper, label, input, addon, error, and icon elements.
Expand Down Expand Up @@ -185,7 +181,7 @@ const props = withDefaults(
multiple: false,
displayValue: (item: any) => item,
filterDebounce: 0,
filterItems: (query?: string, items?: any[]) => {
filterItems: (query?: string, items?: T[]) => {
if (!query || !items) {
return items ?? []
}
Expand All @@ -202,14 +198,14 @@ const props = withDefaults(
)
const emits = defineEmits<{
(event: 'update:modelValue', value?: any | any[]): void
(event: 'update:modelValue', value?: T | T[]): void
}>()
const appConfig = useAppConfig()
const shape = computed(() => props.shape ?? appConfig.nui.defaultShapes?.input)
const value = useVModel(props, 'modelValue', emits, {
passive: true,
}) as Ref<any | any[]>
}) as Ref<any>
const items = shallowRef(props.items)
const query = ref('')
Expand Down Expand Up @@ -527,7 +523,7 @@ function removeItem(item: any) {
:key="String(item)"
class="nui-autocomplete-results-item"
as="div"
:value="item"
:value="item as any"
>
<slot
name="item"
Expand Down
134 changes: 59 additions & 75 deletions components/form/BaseListbox.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="ts" generic="T extends any = string">
import {
Listbox,
ListboxButton,
Expand All @@ -14,7 +14,7 @@ const props = withDefaults(
/**
* The items to display in the multiselect.
*/
items: any[]
items: T[]
/**
* The model value of the multiselect.
Expand All @@ -26,12 +26,14 @@ const props = withDefaults(
* the value property of an object (as defined in properties.value) rather than the object itself
* `v-model.prop="value"`
*/
modelValue?: any
modelValue?: T | T[]
/**
* Used internaly to allow v-model.number and v-model.trim
* Used internaly to allow .prop v-model modifier
*/
modelModifiers?: any
modelModifiers?: {
prop?: boolean
}
/**
* The shape of the multiselect.
Expand Down Expand Up @@ -81,7 +83,7 @@ const props = withDefaults(
/**
* The label to display for multiple selections, or a function that returns the label.
*/
multipleLabel?: string | ((value: any[], labelProperty?: string) => string)
multipleLabel?: string | ((value: T[], labelProperty?: string) => string)
/**
* The placeholder text to display when no selection has been made.
Expand All @@ -105,27 +107,27 @@ const props = withDefaults(
/**
* The property to use for the value of the options.
*/
value?: string
value?: T extends object ? keyof T : string
/**
* The property to use for the label of the options.
*/
label?: string
label?: T extends object ? keyof T : string
/**
* The property to use for the sublabel of the options.
*/
sublabel?: string
sublabel?: T extends object ? keyof T : string
/**
* The property to use for the media of the options.
*/
media?: string
media?: T extends object ? keyof T : string
/**
* The property to use for the icon of the options.
*/
icon?: string
icon?: T extends object ? keyof T : string
}
}>(),
{
Expand All @@ -140,14 +142,14 @@ const props = withDefaults(
shape: undefined,
error: false,
multipleLabel: () => {
return (value: any[], labelProperty?: string): string => {
return (value: T[], labelProperty?: string): string => {
if (value.length === 0) {
return 'No elements selected'
} else if (value.length > 1) {
return `${value.length} elements selected`
}
return labelProperty
? String(value?.[0]?.[labelProperty])
return labelProperty && typeof value?.[0] === 'object'
? String((value?.[0] as any)?.[labelProperty])
: String(value?.[0])
}
},
Expand All @@ -158,7 +160,7 @@ const props = withDefaults(
},
)
const emits = defineEmits<{
(event: 'update:modelValue', value?: any): void
(event: 'update:modelValue', value?: T): void
}>()
const appConfig = useAppConfig()
const shape = computed(() => props.shape ?? appConfig.nui.defaultShapes?.input)
Expand Down Expand Up @@ -200,7 +202,13 @@ const placeholder = computed(() => {
const value = computed(() => {
if (props.modelModifiers.prop && props.properties.value) {
const attr = props.properties.value
return props.items.find((i) => i[attr] === vmodel.value)
return props.items.find(
(item) =>
item &&
typeof item === 'object' &&
attr in item &&
(item as any)[attr] === vmodel.value,
)
}
return vmodel.value
})
Expand Down Expand Up @@ -291,22 +299,24 @@ const value = computed(() => {
<template v-else-if="value">
<BaseAvatar
v-if="
props.properties.media && value[props.properties.media]
props.properties.media &&
(value as any)[props.properties.media]
"
:src="value[props.properties.media]"
:src="(value as any)[props.properties.media]"
size="xs"
class="-ms-2 me-2"
/>
<BaseIconBox
v-else-if="
props.properties.icon && value[props.properties.icon]
props.properties.icon &&
(value as any)[props.properties.icon]
"
size="xs"
shape="rounded"
class="-ms-2 me-2"
>
<Icon
:name="value[props.properties.icon]"
:name="(value as any)[props.properties.icon]"
class="h-4 w-4"
/>
</BaseIconBox>
Expand All @@ -316,9 +326,9 @@ const value = computed(() => {
>
{{
props.properties.label
? value[props.properties.label]
? (value as any)[props.properties.label]
: props.properties.value
? value[props.properties.value]
? (value as any)[props.properties.value]
: value
}}
</div>
Expand Down Expand Up @@ -355,11 +365,13 @@ const value = computed(() => {
v-for="item in props.items"
v-slot="{ active, selected }"
:key="
props.properties.value ? item[props.properties.value] : item
props.properties.value
? (item as any)[props.properties.value]
: item
"
:value="
props.modelModifiers.prop && props.properties.value
? item[props.properties.value]
? (item as any)[props.properties.value]
: item
"
as="template"
Expand All @@ -375,58 +387,30 @@ const value = computed(() => {
:active="active"
:selected="selected"
>
<BaseAvatar
v-if="
props.properties.media && item[props.properties.media]
"
:src="item[props.properties.media]"
size="xs"
/>
<BaseIconBox
v-else-if="
props.properties.icon && item[props.properties.icon]
"
size="sm"
shape="rounded"
>
<Icon
:name="item[props.properties.icon]"
class="text-muted-400 group-hover/nui-listbox-option:text-primary-500 h-5 w-5 transition-colors duration-200"
/>
</BaseIconBox>

<div class="nui-listbox-option-inner">
<BaseHeading
as="h4"
:weight="selected ? 'semibold' : 'normal'"
size="sm"
class="nui-listbox-heading"
>
{{
props.properties.label
? item[props.properties.label]
: props.properties.value
? item[props.properties.value]
: item
}}
</BaseHeading>
<BaseText
v-if="
<BaseListboxItem
:value="{
value: props.properties.label
? (item as any)[props.properties.label]
: props.properties.value
? (item as any)[props.properties.value]
: (item as any),
label:
props.properties.label &&
(item as any)[props.properties.label],
sublabel:
props.properties.sublabel &&
item[props.properties.sublabel]
"
size="xs"
class="nui-listbox-text"
>
{{ item[props.properties.sublabel] }}
</BaseText>
</div>
<span v-if="selected" class="nui-listbox-selected-icon">
<Icon
:name="selectedIcon"
class="nui-listbobx-selected-icon-inner"
/>
</span>
(item as any)[props.properties.sublabel],
media:
props.properties.media &&
(item as any)[props.properties.media],
icon:
props.properties.icon &&
(item as any)[props.properties.icon],
}"
:selected-icon="props.selectedIcon"
:active="active"
:selected="selected"
/>
</slot>
</li>
</ListboxOption>
Expand Down
Loading

0 comments on commit b18930d

Please sign in to comment.