108 lines
2.8 KiB
Vue
108 lines
2.8 KiB
Vue
<template>
|
|
<form @submit.prevent="onSubmit">
|
|
<div class="flex flex-col space-y-5">
|
|
<slot />
|
|
</div>
|
|
</form>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useDebounceFn } from '@vueuse/shared';
|
|
import get from 'lodash.get';
|
|
import { provide, ref, watch } from 'vue';
|
|
import deepUnref from '../../utils/deep-unref';
|
|
import { FormData, FormErrors, FormSubmit } from './form.types';
|
|
import { FormValidator } from './validator.types';
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
modelValue?: FormData;
|
|
errors?: FormErrors;
|
|
validators?: FormValidator[];
|
|
}>(),
|
|
{
|
|
modelValue: () => ({}),
|
|
errors: () => ({}),
|
|
validators: () => [],
|
|
}
|
|
);
|
|
|
|
const isValid = ref(true);
|
|
const invalidFields = ref<string[]>([]);
|
|
const formData = ref<FormData>(props.modelValue);
|
|
const formErrors = ref<FormErrors>(props.errors);
|
|
const validateField = async (field: string) => {
|
|
let valid = true;
|
|
if (formErrors.value[field]) {
|
|
delete formErrors.value[field];
|
|
if (invalidFields.value.includes(field)) {
|
|
invalidFields.value = invalidFields.value.filter((x) => x !== field);
|
|
}
|
|
}
|
|
|
|
for (const validator of props.validators) {
|
|
if (validator.field !== field) continue;
|
|
for (const fn of validator.validators) {
|
|
const result = await fn(
|
|
field,
|
|
get(formData.value, field),
|
|
formData.value
|
|
);
|
|
if (!result.isValid) {
|
|
formErrors.value = {
|
|
...formErrors.value,
|
|
[field]: [...(formErrors.value[field] || []), result.message],
|
|
};
|
|
invalidFields.value.push(field);
|
|
valid = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!valid && isValid.value) {
|
|
isValid.value = false;
|
|
return;
|
|
}
|
|
|
|
isValid.value = !invalidFields.value.length;
|
|
};
|
|
|
|
const validateAll = async () => {
|
|
const fields = props.validators
|
|
.map((validator) => validator.field)
|
|
.filter((value, index, array) => array.indexOf(value) === index);
|
|
if (!fields.length) return;
|
|
return Promise.allSettled(fields.map((field) => validateField(field)));
|
|
};
|
|
|
|
const fieldChange = useDebounceFn(validateField, 300);
|
|
|
|
provide('formData', formData);
|
|
provide('formErrors', formErrors);
|
|
provide('fieldChange', fieldChange);
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'submit', data: FormSubmit): void;
|
|
(e: 'update:modelValue', data: FormData): void;
|
|
(e: 'update:errors', errors: FormErrors): void;
|
|
(e: 'update:validity', validity: boolean): void;
|
|
}>();
|
|
|
|
watch(formData, (val) => emit('update:modelValue', val), { deep: true });
|
|
watch(formErrors, (val) => emit('update:errors', val));
|
|
watch(isValid, (val) => emit('update:validity', val));
|
|
|
|
const onSubmit = async () => {
|
|
await validateAll();
|
|
emit('submit', {
|
|
isValid: isValid.value,
|
|
formData: deepUnref(formData.value),
|
|
fieldErrors: deepUnref(formErrors.value),
|
|
});
|
|
};
|
|
|
|
defineExpose({
|
|
isValid,
|
|
});
|
|
</script>
|