Skip to content

Commit

Permalink
feat(BaseAutocomplete): use generic component definition
Browse files Browse the repository at this point in the history
  • Loading branch information
stafyniaksacha committed Aug 12, 2023
1 parent 5407410 commit cce6d91
Show file tree
Hide file tree
Showing 4 changed files with 139 additions and 51 deletions.
2 changes: 1 addition & 1 deletion .playground/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const routes = computed(() =>
<slot />
</div>
</div>
<div class="fixed top-0 end-0 pr-6 pt-6 z-10">
<div class="fixed top-0 end-0 pr-6 pt-6 z-50">
<BaseSelect v-model="color.preference" size="sm">
<option value="system">system</option>
<option value="light">light</option>
Expand Down
1 change: 1 addition & 0 deletions .playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default defineNuxtConfig({
shim: false,
},
hooks: {
// @ts-ignore
'tailwindcss:resolvedConfig'(config) {
addTemplate({
filename: 'tailwind.config.ts', // gets prepended by .nuxt/
Expand Down
65 changes: 61 additions & 4 deletions .playground/pages/tests/form/autocomplete.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,32 @@
<script setup lang="ts">
import { cp } from 'fs'
definePageMeta({
title: 'Autocomplete',
icon: 'mdi:auto-fix',
description: 'SVG icons',
section: 'form',
})
const items = ref<any[]>(['Javascript', 'Vue.js', 'React.js', 'Angular'])
const selection = ref('')
const multiple = ref<string[]>([])
const items = ref(['Javascript', 'Vue.js', 'React.js', 'Angular'])
const objectSelection = ref({ name: '' })
const itemsObject = ref([
{
name: 'Javascript',
},
{
name: 'Vue.js',
},
{
name: 'React.js',
},
{
name: 'Angular',
},
])
</script>

<template>
Expand Down Expand Up @@ -39,7 +59,13 @@ const items = ref<any[]>(['Javascript', 'Vue.js', 'React.js', 'Angular'])
multiple
/>
<BaseAutocomplete
v-model="multiple"
:items="items"
:display-value="
(item) => {
return item || ''
}
"
label="Test"
placeholder="Let's test autocomplete"
error="This is an error message"
Expand All @@ -49,24 +75,55 @@ const items = ref<any[]>(['Javascript', 'Vue.js', 'React.js', 'Angular'])
multiple
/>
<BaseAutocomplete
v-model="selection"
:items="items"
:display-value="
(item) => {
return item || ''
}
"
:filter-items="
(query, items) => {
if (!query) return items || []
return (
items?.filter(
(item) => item.toLowerCase().indexOf(query.toLowerCase()) > -1
) || []
)
}
"
label="Test"
placeholder="Let's test autocomplete"
error="This is an error message"
shape="curved"
label-float
clearable
multiple
/>
<BaseAutocomplete
:items="items"
v-model="objectSelection"
:items="itemsObject"
:filter-items="
(query, items) => {
if (!query) return items || []
return (
items?.filter(
(item) =>
item.name.toLowerCase().indexOf(query.toLowerCase()) > -1
) || []
)
}
"
:display-value="
(item) => {
return item?.name || ''
}
"
label="Test"
placeholder="Let's test autocomplete"
error="This is an error message"
shape="full"
label-float
clearable
multiple
/>
</div>
</div>
Expand Down
122 changes: 76 additions & 46 deletions components/form/BaseAutocomplete.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<script setup lang="ts">
<script
setup
lang="ts"
generic="T extends object | string | boolean | number | null | undefined"
>
import {
Combobox,
ComboboxInput,
Expand All @@ -13,12 +17,12 @@ const props = withDefaults(
/**
* The model value of the component.
*/
modelValue: any
modelValue?: T | T[]
/**
* The items to display in the component.
*/
items?: any[]
items?: T[]
/**
* The shape of the component.
Expand Down Expand Up @@ -106,7 +110,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 @@ -118,7 +122,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 @@ -146,6 +150,7 @@ const props = withDefaults(
}
}>(),
{
modelValue: undefined,
items: () => [],
shape: undefined,
icon: undefined,
Expand Down Expand Up @@ -184,12 +189,12 @@ const props = withDefaults(
)
const emits = defineEmits<{
(event: 'update:modelValue', value?: 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)
const value = useVModel(props, 'modelValue', emits) as Ref<T | T[]>
const items = shallowRef(props.items)
const query = ref('')
Expand Down Expand Up @@ -307,7 +312,41 @@ function clear() {
value.value = props.clearValue
}
function removeItem(item: any) {
const iconResolved = computed(() => {
if (
value.value &&
typeof value.value === 'object' &&
!Array.isArray(value.value) &&
'icon' in value.value &&
typeof value.value.icon === 'string'
) {
return value.value.icon
}
return props.icon
})
function isAutocompleteItem(
item: unknown
): item is Record<'name' | 'text' | 'media' | 'icon', string> {
if (
item &&
typeof item === 'object' &&
(('name' in item && typeof item.name === 'string') ||
('text' in item && typeof item.text === 'string') ||
('media' in item && typeof item.media === 'string') ||
('icon' in item && typeof item.icon === 'string'))
) {
return true
}
return false
}
function removeItem(item: T) {
if (!Array.isArray(value.value)) {
value.value = props.clearValue
return
}
for (let i = value.value.length - 1; i >= 0; --i) {
// eslint-disable-next-line eqeqeq
if (value.value[i] == item) {
Expand All @@ -320,41 +359,38 @@ function removeItem(item: any) {
<template>
<Combobox
v-model="value"
:multiple="props.multiple"
:disabled="props.disabled"
:multiple="multiple"
:disabled="disabled"
:class="[
'nui-autocomplete',
...wrapperStyle,
sizeStyle[props.size],
contrastStyle[props.contrast],
sizeStyle[size],
contrastStyle[contrast],
shape && shapeStyle[shape],
props.icon && 'nui-has-icon',
props.labelFloat && 'nui-autocomplete-label-float',
props.loading && 'nui-autocomplete-loading',
icon && 'nui-has-icon',
labelFloat && 'nui-autocomplete-label-float',
loading && 'nui-autocomplete-loading',
]"
as="div"
>
<ComboboxLabel
v-if="
('label' in $slots && !props.labelFloat) ||
(props.label && !props.labelFloat)
"
v-if="('label' in $slots && !labelFloat) || (label && !labelFloat)"
class="nui-autocomplete-label"
:class="labelStyle"
>
<slot name="label" v-bind="{ query, filteredItems, pending, items }">
{{ props.label }}
{{ label }}
</slot>
</ComboboxLabel>
<div v-if="props.multiple" class="nui-autocomplete-multiple">
<div v-if="multiple" class="nui-autocomplete-multiple">
<ul
v-if="Array.isArray(value) && value.length > 0"
class="nui-autocomplete-multiple-list"
>
<li v-for="item in value" :key="item.id">
<li v-for="item in value" :key="String(item)">
<div class="nui-autocomplete-multiple-list-item">
{{ props.displayValue(item) }}
{{ displayValue(item) }}
<button type="button" @click="removeItem(item)">
<Icon
:name="chipClearIcon"
Expand All @@ -370,48 +406,42 @@ function removeItem(item: any) {
<ComboboxInput
class="nui-autocomplete-input"
:class="inputStyle"
:display-value="props.multiple ? undefined : props.displayValue"
:placeholder="props.placeholder"
:disabled="props.disabled"
:display-value="multiple ? undefined : (displayValue as any)"
:placeholder="placeholder"
:disabled="disabled"
@change="query = $event.target.value"
/>
<ComboboxLabel
v-if="
('label' in $slots && props.labelFloat) ||
(props.label && props.labelFloat)
"
v-if="('label' in $slots && labelFloat) || (label && labelFloat)"
class="nui-label-float"
:class="labelStyle"
>
<slot name="label" v-bind="{ query, filteredItems, pending, items }">
{{ props.label }}
{{ label }}
</slot>
</ComboboxLabel>
<div v-if="props.icon || value?.icon" class="nui-autocomplete-icon">
<Icon
:name="value?.icon ?? props.icon"
class="nui-autocomplete-icon-inner"
/>
<div v-if="iconResolved" class="nui-autocomplete-icon">
<Icon :name="iconResolved" class="nui-autocomplete-icon-inner" />
</div>
<button
v-if="props.clearable && value"
v-if="clearable && value"
type="button"
class="nui-autocomplete-clear"
:class="iconStyle"
@click="clear"
>
<Icon :name="clearIcon" class="nui-autocomplete-clear-inner" />
</button>
<div v-if="props.loading" class="nui-autocomplete-placeload">
<BasePlaceload class="nui-placeload" :class="props.icon && 'ms-6'" />
<div v-if="loading" class="nui-autocomplete-placeload">
<BasePlaceload class="nui-placeload" :class="icon && 'ms-6'" />
</div>
</div>
<span
v-if="props.error && typeof props.error === 'string'"
v-if="error && typeof error === 'string'"
class="nui-autocomplete-error-text"
>
{{ props.error }}
{{ error }}
</span>
<TransitionRoot
Expand All @@ -431,7 +461,7 @@ function removeItem(item: any) {
v-bind="{ query, filteredItems, pending, items }"
>
<span class="nui-autocomplete-results-placeholder-text">
{{ props.i18n.pending }}
{{ i18n.pending }}
</span>
</slot>
</div>
Expand All @@ -441,7 +471,7 @@ function removeItem(item: any) {
>
<slot name="empty" v-bind="{ query, filteredItems, pending, items }">
<span class="nui-autocomplete-results-placeholder-text">
{{ props.i18n.empty }}
{{ i18n.empty }}
</span>
</slot>
</div>
Expand All @@ -463,7 +493,7 @@ function removeItem(item: any) {
<ComboboxOption
v-for="item in filteredItems"
v-slot="{ active, selected }"
:key="item.name"
:key="String(item)"
class="nui-autocomplete-results-item"
as="div"
:value="item"
Expand All @@ -483,10 +513,10 @@ function removeItem(item: any) {
<BaseAutocompleteItem
:shape="shape"
:value="
typeof item !== 'string'
isAutocompleteItem(item)
? item
: {
name: props.displayValue(item),
name: displayValue(item),
}
"
:active="active"
Expand Down

0 comments on commit cce6d91

Please sign in to comment.