Templates
Templates define the rendering of forms, inputs, and layouts.
They do not change the structure of your data. They do not change your business validation logic either. Their only role is to turn the system props and slots provided by the library into a Vue interface.
In other words:
FormFieldinstances describe the structure;- layouts compose that structure;
- templates render that structure.
What a template is for
A template is used to choose:
- where to display a label;
- where to display an error message;
- how to position an input or subform slot;
- which classes or visual variants to apply;
- how to wrap a
form,input,multi,repeat,section,step,union, orcheckblock.
The important point is this: a template does not know your business schema. It only receives system props and slots, then returns a VNode.
The createTemplate contract
The createTemplate(...) function builds a template factory.
Its role is to:
- bind a template type such as
inputorform; - associate that type with a Vue component;
- inject default props;
- then merge local overrides at usage time.
Internally, the library also automatically adds:
fieldKey;class;- a
DFV-template_<type>class; - a
DFV-deep_<fieldKey>class.
These classes are useful if you want to target a specific rendering precisely without changing the form structure.
Create a custom input template
An input template is a Vue component receiving the system props of an input and the input slot.
<script setup lang="ts">
import { type InputTemplateProperties } from "@duplojs/form/vue";
type Props = (
& InputTemplateProperties["props"]
& {
tone?: "default" | "accent";
}
);
const props = withDefaults(
defineProps<Props>(),
{
tone: "default",
},
);
defineSlots<InputTemplateProperties["slots"]>();
</script>
<template>
<div
class="hero-input-template"
:data-tone="props.tone"
>
<div class="hero-input-template__head">
<small class="hero-input-template__key">
{{ props.fieldKey }}
</small>
<label
v-if="props.getLabel"
:for="props.fieldKey"
class="hero-input-template__label"
>
{{ props.getLabel() }}
</label>
</div>
<div class="hero-input-template__body">
<slot name="input" />
</div>
<small
v-if="props.getErrorMessage"
class="hero-input-template__error"
>
{{ props.getErrorMessage() }}
</small>
</div>
</template>
<style scoped>
.hero-input-template {
grid-column: span 6;
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 0.9rem;
border: 1px solid #d4d4d8;
border-radius: 16px;
background: #fafaf9;
}
.hero-input-template[data-tone="accent"] {
border-color: #0f766e;
background: linear-gradient(180deg, #f0fdfa 0%, #ffffff 100%);
}
.hero-input-template__head {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.hero-input-template__key {
color: #78716c;
font-size: 0.7rem;
}
.hero-input-template__label {
font-weight: 600;
}
.hero-input-template__error {
min-height: 1rem;
color: #dc2626;
font-size: 0.75rem;
}
</style>In this example:
getLabel()lets you display the label;getErrorMessage()lets you display the current error;fieldKeyremains available for attributes or CSS hooks;- the
inputslot contains the actual input component.
Then you turn that component into a factory with createTemplate(...).
import { createTemplate } from "@duplojs/form/vue";
import HeroInputTemplate from "./HeroInputTemplate.vue";
export const useHeroInputTemplate = createTemplate(
"input",
HeroInputTemplate,
{
props: {
tone: "default",
},
},
);Here, useHeroInputTemplate(...) is a template factory. It can be used globally or locally.
Global template override
The simplest way to change the rendering of a given type everywhere is to replace that template in the object passed to createForm(...).
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";
import { useHeroInputTemplate } from "./customInputTemplate";
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(),
input: useHeroInputTemplate({ tone: "accent" }),
});In this example:
createGridTemplates(...)provides a complete base;templatesGrid.useTemplates()returns the full set of ready-to-use templates;- the
inputkey is replaced byuseHeroInputTemplate({ tone: "accent" }).
As a result, every input using that useForm will use this template by default.
Local template override
You can also change the template of a single field without touching the rest of the form.
import { useMultiLayout } from "@duplojs/form/vue";
import {
useTextInput,
useTextareaInput,
} from "@duplojs/form/vueDesignSystem";
import { useHeroInputTemplate } from "./customInputTemplate";
import { templatesGrid, useForm } from "./init";
export function useFormWithCustomTemplate() {
const { component, check, currentValue, reset, dispose } = useForm(
useMultiLayout({
title: useTextInput({
label: "Title",
}),
subtitle: useTextInput({
label: "Subtitle",
template: useHeroInputTemplate({ tone: "default" }),
}),
summary: useTextareaInput({
label: "Summary",
template: templatesGrid.useInputTemplate({
columns: 12,
}),
}),
}),
);
return {
FormWithCustomTemplate: component,
checkFormWithCustomTemplate: check,
currentFormWithCustomTemplateValue: currentValue,
resetFormWithCustomTemplate: reset,
disposeFormWithCustomTemplate: dispose,
};
}In this example:
titleuses the global input template defined ininit.ts;subtitlelocally overrides that rendering withuseHeroInputTemplate({ tone: "default" });summaryexplicitly switches back to the standard grid template withtemplatesGrid.useInputTemplate(...).
This clearly shows that templates are interchangeable at multiple levels:
- globally, during initialization;
- locally, on a specific field or layout.
Vue implementation
<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 { useFormWithCustomTemplate } from "./templateForm";
const {
FormWithCustomTemplate,
checkFormWithCustomTemplate,
currentFormWithCustomTemplateValue,
resetFormWithCustomTemplate,
disposeFormWithCustomTemplate,
} = useFormWithCustomTemplate();
const submitResult = ref<unknown>(null);
function submit() {
const result = checkFormWithCustomTemplate();
if (EE.isRight(result)) {
submitResult.value = unwrap(result);
}
}
function reset() {
resetFormWithCustomTemplate();
submitResult.value = null;
}
onBeforeUnmount(disposeFormWithCustomTemplate);
</script>
<template>
<FormWithCustomTemplate @submit="submit">
<div style="display: flex; gap: 8px; margin-top: 16px;">
<PrimaryButton
type="submit"
label="Submit"
/>
<OutlineButton
type="button"
label="Reset"
@click="reset"
/>
</div>
</FormWithCustomTemplate>
<pre>currentValue: {{ currentFormWithCustomTemplateValue }}</pre>
<pre>checkedValue: {{ submitResult }}</pre>
</template>Result
currentValue: {
"title": "",
"subtitle": "",
"summary": ""
}checkedValue:
Default templates
The grid templates provided by @duplojs/form/vueGrid cover the following types:
forminputmulticheckrepeatunionstepsection
They are a practical starting point, but they are not special. You can replace them one by one, mix them with your own templates, or reuse them partially.
How to think about templates
A good template should stay generic.
It should:
- display what the library passes to it;
- expose the slots clearly;
- avoid depending on a specific business form;
- remain reusable across multiple forms.
In practice, if your component feels like it needs to know firstName, age, or a specific data structure, you are probably writing form logic, not a template.
What to remember
- a template controls rendering, not structure;
createTemplate(...)builds a template factory;- that factory can be used globally or locally;
- grid templates are only default implementations;
- system props and slots are the real template contract.
