Skip to content

Layouts ​

A layout is used to compose FormField instances in order to build a richer form.

An input already creates a FormField, but a layout also creates a FormField. The difference is:

  • an input handles a single unit of input;
  • a layout organizes, transforms, or conditions one or more FormField instances;
  • the final form is always driven by a single root FormField.

In other words, a layout is a composition building block. It lets you describe the structure of a form without leaving the main model of the library.

What a layout adds ​

A layout can be used to:

  • group several fields into an object;
  • repeat the same field or block;
  • switch between several subforms;
  • split a form into steps;
  • inject custom rendering with a slot;
  • wrap a block with a title or presentation;
  • add cross-field validation;
  • conditionally disable part of the form.

The key idea is this: the structure of the form stays readable and stable in code, while the behavior can become highly dynamic.

Built-in layouts ​

  • useMultiLayout composes several fields under known keys.
  • useRepeatLayout repeats a field or subform in an array.
  • useUnionLayout lets you choose one active variant among several branches.
  • useStepLayout splits a form into several successive steps.
  • useSectionLayout wraps a block in a titled section.
  • useSlotLayout delegates part of the rendering to a Vue slot.
  • useCheckLayout adds extra validation around an existing field.
  • useDisabledLayout hides and neutralizes a field conditionally.

For detailed reference on each layout, see also the layout API index.

Fixed structure, dynamic behavior ​

useMultiLayout is often the most useful entry point because it lets you define a stable structure with explicit keys.

In the following example, the keys identity, contacts, preferredChannel, and message always exist. Then each key can contain either a simple input or another layout.

ts
ts
import {
	
useMultiLayout
,
useRepeatLayout
,
useSectionLayout
,
useSlotLayout
,
useUnionLayout
,
} from "@duplojs/form/vue"; import {
useForm
} from "./init";
import {
useTextInput
,
useTextareaInput
,
} from "@duplojs/form/vueDesignSystem"; export function
useProfileLayoutForm
() {
const {
component
,
check
} =
useForm
(
useSectionLayout
(
useMultiLayout
({
identity
:
useMultiLayout
({
firstName
:
useTextInput
({
label
: "First Name" }),
lastName
:
useTextInput
({
label
: "Last Name" }),
}),
contacts
:
useRepeatLayout
(
useTextInput
({
label
: "Email" }),
{
min
: 1,
max
: 3,
}, ),
preferredChannel
:
useUnionLayout
(
[ ["email",
useTextInput
({
label
: "Email" })],
["phone",
useTextInput
({
label
: "Phone" })],
], {
defaultKind
: "email" },
),
message
:
useSlotLayout
(
"customMessage",
useTextareaInput
({
label
: "Message" }),
), }), {
title
: "Profile" },
), ); return {
TheForm
:
component
,
checkForm
:
check
,
}; }

In this same example:

  • useSectionLayout wraps the whole form in a Profile section;
  • useMultiLayout structures the data into blocks;
  • useRepeatLayout makes contacts dynamic;
  • useUnionLayout switches the active field between email and phone;
  • useSlotLayout gives you full control over the rendering of message.

This model is useful for advanced forms: you keep a predictable architecture, but each block can become adaptive.

Vue implementation ​

vue
vue
<script setup lang="ts">
import * as EE from "@duplojs/utils/either";
import { PrimaryButton } from "@duplojs/form/vueDesignSystem";
import { useProfileLayoutForm } from "./profileLayoutForm";

const { TheForm, checkForm } = useProfileLayoutForm();

function onSubmit() {
	void EE.whenIsRight(
		checkForm(),
		(result) => {
			alert(JSON.stringify(result));
		},
	);
}
</script>

<template>
	<TheForm @submit="onSubmit">
		<PrimaryButton
			type="submit"
			label="Submit"
		/>

		<template #customMessage="{ formField, value, update }">
			<div class="DFV-grid-element">
				<p>Custom render with slot</p>

				<button
					type="button"
					@click="update('')"
				>
					reset
				</button>

				{{ value }}

				<component :is="formField" />
			</div>
		</template>
	</TheForm>
</template>

The #customMessage slot clearly shows the role of useSlotLayout:

  • the current value is available in value;
  • you can update it with update(...);
  • you can re-inject the original field through formField.

Result ​

Custom render with slot

Control layouts ​

Not all layouts are only about structure. Some are mostly there to control form behavior.

The following example shows three cases:

  • useStepLayout enforces a step-by-step flow;
  • useCheckLayout adds extra validation on an existing field;
  • useDisabledLayout neutralizes a field until an external condition is met.
ts
ts
import * as 
EE
from "@duplojs/utils/either";
import {
ref
} from "vue";
import {
useCheckLayout
,
useDisabledLayout
,
useMultiLayout
,
useStepLayout
,
} from "@duplojs/form/vue"; import {
useTextInput
,
useTextareaInput
,
} from "@duplojs/form/vueDesignSystem"; import {
useForm
} from "./init";
export function
useFlowLayoutForm
() {
const
hasCompany
=
ref
(false);
const {
component
,
check
,
currentValue
} =
useForm
(
useStepLayout
(
[
useMultiLayout
({
fullName
:
useTextInput
({
label
: "Full name" }),
email
:
useCheckLayout
(
useTextInput
({
label
: "Email" }),
{
refine
: (
value
) =>
value
.
includes
("@")
?
EE
.
ok
()
:
EE
.
error
("Email must contain @."),
}, ), }),
useMultiLayout
({
company
:
useDisabledLayout
(
useTextInput
({
label
: "Company" }),
{
isDisabled
: () => !
hasCompany
.
value
,
}, ),
notes
:
useTextareaInput
({
label
: "Notes" }),
}), ], {
errorMessageNotAtLastStep
: "Complete all steps before submitting.",
}, ), ); return {
FlowForm
:
component
,
checkFlowForm
:
check
,
currentFlowValue
:
currentValue
,
hasCompany
,
}; }

Here:

  • email remains a simple text input, but useCheckLayout adds a local business rule;
  • company remains part of the form structure, but useDisabledLayout hides it and removes it from the flow while hasCompany is false;
  • useStepLayout turns two blocks into a sequential flow with step control.

How to choose a layout ​

  • Use useMultiLayout when the final object shape is known in advance.
  • Use useRepeatLayout when you have a list of homogeneous items.
  • Use useUnionLayout when only one branch must be active at a time.
  • Use useStepLayout when input order is part of the experience.
  • Use useSectionLayout when you want to clarify a block visually.
  • Use useSlotLayout when the standard rendering is no longer enough.
  • Use useCheckLayout when validation depends on extra rules around an existing field.
  • Use useDisabledLayout when a field must temporarily disappear from the form.

What to remember ​

  • a layout also returns a FormField;
  • layouts can therefore be nested freely;
  • they are used both to structure data and to drive behavior;
  • the final form is always defined by a single FormField tree.

Released under the MIT License.