Initialize a form
Form initialization happens in two steps:
- you create a
useFormfunction withcreateForm(...); - you pass a root
FormFieldto that function, which can be a simple input or a composed layout.
This is the key point of the library:
- an input returns a
FormField; - a layout also returns a
FormField; - a form is therefore nothing more than the instantiation of a root
FormFieldon Vue state.
In other words, createForm does not know your business schema. It only knows how to take a FormField, clone its defaultValue, instantiate the whole tree, and expose a rendering, validation, and reset API.
Global initialization
The global step is to prepare the templates that will be used by all forms.
import "@duplojs/form/vueGrid.css";
import "@duplojs/form/vueDesignSystem.css";
import { createForm } from "@duplojs/form/vue";
import {
templateFormAddButton,
templateFormNextButton,
templateFormPreviousButton,
templateFormRemoveButton,
templateFormResetButton,
templateFormSelect,
} from "@duplojs/form/vueDesignSystem";
import { createGridTemplates } from "@duplojs/form/vueGrid";
export const templatesGrid = createGridTemplates({
repeat: {
addLabel: "Add item",
removeLabel: "Remove item",
addButton: templateFormAddButton,
removeButton: templateFormRemoveButton,
resetButton: templateFormResetButton,
},
step: {
nextLabel: "Continue",
previousLabel: "Back",
resetButton: templateFormResetButton,
nextButton: templateFormNextButton,
previousButton: templateFormPreviousButton,
},
union: { selectInputKind: templateFormSelect },
});
export const useForm = createForm(templatesGrid.useTemplates());createForm(templatesGrid.useTemplates()) returns an already configured useForm function.
This function, closed over your templates, becomes the single entry point used to initialize all your forms.
INFO
The vueGrid templates and the vueDesignSystem components are only default values. You can replace all or part of the rendering without changing the structure of your forms.
The fundamental principle
When you call useForm(rootField), the library:
- clones
rootField.defaultValueto createcurrentValue; - instantiates the root
FormFieldand all its children; - builds a Vue component ready to render the form template;
- returns
check,reset,dispose, andcurrentValue.
This implies an essential distinction:
currentValuerepresents the raw current state of the form;check()returns the validated and parsed value.
If an input uses a dataParser, the currentValue may still be raw, while the value returned by check() is already transformed and validated.
The root field can be minimal
The root field does not need to be a complex object. A form can be initialized directly from a single input.
import { useTextInput } from "@duplojs/form/vueDesignSystem";
import { useForm } from "./init";
export function useNewsletterForm() {
const { component, check, currentValue, reset, dispose } = useForm(
useTextInput({
label: "Email",
defaultValue: "",
}),
);
return {
NewsletterForm: component,
checkNewsletterForm: check,
currentNewsletterValue: currentValue,
resetNewsletterForm: reset,
disposeNewsletterForm: dispose,
};
}Here, the form value is simply a string, because the root field is a useTextInput(...).
Declare a structured form
As soon as you need several fields, you compose a root FormField with a layout such as useMultiLayout.
import { useMultiLayout, type GetCheckedValue } from "@duplojs/form/vue";
import { useNumberInput, useTextInput } from "@duplojs/form/vueDesignSystem";
import * as DP from "@duplojs/utils/dataParser";
import { useForm } from "./init";
export function useProfileForm() {
const { component, check, reset, currentValue, dispose } = useForm(
useMultiLayout({
firstName: useTextInput({
label: "first name",
defaultValue: "Math",
}),
age: useNumberInput({
label: "Age",
defaultValue: 16,
dataParser: DP.number()
.addChecker(
DP.checkerNumberMin(
18,
{ errorMessage: "You must be at least 18." },
),
),
}),
}),
);
return {
ProfileForm: component,
checkProfileForm: check,
currentProfileValue: currentValue,
resetProfileForm: reset,
disposeProfileForm: dispose,
};
}
export type ProfileFormSubmitValue = GetCheckedValue<
ReturnType<typeof useProfileForm>["checkProfileForm"]
>;In this example:
firstNameandagedefine the shape ofcurrentValue;- the
dataParseronagedefines the shape of the value returned bycheck(); - the
ProfileFormSubmitValuetype can be derived automatically fromcheck.
So the form is not described by a separate schema. Its structure and type come directly from the FormField tree you compose.
Use the form in a Vue component
The component returned by useForm acts as the rendering container. Its default slot matches the submit area.
<script setup lang="ts">
import * as EE from "@duplojs/utils/either";
import { unwrap } from "@duplojs/utils";
import { OutlineButton, PrimaryButton } from "@duplojs/form/vueDesignSystem";
import { onBeforeUnmount, ref } from "vue";
import {
type ProfileFormSubmitValue,
useProfileForm,
} from "./profileForm";
const {
ProfileForm,
checkProfileForm,
currentProfileValue,
resetProfileForm,
disposeProfileForm,
} = useProfileForm();
const submitResult = ref<ProfileFormSubmitValue | null>(null);
function submit() {
const result = checkProfileForm();
if (EE.isRight(result)) {
submitResult.value = unwrap(result);
}
}
function reset() {
resetProfileForm();
submitResult.value = null;
}
onBeforeUnmount(disposeProfileForm);
</script>
<template>
<ProfileForm @submit="submit">
<PrimaryButton
type="submit"
label="Submit"
/>
<OutlineButton
type="button"
label="Reset"
@click="reset"
/>
</ProfileForm>
<pre>currentValue: {{ currentProfileValue }}</pre>
<pre>checkedValue: {{ submitResult }}</pre>
</template>In this implementation:
@submitexplicitly triggerscheckProfileForm();currentProfileValuelets you observe the current state live;resetProfileForm()restores the default values;disposeProfileForm()is called on unmount to release internal scopes.
Result
currentValue: {
"firstName": "Math",
"age": 16
}checkedValue:
API returned by useForm
useForm(...) always returns an object with the following properties:
component: the Vue component to render.currentValue: aRefholding the raw current value.check(): validates the whole tree and returns either errors or the output value.reset(): restores the form from thedefaultValuevalues.dispose(): destroys the internal effects tied to the form instance.
What to remember
- the form is initialized from a single root
FormField; - layouts are only used to compose that root
FormField; currentValueshows the live state of the form;check()produces the validated business value;- templates define the rendering, not the structure.
