Skip to content

Créer un input ​

Un input dans @duplojs/form n'est pas directement un composant Vue.

La séquence exacte est la suivante :

  1. vous écrivez un composant Vue compatible avec le contrat des inputs ;
  2. vous le transformez en factory avec createInput(...) ;
  3. vous appelez cette factory pour obtenir un FormField ;
  4. ce FormField est 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 un FormField, 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(...).

vue
vue
<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 dataParser ou via defineExpose.

Transformer le composant avec createInput ​

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

ts
ts
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 ;
  • dataParser vé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.

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

ts
ts
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 defaultValue effective ;
  • monte le composant Vue avec modelValue et update:modelValue ;
  • applique le template d'input ;
  • appelle le check() exposé par le composant s'il existe ;
  • applique ensuite dataParser s'il est fourni ;
  • gère un message d'erreur réactif pour le template ;
  • expose enfin un FormField.

Le point important est l'ordre :

  1. validation locale du composant, si elle existe ;
  2. validation/transformation par dataParser, si elle existe.

Utiliser les inputs dans un formulaire ​

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

  • email est un input simple validé par dataParser ;
  • terms est un input qui expose sa propre validation via defineExpose ;
  • le formulaire compose les deux sans différence, parce qu'ils retournent tous les deux un FormField.

Implémentation ​

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 {
	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(...):

  • defaultValue est obligatoire.
  • props sert à définir les props par défaut du composant.
  • template sert à changer le rendu par défaut de l'input.

Pour useMyInput(...):

  • label alimente le template.
  • defaultValue surcharge la valeur initiale.
  • props configure l'instance locale du composant.
  • dataParser valide ou transforme la valeur.
  • class ajoute une classe au conteneur template.
  • template remplace 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 ;
  • dataParser sert à la validation métier ;
  • defineExpose sert à la validation comportementale propre au composant ;
  • les deux mécanismes peuvent se cumuler.

Released under the MIT License.