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.