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
FormFieldinstances; - 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
useMultiLayoutcomposes several fields under known keys.useRepeatLayoutrepeats a field or subform in an array.useUnionLayoutlets you choose one active variant among several branches.useStepLayoutsplits a form into several successive steps.useSectionLayoutwraps a block in a titled section.useSlotLayoutdelegates part of the rendering to a Vue slot.useCheckLayoutadds extra validation around an existing field.useDisabledLayouthides 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.
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:
useSectionLayoutwraps the whole form in aProfilesection;useMultiLayoutstructures the data into blocks;useRepeatLayoutmakescontactsdynamic;useUnionLayoutswitches the active field betweenemailandphone;useSlotLayoutgives you full control over the rendering ofmessage.
This model is useful for advanced forms: you keep a predictable architecture, but each block can become adaptive.
Vue implementation
<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
Control layouts
Not all layouts are only about structure. Some are mostly there to control form behavior.
The following example shows three cases:
useStepLayoutenforces a step-by-step flow;useCheckLayoutadds extra validation on an existing field;useDisabledLayoutneutralizes a field until an external condition is met.
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:
emailremains a simple text input, butuseCheckLayoutadds a local business rule;companyremains part of the form structure, butuseDisabledLayouthides it and removes it from the flow whilehasCompanyisfalse;useStepLayoutturns two blocks into a sequential flow with step control.
How to choose a layout
- Use
useMultiLayoutwhen the final object shape is known in advance. - Use
useRepeatLayoutwhen you have a list of homogeneous items. - Use
useUnionLayoutwhen only one branch must be active at a time. - Use
useStepLayoutwhen input order is part of the experience. - Use
useSectionLayoutwhen you want to clarify a block visually. - Use
useSlotLayoutwhen the standard rendering is no longer enough. - Use
useCheckLayoutwhen validation depends on extra rules around an existing field. - Use
useDisabledLayoutwhen 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
FormFieldtree.
