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.