Skip to content

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:

  • FormField instances 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, or check block.

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 input or form;
  • 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.

vue
vue
<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;
  • fieldKey remains available for attributes or CSS hooks;
  • the input slot contains the actual input component.

Then you turn that component into a factory with createTemplate(...).

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

ts
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";
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 input key is replaced by useHeroInputTemplate({ 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.

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

  • title uses the global input template defined in init.ts;
  • subtitle locally overrides that rendering with useHeroInputTemplate({ tone: "default" });
  • summary explicitly switches back to the standard grid template with templatesGrid.useInputTemplate(...).

This clearly shows that templates are interchangeable at multiple levels:

  • globally, during initialization;
  • locally, on a specific field or layout.

Vue implementation

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 { 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

FRM_MUL-title_INP
FRM_MUL-subtitle_INP
currentValue: {
  "title": "",
  "subtitle": "",
  "summary": ""
}
checkedValue: 

Default templates

The grid templates provided by @duplojs/form/vueGrid cover the following types:

  • form
  • input
  • multi
  • check
  • repeat
  • union
  • step
  • section

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.

Released under the MIT License.