facilmap/frontend/src/lib/components/ui/validated-form/validated-form.vue

165 wiersze
4.3 KiB
Vue

<script lang="ts">
import { type Ref, onScopeDispose, reactive, readonly, ref, watchEffect, toRef, computed } from "vue";
import { type ExtendableEventMixin, extendableEventMixin, useDomEventListener } from "../../../utils/utils";
import { useToasts } from "../toasts/toasts.vue";
export interface ValidatedFormData {
isTouched: boolean;
isSubmitting: boolean;
isValidating: boolean;
formValidationError: string | undefined;
submit: () => Promise<void>;
setValidationPromise: (element: Element, promise: Promise<any> | undefined) => void;
}
export interface CustomSubmitEvent extends ExtendableEventMixin {
preventDefault(): void;
}
const allForms = reactive(new Map<Ref<HTMLFormElement | undefined>, ValidatedFormData>());
export function useValidatedForm(
formRef: Ref<HTMLFormElement | undefined>,
onSubmit: (event: CustomSubmitEvent) => void,
{ noValidate, formValidationError }: {
noValidate?: Ref<boolean>;
/** A form validation error that will prevent the form from submitting. */
formValidationError?: Ref<string | undefined>;
} = {}
): Readonly<ValidatedFormData> {
const toasts = useToasts();
const validationPromises = new Map<Element, Promise<any>>();
const isValidating = reactive(new Map<Element, boolean>());
const data: ValidatedFormData = reactive<Omit<ValidatedFormData, "formValidationError"> & { formValidationError: Ref<string | undefined> }>({
isTouched: false,
isSubmitting: false,
isValidating: false,
formValidationError: toRef(formValidationError),
submit: toasts.toastErrors(async () => {
data.isTouched = true;
data.isSubmitting = true;
try {
if (!noValidate?.value) {
await Promise.all(validationPromises.values());
if (!formRef.value!.checkValidity() || formValidationError?.value) {
return;
}
}
let prevented = false;
const event = {
...extendableEventMixin,
preventDefault() {
prevented = true;
}
};
onSubmit(event);
await event._awaitPromises();
if (!prevented) {
formRef.value?.submit();
}
} finally {
data.isSubmitting = false;
}
}),
setValidationPromise: (element, promise) => {
if (promise) {
if (validationPromises.get(element) !== promise) {
validationPromises.set(element, promise);
isValidating.set(element, true);
void promise.finally(() => {
if (validationPromises.get(element) === promise) {
isValidating.set(element, false);
}
});
}
} else {
validationPromises.delete(element);
isValidating.delete(element);
}
}
});
watchEffect(() => {
data.isValidating = [...isValidating.values()].some((v) => v);
});
useDomEventListener(formRef, "submit", (e) => {
e.preventDefault();
void data.submit();
});
allForms.set(formRef, data);
onScopeDispose(() => {
allForms.delete(formRef);
});
return readonly(data);
}
export function getValidatedForm(form: HTMLFormElement): Readonly<ValidatedFormData> | undefined {
for (const [formRef, formData] of allForms) {
if (formRef.value === form) {
return formData;
}
}
}
</script>
<script setup lang="ts">
const props = defineProps<{
action?: string;
target?: string;
noValidate?: boolean;
formValidationError?: string | undefined;
}>();
const emit = defineEmits<{
submit: [event: CustomSubmitEvent];
}>();
const formRef = ref<HTMLFormElement>();
const formData = useValidatedForm(formRef, (event) => {
emit("submit", event);
}, {
noValidate: toRef(() => props.noValidate),
formValidationError: toRef(() => props.formValidationError)
});
const actionWithoutQuery = computed(() => {
if (props.action != null) {
const url = new URL(props.action);
url.search = "";
return url.toString();
}
});
const actionParams = computed(() => {
if (props.action != null) {
const url = new URL(props.action);
return url.searchParams;
}
});
defineExpose({ formData });
</script>
<template>
<form
novalidate
ref="formRef"
:action="actionWithoutQuery ?? 'javascript:'"
:target="props.target"
:class="{ 'fm-was-validated': formData.isTouched }"
>
<template v-for="[key, value] in actionParams?.entries()" :key="key">
<input type="hidden" :name="key" :value="value" />
</template>
<slot :formData="formData"/>
</form>
</template>