First shot on form validation.
This commit is contained in:
parent
096b792caa
commit
4ed749eee3
|
@ -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;
|
||||||
|
|
45
frontend/src/components/form/ValidationForm.vue
Normal file
45
frontend/src/components/form/ValidationForm.vue
Normal 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>
|
105
frontend/src/components/form/ValidationFormInput.vue
Normal file
105
frontend/src/components/form/ValidationFormInput.vue
Normal 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>
|
Loading…
Reference in a new issue