Skip to content

Initialize a form

Form initialization happens in two steps:

  1. you create a useForm function with createForm(...);
  2. you pass a root FormField to 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 FormField on 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.

ts
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.defaultValue to create currentValue;
  • instantiates the root FormField and all its children;
  • builds a Vue component ready to render the form template;
  • returns check, reset, dispose, and currentValue.

This implies an essential distinction:

  • currentValue represents 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.

ts
ts
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.

ts
ts
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:

  • firstName and age define the shape of currentValue;
  • the dataParser on age defines the shape of the value returned by check();
  • the ProfileFormSubmitValue type can be derived automatically from check.

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.

vue
vue
<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:

  • @submit explicitly triggers checkProfileForm();
  • currentProfileValue lets 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: a Ref holding the raw current value.
  • check(): validates the whole tree and returns either errors or the output value.
  • reset(): restores the form from the defaultValue values.
  • 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;
  • currentValue shows the live state of the form;
  • check() produces the validated business value;
  • templates define the rendering, not the structure.

Released under the MIT License.