Créer un input
Un input dans @duplojs/form n'est pas directement un composant Vue.
La séquence exacte est la suivante :
- vous écrivez un composant Vue compatible avec le contrat des inputs ;
- vous le transformez en factory avec
createInput(...); - vous appelez cette factory pour obtenir un
FormField; - ce
FormFieldest ensuite composé dans un formulaire.
Cette séparation est importante :
- le composant Vue gère l'interface et, si besoin, une validation locale ;
createInput(...)fabrique une factory réutilisable ;useMyInput(...)retourne unFormField, donc une brique de formulaire.
Le contrat minimal d'un composant d'input
Un composant compatible doit au minimum :
- accepter
modelValue; - émettre
update:modelValue; - laisser la librairie lui injecter un
id.
Dans la pratique, avec Vue, le plus simple est d'utiliser defineModel(...).
<script setup lang="ts">
export interface Props {
placeholder?: string;
}
const props = defineProps<Props>();
const model = defineModel<string>({ required: true });
</script>
<template>
<input
v-model="model"
type="text"
:placeholder="props.placeholder"
/>
</template>Ce composant est volontairement minimal :
- il ne valide rien lui-même ;
- il se contente de relayer la valeur ;
- la validation pourra être ajoutée plus tard via
dataParserou viadefineExpose.
Transformer le composant avec createInput
import { createInput } from "@duplojs/form/vue";
import BasicTextInput from "./BasicTextInput.vue";
export const useBasicTextInput = createInput(
BasicTextInput,
{
defaultValue: "",
},
);createInput(component, defaultParams) retourne une fonction du type useBasicTextInput(...).
Les paramètres importants de defaultParams sont :
defaultValue: valeur par défaut de l'input. C'est obligatoire.props: props par défaut injectées dans le composant.template: template d'input par défaut à utiliser pour le rendu.
defaultValue peut être une valeur directe ou une fonction. Utilisez une fonction dès que la valeur est dynamique ou doit être recréée proprement.
Instancier un input
Une fois la factory créée, vous l'appelez pour produire un FormField.
import { DP } from "@duplojs/utils";
import { useBasicTextInput } from "./basicTextInput";
export const emailField = useBasicTextInput({
label: "Email",
props: {
placeholder: "[email protected]",
},
dataParser: DP.string()
.addChecker(
DP.checkerStringMin(
5,
{ errorMessage: "Email is too short." },
),
),
});Les paramètres importants de useBasicTextInput(...) sont :
label: libellé passé au template d'input.defaultValue: surcharge locale de la valeur par défaut.props: props passées au composant.dataParser: validation et transformation de la valeur.class: classe CSS ajoutée au template de l'input.template: surcharge locale du template de rendu.
Où se fait la validation
Il y a deux niveaux possibles de validation.
Validation externe avec dataParser
Si votre composant remonte déjà la bonne forme de valeur, laissez dataParser faire la validation métier.
Exemple courant :
- un composant texte remonte une
string; dataParservérifie la longueur, le format, puis retourne la valeur validée.
Quand dataParser échoue :
check()retourne une erreur ;- le message de la première issue est exposé au template via
getErrorMessage().
Validation interne avec defineExpose
Si le composant doit contrôler lui-même sa validité, il peut exposer check, reset et dispose.
<script setup lang="ts">
import * as EE from "@duplojs/utils/either";
import { type ExposeInputProperties } from "@duplojs/form/vue";
import { ref } from "vue";
export interface Props {
id: string;
label: string;
required?: boolean;
errorMessage?: string;
}
const props = withDefaults(
defineProps<Props>(),
{
required: false,
errorMessage: "You must accept this condition.",
},
);
const model = defineModel<boolean>({ default: false });
const currentError = ref<string | null>(null);
defineExpose<ExposeInputProperties>({
check: () => {
if (!props.required || model.value) {
currentError.value = null;
return EE.success(model.value);
}
currentError.value = props.errorMessage;
return EE.error([{ key: props.id }]);
},
reset: () => {
currentError.value = null;
},
});
</script>
<template>
<div>
<label>
<input
v-model="model"
type="checkbox"
:id="props.id"
/>
{{ props.label }}
</label>
<small v-if="currentError">
{{ currentError }}
</small>
</div>
</template>Ici, le composant :
- garde son propre message d'erreur local ;
- bloque
check()tant que la case obligatoire n'est pas cochée ; - nettoie son état d'erreur dans
reset().
Puis la factory d'input reste très simple :
import { createInput } from "@duplojs/form/vue";
import PolicyCheckbox from "./PolicyCheckbox.vue";
export const usePolicyCheckbox = createInput(
PolicyCheckbox,
{
defaultValue: false,
props: {
label: "Accept terms",
},
},
);Ce que fait vraiment createInput
Quand un input est instancié, la librairie :
- détermine la
defaultValueeffective ; - monte le composant Vue avec
modelValueetupdate:modelValue; - applique le template d'input ;
- appelle le
check()exposé par le composant s'il existe ; - applique ensuite
dataParsers'il est fourni ; - gère un message d'erreur réactif pour le template ;
- expose enfin un
FormField.
Le point important est l'ordre :
- validation locale du composant, si elle existe ;
- validation/transformation par
dataParser, si elle existe.
Utiliser les inputs dans un formulaire
import { useMultiLayout, type GetCheckedValue } from "@duplojs/form/vue";
import * as DP from "@duplojs/utils/dataParser";
import { useForm } from "./init";
import { useBasicTextInput } from "./basicTextInput";
import { usePolicyCheckbox } from "./policyCheckbox";
export function useContactForm() {
const { component, check, reset, currentValue, dispose } = useForm(
useMultiLayout({
email: useBasicTextInput({
label: "Email",
props: {
placeholder: "[email protected]",
},
dataParser: DP.string()
.addChecker(
DP.checkerStringMin(
5,
{ errorMessage: "Email is too short." },
),
),
}),
terms: usePolicyCheckbox({
props: {
label: "I accept the terms of use",
required: true,
},
}),
}),
);
return {
ContactForm: component,
checkContactForm: check,
currentContactValue: currentValue,
resetContactForm: reset,
disposeContactForm: dispose,
};
}
export type ContactFormSubmitValue = GetCheckedValue<
ReturnType<typeof useContactForm>["checkContactForm"]
>;Dans cet exemple :
emailest un input simple validé pardataParser;termsest un input qui expose sa propre validation viadefineExpose;- le formulaire compose les deux sans différence, parce qu'ils retournent tous les deux un
FormField.
Implémentation
<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 {
type ContactFormSubmitValue,
useContactForm,
} from "./contactForm";
const {
ContactForm,
checkContactForm,
currentContactValue,
resetContactForm,
disposeContactForm,
} = useContactForm();
const submitResult = ref<ContactFormSubmitValue | null>(null);
function submit() {
const result = checkContactForm();
if (EE.isRight(result)) {
submitResult.value = unwrap(result);
}
}
function reset() {
resetContactForm();
submitResult.value = null;
}
onBeforeUnmount(disposeContactForm);
</script>
<template>
<ContactForm @submit="submit">
<div style="display: flex; gap: 8px; margin-top: 16px;">
<PrimaryButton
type="submit"
label="Submit"
/>
<OutlineButton
type="button"
label="Reset"
@click="reset"
/>
</div>
</ContactForm>
<pre>currentValue: {{ currentContactValue }}</pre>
<pre>checkedValue: {{ submitResult }}</pre>
</template>Résultat
currentValue: {
"email": "",
"terms": false
}checkedValue:
Paramètres à retenir
Pour createInput(...):
defaultValueest obligatoire.propssert à définir les props par défaut du composant.templatesert à changer le rendu par défaut de l'input.
Pour useMyInput(...):
labelalimente le template.defaultValuesurcharge la valeur initiale.propsconfigure l'instance locale du composant.dataParservalide ou transforme la valeur.classajoute une classe au conteneur template.templateremplace localement le template d'input.
Ce qu'il faut retenir
- un composant Vue d'input n'est pas encore un
FormField; createInput(...)fabrique une factory ;- appeler cette factory retourne un
FormField; dataParsersert à la validation métier ;defineExposesert à la validation comportementale propre au composant ;- les deux mécanismes peuvent se cumuler.
