Skip to content

Les templates

Les templates définissent le rendu des formulaires, des inputs et des layouts.

Ils ne changent pas la structure de vos données. Ils ne changent pas non plus la logique métier de validation. Leur rôle consiste uniquement à transformer les propriétés système et les slots fournis par la librairie en interface Vue.

Autrement dit :

  • les FormField décrivent la structure ;
  • les layouts composent cette structure ;
  • les templates rendent cette structure.

À quoi sert un template

Un template sert à choisir :

  • où afficher un label ;
  • où afficher un message d'erreur ;
  • comment disposer un slot d'input ou de sous-formulaire ;
  • quelles classes ou variantes visuelles appliquer ;
  • comment encapsuler un bloc form, input, multi, repeat, section, step, union ou check.

Le point important est le suivant : un template ne connaît pas votre schéma métier. Il reçoit simplement des props système et des slots, puis retourne un VNode.

Le contrat de createTemplate

La fonction createTemplate(...) fabrique une factory de templates.

Son rôle est de :

  • lier un type de template, comme input ou form ;
  • associer ce type à un composant Vue ;
  • injecter des props par défaut ;
  • fusionner ensuite les surcharges locales au moment de l'utilisation.

En interne, la librairie ajoute aussi automatiquement :

  • fieldKey ;
  • class ;
  • une classe DFV-template_<type> ;
  • une classe DFV-deep_<fieldKey>.

Ces classes sont utiles si vous voulez cibler finement un rendu sans modifier la structure du formulaire.

Créer un template d'input personnalisé

Un template d'input est un composant Vue qui reçoit les props système d'un input et le slot input.

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>

Dans cet exemple :

  • getLabel() permet d'afficher le libellé ;
  • getErrorMessage() permet d'afficher l'erreur courante ;
  • fieldKey reste disponible pour les attributs ou les hooks CSS ;
  • le slot input contient le vrai composant de saisie.

Ensuite, vous transformez ce composant en factory avec createTemplate(...).

ts
ts
import { createTemplate } from "@duplojs/form/vue";
import HeroInputTemplate from "./HeroInputTemplate.vue";

export const useHeroInputTemplate = createTemplate(
	"input",
	HeroInputTemplate,
	{
		props: {
			tone: "default",
		},
	},
);

Ici, useHeroInputTemplate(...) est une factory de template. Elle pourra être utilisée globalement ou localement.

Surcharge globale d'un template

La façon la plus simple de changer tout le rendu d'un type donné consiste à remplacer ce template dans l'objet passé à 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" }),
});

Dans cet exemple :

  • createGridTemplates(...) fournit une base complète ;
  • templatesGrid.useTemplates() retourne l'ensemble des templates prêts à l'emploi ;
  • la clé input est remplacée par useHeroInputTemplate({ tone: "accent" }).

Conséquence : tous les inputs de ce useForm utiliseront ce template par défaut.

Surcharge locale d'un template

Vous pouvez aussi changer le template d'un seul champ, sans toucher au reste du formulaire.

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,
	};
}

Dans cet exemple :

  • title utilise le template d'input global défini dans init.ts ;
  • subtitle surcharge localement ce rendu avec useHeroInputTemplate({ tone: "default" }) ;
  • summary revient explicitement sur le template grid standard avec templatesGrid.useInputTemplate(...).

Cela montre bien que les templates sont interchangeables à plusieurs niveaux :

  • globalement, lors de l'initialisation ;
  • localement, sur un field ou un layout précis.

Implémentation Vue

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>

Résultat

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

Les templates fournis par défaut

Les templates grid fournis par @duplojs/form/vueGrid couvrent les types suivants :

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

Ils servent de base pratique, mais ne sont pas spéciaux. Vous pouvez les remplacer un par un, les mélanger avec vos propres templates, ou les réutiliser partiellement.

Comment penser les templates

Un bon template doit rester générique.

Il doit :

  • afficher ce que la librairie lui transmet ;
  • exposer clairement les slots ;
  • éviter de dépendre d'un formulaire métier précis ;
  • rester réutilisable pour plusieurs formulaires.

En pratique, si vous sentez que votre composant doit connaître firstName, age ou une structure de données spécifique, vous êtes probablement en train d'écrire de la logique de formulaire, pas un template.

Ce qu'il faut retenir

  • un template contrôle le rendu, pas la structure ;
  • createTemplate(...) fabrique une factory de templates ;
  • cette factory peut être utilisée globalement ou localement ;
  • les templates grid sont seulement des implémentations par défaut ;
  • les props système et les slots sont le vrai contrat d'un template.

Released under the MIT License.