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
FormFielddé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,unionoucheck.
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
inputouform; - 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.
<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 ;fieldKeyreste disponible pour les attributs ou les hooks CSS ;- le slot
inputcontient le vrai composant de saisie.
Ensuite, vous transformez ce composant en factory avec createTemplate(...).
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(...).
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é
inputest remplacée paruseHeroInputTemplate({ 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.
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 :
titleutilise le template d'input global défini dansinit.ts;subtitlesurcharge localement ce rendu avecuseHeroInputTemplate({ tone: "default" });summaryrevient explicitement sur le template grid standard avectemplatesGrid.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
<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
currentValue: {
"title": "",
"subtitle": "",
"summary": ""
}checkedValue:
Les templates fournis par défaut
Les templates grid fournis par @duplojs/form/vueGrid couvrent les types suivants :
forminputmulticheckrepeatunionstepsection
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.
