homemanager-fe/src/components/form/Form.vue

136 lines
3.6 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 knownFields = ref<string[]>([]);
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((field) => knownFields.value.includes(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);
provide('registerField', (field: string) => {
if (knownFields.value?.includes(field)) return;
knownFields.value.push(field);
});
provide('unregisterField', (field: string) => {
if (!knownFields.value?.includes(field)) return;
knownFields.value = knownFields.value.filter((entry) => field !== entry);
invalidFields.value = invalidFields.value.filter((entry) => field !== entry);
if (formErrors.value[field]) {
delete formErrors.value[field];
}
if (formData.value[field]) {
delete formData.value[field];
}
if (!isValid.value) {
isValid.value = !invalidFields.value.length;
}
});
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),
});
};
const reset = () => {
formData.value = {};
formErrors.value = {};
invalidFields.value = [];
};
defineExpose({
isValid,
validateAll,
reset,
});
</script>