First shot on form validation.

This commit is contained in:
baldo 2022-08-23 15:38:10 +02:00
parent 096b792caa
commit 4ed749eee3
3 changed files with 151 additions and 0 deletions

View file

@ -58,6 +58,7 @@ h2 {
font-size: $h2-font-size; font-size: $h2-font-size;
margin: $h2-margin; margin: $h2-margin;
} }
input { input {
padding: $input-padding; padding: $input-padding;
border: none; border: none;

View file

@ -0,0 +1,45 @@
<script setup lang="ts">
import {type ComponentInternalInstance, ref, type Slot} from "vue";
const emit = defineEmits<{
(e: "submit"): void
}>();
const validationComponents = ref<ComponentInternalInstance[]>([]);
defineExpose({
registerValidationComponent(component: ComponentInternalInstance): void {
validationComponents.value.push(component);
}
});
function validate(): boolean {
let valid = true;
for (const component of validationComponents.value) {
if (!(component.exposed?.validate)) {
throw new Error(`Component has no exposed validate() method: ${component.type.__name}`);
}
valid = component.exposed.validate() && valid;
}
return valid;
}
function onSubmit() {
const valid = validate();
if (valid) {
emit("submit");
}
// TODO: Else scroll to first error and focus input.
}
</script>
<template>
<form @submit.prevent="onSubmit">
<slot/>
</form>
</template>
<style lang="scss">
</style>

View file

@ -0,0 +1,105 @@
<script setup lang="ts">
import {getCurrentInstance, onMounted, ref} from "vue";
import {type Constraint, forConstraint} from "@/shared/validation/validator";
interface Props {
modelValue?: string;
label: string;
type?: string;
placeholder: string;
constraint: Constraint;
validationError: string;
}
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
}>();
const input = ref<HTMLInputElement>();
const valid = ref(true);
const validated = ref(false);
function registerValidationComponent() {
const instance = getCurrentInstance();
let parent = instance?.parent;
while (parent) {
if (parent.exposed?.registerValidationComponent) {
parent.exposed.registerValidationComponent(instance);
return;
}
parent = parent.parent;
}
throw new Error("Could not find matching ValidationForm for ValidationFormInpunt.");
}
function onInput() {
if (validated.value) {
validate();
}
const element = input.value;
if (!element) {
console.warn("Could not get referenced input element.")
return;
}
emit("update:modelValue", element.value);
}
function validate(): boolean {
const element = input.value;
if (!element) {
console.warn("Could not get referenced input element.")
return false;
}
valid.value = forConstraint(props.constraint, false)(element.value);
validated.value = true;
return valid.value;
}
defineExpose({
validate
});
onMounted(() => {
registerValidationComponent();
});
</script>
<template>
<div>
<label>
{{label}}:
<input
ref="input"
:value="modelValue"
@input="onInput"
:type="type || 'text'"
:placeholder="placeholder"
/>
</label>
<div class="validation-error" v-if="!valid">
{{validationError}}
</div>
</div>
</template>
<style scoped lang="scss">
@import "../../scss/variables";
label {
display: block;
font-weight: $label-font-weight;
cursor: pointer;
}
input {
box-sizing: border-box;
width: 100%;
margin: 0.25em 0;
}
.validation-error {
color: $variant-color-danger;
margin: 0.25em 0;
}
</style>