include datepicker and autocomplete
This commit is contained in:
parent
4a696eef0d
commit
266e7194c0
56
package-lock.json
generated
56
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/vue": "^1.7.7",
|
"@headlessui/vue": "^1.7.7",
|
||||||
"@heroicons/vue": "^2.0.13",
|
"@heroicons/vue": "^2.0.13",
|
||||||
|
"@vuepic/vue-datepicker": "^3.6.5",
|
||||||
"@vueuse/core": "^9.10.0",
|
"@vueuse/core": "^9.10.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
@ -698,6 +699,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz",
|
||||||
"integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg=="
|
"integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@vuepic/vue-datepicker": {
|
||||||
|
"version": "3.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.6.5.tgz",
|
||||||
|
"integrity": "sha512-axf9st9UIjxbLUL/WjbYPSzkNKfedfDqV3wQIBeCAekN2/w45RNmWfIUXIxpbfZKiRo2ie+U23IW20I33oGauw==",
|
||||||
|
"dependencies": {
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^1.3.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"vue": ">=3.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vueuse/core": {
|
"node_modules/@vueuse/core": {
|
||||||
"version": "9.10.0",
|
"version": "9.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.10.0.tgz",
|
||||||
@ -1000,6 +1016,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
|
||||||
"integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
|
"integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.11"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/date-fns"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/date-fns-tz": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"date-fns": ">=2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
@ -2456,6 +2492,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz",
|
||||||
"integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg=="
|
"integrity": "sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg=="
|
||||||
},
|
},
|
||||||
|
"@vuepic/vue-datepicker": {
|
||||||
|
"version": "3.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-3.6.5.tgz",
|
||||||
|
"integrity": "sha512-axf9st9UIjxbLUL/WjbYPSzkNKfedfDqV3wQIBeCAekN2/w45RNmWfIUXIxpbfZKiRo2ie+U23IW20I33oGauw==",
|
||||||
|
"requires": {
|
||||||
|
"date-fns": "^2.29.3",
|
||||||
|
"date-fns-tz": "^1.3.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@vueuse/core": {
|
"@vueuse/core": {
|
||||||
"version": "9.10.0",
|
"version": "9.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.10.0.tgz",
|
||||||
@ -2632,6 +2677,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz",
|
||||||
"integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
|
"integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w=="
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "2.29.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||||
|
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||||
|
},
|
||||||
|
"date-fns-tz": {
|
||||||
|
"version": "1.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.7.tgz",
|
||||||
|
"integrity": "sha512-1t1b8zyJo+UI8aR+g3iqr5fkUHWpd58VBx8J/ZSQ+w7YrGlw80Ag4sA86qkfCXRBLmMc4I2US+aPMd4uKvwj5g==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"de-indent": {
|
"de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/vue": "^1.7.7",
|
"@headlessui/vue": "^1.7.7",
|
||||||
"@heroicons/vue": "^2.0.13",
|
"@heroicons/vue": "^2.0.13",
|
||||||
|
"@vuepic/vue-datepicker": "^3.6.5",
|
||||||
"@vueuse/core": "^9.10.0",
|
"@vueuse/core": "^9.10.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
as="h3"
|
as="h3"
|
||||||
class="text-lg font-bold leading-6 text-gray-900"
|
class="text-lg font-bold leading-6 text-gray-900"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="mb-4 flex items-center justify-between">
|
||||||
<slot name="title" />
|
<slot name="title" />
|
||||||
<button v-if="closeButton" @click="closeModal">
|
<button v-if="closeButton" @click="closeModal">
|
||||||
<XMarkIcon class="h-6 w-6" />
|
<XMarkIcon class="h-6 w-6" />
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
:class="inputClass"
|
:class="inputClass"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
|
:readonly="readonly"
|
||||||
@input="onInput"
|
@input="onInput"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@ -52,26 +53,22 @@ import set from 'lodash.set';
|
|||||||
import get from 'lodash.get';
|
import get from 'lodash.get';
|
||||||
import { computed, ref, Ref } from 'vue';
|
import { computed, ref, Ref } from 'vue';
|
||||||
import { inject } from 'vue';
|
import { inject } from 'vue';
|
||||||
import { FormData, FormErrors } from './form.types';
|
import { FormData, FormErrors, InputType } from './form.types';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
type?:
|
type?: InputType;
|
||||||
| 'text'
|
forIdPrefix?: string;
|
||||||
| 'number'
|
|
||||||
| 'password'
|
|
||||||
| 'color'
|
|
||||||
| 'email'
|
|
||||||
| 'checkbox'
|
|
||||||
| 'radio';
|
|
||||||
label: string;
|
label: string;
|
||||||
name: string;
|
name: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
readonly?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
type: 'text',
|
type: 'text',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
readonly: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -90,7 +87,9 @@ const fieldName = computed(() =>
|
|||||||
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
||||||
);
|
);
|
||||||
|
|
||||||
const forId = computed(() => `form-${fieldName.value}`);
|
const forId = computed(
|
||||||
|
() => `${props.forIdPrefix || 'form'}-${fieldName.value}`
|
||||||
|
);
|
||||||
const value = computed(() => get(formData?.value, fieldName.value));
|
const value = computed(() => get(formData?.value, fieldName.value));
|
||||||
const errors = computed(() => formErrors?.value[fieldName.value] || []);
|
const errors = computed(() => formErrors?.value[fieldName.value] || []);
|
||||||
|
|
||||||
|
209
src/components/form/base/Autocomplete.vue
Normal file
209
src/components/form/base/Autocomplete.vue
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<Combobox :modelValue="localValue" @update:modelValue="valueChanged" nullable>
|
||||||
|
<div class="relative mt-1">
|
||||||
|
<div
|
||||||
|
class="relative w-full cursor-default overflow-hidden rounded-md bg-white text-left focus:outline-none focus-visible:ring-2 sm:text-sm"
|
||||||
|
>
|
||||||
|
<ComboboxInput
|
||||||
|
:id="forId"
|
||||||
|
:class="inputClass"
|
||||||
|
:displayValue="(option: any) => getLabel(option)"
|
||||||
|
@change="query = $event.target.value"
|
||||||
|
/>
|
||||||
|
<ComboboxButton
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center pr-2"
|
||||||
|
>
|
||||||
|
<ArrowPathIcon
|
||||||
|
class="h-5 w-5 animate-spin text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
v-if="searching"
|
||||||
|
/>
|
||||||
|
<ChevronUpDownIcon class="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||||
|
</ComboboxButton>
|
||||||
|
</div>
|
||||||
|
<TransitionRoot
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
@after-leave="query = ''"
|
||||||
|
>
|
||||||
|
<ComboboxOptions
|
||||||
|
class="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="options.length === 0 && (query !== '' || searching)"
|
||||||
|
class="relative cursor-default select-none py-2 px-4 text-gray-700"
|
||||||
|
>
|
||||||
|
<slot name="notfound" :query="query" :searching="searching">
|
||||||
|
{{ searching ? 'Loading..' : 'Nothing found.' }}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ComboboxOption
|
||||||
|
v-for="option in options"
|
||||||
|
as="template"
|
||||||
|
:key="option[bindValue]"
|
||||||
|
:value="option"
|
||||||
|
:disabled="option?.disabled"
|
||||||
|
v-slot="{ selected, active }"
|
||||||
|
>
|
||||||
|
<li :class="itemClass(active, selected)">
|
||||||
|
<slot
|
||||||
|
name="option"
|
||||||
|
:selected="selected"
|
||||||
|
:active="active"
|
||||||
|
:option="option"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="block truncate"
|
||||||
|
:class="{ 'font-medium': selected, 'font-normal': !selected }"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
typeof bindLabel === 'function'
|
||||||
|
? bindLabel(option)
|
||||||
|
: option[bindLabel]
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
<span
|
||||||
|
v-if="selected"
|
||||||
|
class="absolute inset-y-0 left-0 flex items-center pl-2 text-blue-600"
|
||||||
|
>
|
||||||
|
<CheckIcon class="h-5 w-5" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ComboboxOption>
|
||||||
|
</ComboboxOptions>
|
||||||
|
</TransitionRoot>
|
||||||
|
</div>
|
||||||
|
</Combobox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, shallowRef } from 'vue';
|
||||||
|
import {
|
||||||
|
Combobox,
|
||||||
|
ComboboxInput,
|
||||||
|
ComboboxButton,
|
||||||
|
ComboboxOptions,
|
||||||
|
ComboboxOption,
|
||||||
|
TransitionRoot,
|
||||||
|
} from '@headlessui/vue';
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/20/solid';
|
||||||
|
import deepUnref from '../../../utils/deep-unref';
|
||||||
|
import { ArrowPathIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { useDebounceFn } from '@vueuse/shared';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
forId?: string;
|
||||||
|
modelValue: any;
|
||||||
|
initialOptions?: any[];
|
||||||
|
searchFn?: (query: string) => Promise<any[]>;
|
||||||
|
bindValue?: string;
|
||||||
|
bindLabel?: string | ((obj: any) => string);
|
||||||
|
disabled?: boolean;
|
||||||
|
invalid?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: null,
|
||||||
|
disabled: false,
|
||||||
|
invalid: false,
|
||||||
|
bindValue: 'id',
|
||||||
|
bindLabel: 'displayName',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!props.initialOptions && !props.searchFn) {
|
||||||
|
throw new Error(
|
||||||
|
'[Autocomplete] you must either pass initial props or a search function!'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = shallowRef<any[]>(props.initialOptions || []);
|
||||||
|
const query = ref('');
|
||||||
|
const searching = ref(false);
|
||||||
|
|
||||||
|
const inputClass = computed(() => {
|
||||||
|
return [
|
||||||
|
props.invalid
|
||||||
|
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||||
|
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||||
|
`block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||||
|
'py-2 pl-3 pr-10 text-left cursor-default h-[38px] border-[1px] focus:ring-1',
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemClass = (active: boolean, selected: boolean) => {
|
||||||
|
const classList = ['relative cursor-default select-none py-2 pl-8 pr-4'];
|
||||||
|
if (selected) {
|
||||||
|
classList.push('text-blue-900');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (active && selected) {
|
||||||
|
classList.push('bg-blue-200');
|
||||||
|
} else if (active || selected) {
|
||||||
|
classList.push('bg-blue-100');
|
||||||
|
} else {
|
||||||
|
classList.push('text-gray-900');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classList;
|
||||||
|
};
|
||||||
|
|
||||||
|
const localValue = shallowRef(props.modelValue);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:modelValue', value: string | number | null): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const valueChanged = (option: any) => {
|
||||||
|
localValue.value = option;
|
||||||
|
emit('update:modelValue', deepUnref(localValue.value || null));
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchFnDebounced = useDebounceFn((text: string) => {
|
||||||
|
searching.value = true;
|
||||||
|
props
|
||||||
|
.searchFn?.(text)
|
||||||
|
.then((response) => {
|
||||||
|
options.value = response;
|
||||||
|
})
|
||||||
|
.catch(console.error)
|
||||||
|
.finally(() => (searching.value = false));
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
const getLabel = (option: any) =>
|
||||||
|
option
|
||||||
|
? typeof props.bindLabel === 'function'
|
||||||
|
? props.bindLabel(option)
|
||||||
|
: option[props.bindLabel]
|
||||||
|
: '';
|
||||||
|
|
||||||
|
const localSearch = (text: string) => {
|
||||||
|
if (!props.initialOptions) return;
|
||||||
|
|
||||||
|
options.value =
|
||||||
|
text === ''
|
||||||
|
? props.initialOptions
|
||||||
|
: props.initialOptions.filter((option) =>
|
||||||
|
getLabel(option)
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.includes(text.toLowerCase().replace(/\s+/g, ''))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
query,
|
||||||
|
(text) => {
|
||||||
|
if (props.searchFn) {
|
||||||
|
return searchFnDebounced(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
localSearch(text);
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
@ -31,14 +31,21 @@
|
|||||||
as="template"
|
as="template"
|
||||||
>
|
>
|
||||||
<li :class="itemClass(active, selected)">
|
<li :class="itemClass(active, selected)">
|
||||||
<span
|
<slot
|
||||||
:class="[
|
name="option"
|
||||||
selected ? 'font-medium' : 'font-normal',
|
:selected="selected"
|
||||||
'block truncate',
|
:active="active"
|
||||||
]"
|
:option="option"
|
||||||
>
|
>
|
||||||
{{ option.name }}
|
<span
|
||||||
</span>
|
:class="[
|
||||||
|
selected ? 'font-medium' : 'font-normal',
|
||||||
|
'block truncate',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ option.name }}
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
<span
|
<span
|
||||||
v-if="selected"
|
v-if="selected"
|
||||||
class="absolute inset-y-0 left-0 flex items-center pl-2 text-blue-600"
|
class="absolute inset-y-0 left-0 flex items-center pl-2 text-blue-600"
|
||||||
@ -62,13 +69,7 @@ import {
|
|||||||
ListboxOption,
|
ListboxOption,
|
||||||
} from '@headlessui/vue';
|
} from '@headlessui/vue';
|
||||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/outline';
|
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { SelectOption } from '../form.types';
|
||||||
export interface SelectOption {
|
|
||||||
value: string | number;
|
|
||||||
name: string;
|
|
||||||
description?: string;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
53
src/components/form/fields/FormAutocompleteField.vue
Normal file
53
src/components/form/fields/FormAutocompleteField.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<FormField
|
||||||
|
:name="name"
|
||||||
|
:label="label"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<template #input="{ invalid, forId, value, setValue }">
|
||||||
|
<Autocomplete
|
||||||
|
:for-id="forId"
|
||||||
|
:invalid="invalid"
|
||||||
|
:disabled="disabled"
|
||||||
|
:initialOptions="initialOptions"
|
||||||
|
:searchFn="searchFn"
|
||||||
|
:bindValue="bindValue"
|
||||||
|
:bindLabel="bindLabel"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:model-value="value"
|
||||||
|
@update:model-value="(newValue) => setValue(newValue)"
|
||||||
|
>
|
||||||
|
<template #notfound="slotProps">
|
||||||
|
<slot name="notfound" v-bind="slotProps" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #option="slotProps">
|
||||||
|
<slot name="option" v-bind="slotProps" />
|
||||||
|
</template>
|
||||||
|
</Autocomplete>
|
||||||
|
</template>
|
||||||
|
<template #default><slot /></template>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormField from '../FormField.vue';
|
||||||
|
import Autocomplete from '../base/Autocomplete.vue';
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
initialOptions?: any[];
|
||||||
|
searchFn?: (query: string) => Promise<any[]>;
|
||||||
|
bindValue?: string;
|
||||||
|
bindLabel?: string | ((obj: any) => string);
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
43
src/components/form/fields/FormDateField.vue
Normal file
43
src/components/form/fields/FormDateField.vue
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<FormField
|
||||||
|
:name="name"
|
||||||
|
:label="label"
|
||||||
|
:disabled="disabled"
|
||||||
|
for-id-prefix="dp-input"
|
||||||
|
>
|
||||||
|
<template #input="{ invalid, fieldName, value, setValue }">
|
||||||
|
<Datepicker
|
||||||
|
text-input
|
||||||
|
arrow-navigation
|
||||||
|
:class="[invalid ? 'dp__invalid' : '']"
|
||||||
|
:uid="fieldName"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:disabled="disabled"
|
||||||
|
:clearable="clearable"
|
||||||
|
v-bind="$attrs"
|
||||||
|
:model-value="(value as string)"
|
||||||
|
@update:model-value="setValue"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #default><slot /></template>
|
||||||
|
</FormField>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import FormField from '../FormField.vue';
|
||||||
|
import Datepicker from '@vuepic/vue-datepicker';
|
||||||
|
import { ExtractComponentProps } from '../../../utils/extract-component-props';
|
||||||
|
|
||||||
|
interface DatePickerProps extends ExtractComponentProps<typeof Datepicker> {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
clearable?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://vue3datepicker.com/
|
||||||
|
const props = withDefaults(defineProps<DatePickerProps>(), {
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
</script>
|
@ -14,7 +14,11 @@
|
|||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:model-value="(value as string)"
|
:model-value="(value as string)"
|
||||||
@update:model-value="(newValue) => setValue(newValue)"
|
@update:model-value="(newValue) => setValue(newValue)"
|
||||||
/>
|
>
|
||||||
|
<template #option="slotProps">
|
||||||
|
<slot name="option" v-bind="slotProps" />
|
||||||
|
</template>
|
||||||
|
</SelectVue>
|
||||||
</template>
|
</template>
|
||||||
<template #default><slot /></template>
|
<template #default><slot /></template>
|
||||||
</FormField>
|
</FormField>
|
||||||
@ -22,7 +26,8 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import FormField from '../FormField.vue';
|
import FormField from '../FormField.vue';
|
||||||
import SelectVue, { SelectOption } from '../base/Select.vue';
|
import SelectVue from '../base/Select.vue';
|
||||||
|
import { SelectOption } from '../form.types';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -5,3 +5,27 @@ export interface FormSubmit {
|
|||||||
formData: FormData;
|
formData: FormData;
|
||||||
fieldErrors: FormErrors;
|
fieldErrors: FormErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type InputType =
|
||||||
|
| 'text'
|
||||||
|
| 'number'
|
||||||
|
| 'password'
|
||||||
|
| 'color'
|
||||||
|
| 'email'
|
||||||
|
| 'checkbox'
|
||||||
|
| 'radio';
|
||||||
|
|
||||||
|
export interface SharedFieldProps {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
value: string | number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
@ -33,12 +33,11 @@ import {
|
|||||||
} from '../../interfaces/storage.interfaces';
|
} from '../../interfaces/storage.interfaces';
|
||||||
import jfetch from '../../utils/jfetch';
|
import jfetch from '../../utils/jfetch';
|
||||||
import takeError from '../../utils/take-error';
|
import takeError from '../../utils/take-error';
|
||||||
import { FormSubmit } from '../form/form.types';
|
import { FormSubmit, SelectOption } from '../form/form.types';
|
||||||
import Form from '../form/Form.vue';
|
import Form from '../form/Form.vue';
|
||||||
import FormAlert from '../form/FormAlert.vue';
|
import FormAlert from '../form/FormAlert.vue';
|
||||||
import FormField from '../form/FormField.vue';
|
import FormField from '../form/FormField.vue';
|
||||||
import FormSelectField from '../form/fields/FormSelectField.vue';
|
import FormSelectField from '../form/fields/FormSelectField.vue';
|
||||||
import { SelectOption } from '../form/base/Select.vue';
|
|
||||||
import { IsRequired, MinLength } from '../form/validators';
|
import { IsRequired, MinLength } from '../form/validators';
|
||||||
import Modal from '../Modal.vue';
|
import Modal from '../Modal.vue';
|
||||||
import FormColorField from '../form/fields/FormColorField.vue';
|
import FormColorField from '../form/fields/FormColorField.vue';
|
||||||
|
@ -1,3 +1,13 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
@import '@vuepic/vue-datepicker/src/VueDatePicker/style/main.scss';
|
||||||
|
|
||||||
|
.dp__input {
|
||||||
|
@apply block w-full rounded-md border-gray-300 shadow-sm transition-colors duration-200 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm;
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dp__invalid .dp__input {
|
||||||
|
@apply border-red-300 focus:border-red-500 focus:ring-red-500;
|
||||||
|
}
|
||||||
|
5
src/utils/extract-component-props.ts
Normal file
5
src/utils/extract-component-props.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export type ExtractComponentProps<TComponent> = TComponent extends new () => {
|
||||||
|
$props: infer P;
|
||||||
|
}
|
||||||
|
? P
|
||||||
|
: never;
|
@ -2,6 +2,23 @@
|
|||||||
<StandardLayout>
|
<StandardLayout>
|
||||||
<h1>Dashboard</h1>
|
<h1>Dashboard</h1>
|
||||||
<p>Hello, {{ user.name }}</p>
|
<p>Hello, {{ user.name }}</p>
|
||||||
|
|
||||||
|
<Form @submit="test" :validators="validators">
|
||||||
|
<FormDateField
|
||||||
|
name="date"
|
||||||
|
label="datepicker"
|
||||||
|
locale="en-UK"
|
||||||
|
clearable
|
||||||
|
:enable-time-picker="false"
|
||||||
|
/>
|
||||||
|
<FormField name="test" label="test" />
|
||||||
|
<FormAutocompleteField
|
||||||
|
:search-fn="searchFn"
|
||||||
|
name="autocomplete"
|
||||||
|
label="autocomplete"
|
||||||
|
/>
|
||||||
|
<button type="submit">test</button>
|
||||||
|
</Form>
|
||||||
</StandardLayout>
|
</StandardLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -9,7 +26,46 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useUserStore } from '../store/user.store';
|
import { useUserStore } from '../store/user.store';
|
||||||
import StandardLayout from '../components/StandardLayout.vue';
|
import StandardLayout from '../components/StandardLayout.vue';
|
||||||
|
import FormDateField from '../components/form/fields/FormDateField.vue';
|
||||||
|
import Form from '../components/form/Form.vue';
|
||||||
|
import FormField from '../components/form/FormField.vue';
|
||||||
|
import { IsRequired } from '../components/form/validators';
|
||||||
|
import FormAutocompleteField from '../components/form/fields/FormAutocompleteField.vue';
|
||||||
|
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const user = ref(userStore.user);
|
const user = ref(userStore.user);
|
||||||
|
|
||||||
|
const test = console.log;
|
||||||
|
const validators = [{ field: 'date', validators: [IsRequired()] }];
|
||||||
|
|
||||||
|
const autocompleteTest = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
displayName: 'search things',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
displayName: 'potato',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
displayName: 'carrot',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
displayName: 'beet',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const searchFn = async (query: string) => {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||||
|
return query === ''
|
||||||
|
? autocompleteTest
|
||||||
|
: autocompleteTest.filter((person) =>
|
||||||
|
person.displayName
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.includes(query.toLowerCase().replace(/\s+/g, ''))
|
||||||
|
);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
Loading…
Reference in New Issue
Block a user