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": {
|
||||
"@headlessui/vue": "^1.7.7",
|
||||
"@heroicons/vue": "^2.0.13",
|
||||
"@vuepic/vue-datepicker": "^3.6.5",
|
||||
"@vueuse/core": "^9.10.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
@ -698,6 +699,21 @@
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.2.45.tgz",
|
||||
"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": {
|
||||
"version": "9.10.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "9.10.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
|
@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@headlessui/vue": "^1.7.7",
|
||||
"@heroicons/vue": "^2.0.13",
|
||||
"@vuepic/vue-datepicker": "^3.6.5",
|
||||
"@vueuse/core": "^9.10.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash.get": "^4.4.2",
|
||||
|
@ -33,7 +33,7 @@
|
||||
as="h3"
|
||||
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" />
|
||||
<button v-if="closeButton" @click="closeModal">
|
||||
<XMarkIcon class="h-6 w-6" />
|
||||
|
@ -31,6 +31,7 @@
|
||||
:class="inputClass"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:readonly="readonly"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@focus="onFocus"
|
||||
@ -52,26 +53,22 @@ import set from 'lodash.set';
|
||||
import get from 'lodash.get';
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import { FormData, FormErrors } from './form.types';
|
||||
import { FormData, FormErrors, InputType } from './form.types';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?:
|
||||
| 'text'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'color'
|
||||
| 'email'
|
||||
| 'checkbox'
|
||||
| 'radio';
|
||||
type?: InputType;
|
||||
forIdPrefix?: string;
|
||||
label: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
readonly?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
}
|
||||
);
|
||||
|
||||
@ -90,7 +87,9 @@ const fieldName = computed(() =>
|
||||
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 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,6 +31,12 @@
|
||||
as="template"
|
||||
>
|
||||
<li :class="itemClass(active, selected)">
|
||||
<slot
|
||||
name="option"
|
||||
:selected="selected"
|
||||
:active="active"
|
||||
:option="option"
|
||||
>
|
||||
<span
|
||||
:class="[
|
||||
selected ? 'font-medium' : 'font-normal',
|
||||
@ -39,6 +45,7 @@
|
||||
>
|
||||
{{ option.name }}
|
||||
</span>
|
||||
</slot>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute inset-y-0 left-0 flex items-center pl-2 text-blue-600"
|
||||
@ -62,13 +69,7 @@ import {
|
||||
ListboxOption,
|
||||
} from '@headlessui/vue';
|
||||
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/vue/24/outline';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string | number;
|
||||
name: string;
|
||||
description?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
import { SelectOption } from '../form.types';
|
||||
|
||||
const props = withDefaults(
|
||||
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"
|
||||
:model-value="(value as string)"
|
||||
@update:model-value="(newValue) => setValue(newValue)"
|
||||
/>
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<slot name="option" v-bind="slotProps" />
|
||||
</template>
|
||||
</SelectVue>
|
||||
</template>
|
||||
<template #default><slot /></template>
|
||||
</FormField>
|
||||
@ -22,7 +26,8 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
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(
|
||||
defineProps<{
|
||||
|
@ -5,3 +5,27 @@ export interface FormSubmit {
|
||||
formData: FormData;
|
||||
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';
|
||||
import jfetch from '../../utils/jfetch';
|
||||
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 FormAlert from '../form/FormAlert.vue';
|
||||
import FormField from '../form/FormField.vue';
|
||||
import FormSelectField from '../form/fields/FormSelectField.vue';
|
||||
import { SelectOption } from '../form/base/Select.vue';
|
||||
import { IsRequired, MinLength } from '../form/validators';
|
||||
import Modal from '../Modal.vue';
|
||||
import FormColorField from '../form/fields/FormColorField.vue';
|
||||
|
@ -1,3 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@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>
|
||||
<h1>Dashboard</h1>
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -9,7 +26,46 @@
|
||||
import { ref } from 'vue';
|
||||
import { useUserStore } from '../store/user.store';
|
||||
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 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>
|
||||
|
Loading…
Reference in New Issue
Block a user