Moodinary/UI

Field + form con validación

Todo formulario. Validá al salir del campo y al enviar; el error dice cómo arreglarlo.

Composición de label, control, ayuda y error. Field organiza el espacio: la validación y el envío quedan a cargo del formulario.

Instalación

shadcn
npx shadcn@latest add https://design.moodinary.com/r/field.json
import
import { Field, FieldLabel, FieldHint, FieldError, FieldLegend } from "@moodinary/ui/components/field";

Ejemplos

Meta semanal

Replica WeeklyTargetForm: ayuda y error asociados al control, validación al enviar y foco en el campo inválido. La confirmación del ejemplo es local, no guarda una meta real.

Meta semanal

Cuántos pulsos esperás por semana. Inicio la usa para mostrar el avance; dejala vacía para no tener meta.

Entero entre 1 y 1.000.000.

Código
default.tsx
"use client";

import { Button } from "@moodinary/ui/components/button";
import {
	Field,
	FieldError,
	FieldHint,
	FieldLabel,
} from "@moodinary/ui/components/field";
import { Input } from "@moodinary/ui/components/input";
import { useCallback, useState } from "react";

const DIGITS = /^\d+$/;

export default function FieldDefault() {
	const [value, setValue] = useState("");
	const [error, setError] = useState<string | null>(null);
	const [saved, setSaved] = useState(false);
	const change = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
		setValue(event.target.value);
		setError(null);
		setSaved(false);
	}, []);
	const submit = useCallback(
		(event: React.FormEvent<HTMLFormElement>) => {
			event.preventDefault();
			if (
				value &&
				(!DIGITS.test(value) || Number(value) < 1 || Number(value) > 1_000_000)
			) {
				setError("Ingresá un entero entre 1 y 1.000.000.");
				(
					event.currentTarget.elements.namedItem("target") as HTMLInputElement
				).focus();
				return;
			}
			setSaved(true);
		},
		[value]
	);
	return (
		<form className="w-full max-w-[520px]" noValidate onSubmit={submit}>
			<section className="space-y-5 rounded-[10px] border border-line bg-card p-4 text-ink text-sm shadow-card">
				<div>
					<h3 className="font-medium text-foreground text-sm">Meta semanal</h3>
					<p className="mt-0.5 text-muted-foreground text-xs">
						Cuántos pulsos esperás por semana. Inicio la usa para mostrar el
						avance; dejala vacía para no tener meta.
					</p>
				</div>
				<Field>
					<FieldLabel htmlFor="weekly-target-field">
						Pulsos por semana
					</FieldLabel>
					<Input
						aria-describedby={
							error ? "target-hint target-error" : "target-hint"
						}
						aria-invalid={error ? true : undefined}
						autoComplete="off"
						id="weekly-target-field"
						inputMode="numeric"
						name="target"
						onChange={change}
						placeholder="Sin meta"
						value={value}
					/>
					<FieldHint id="target-hint">Entero entre 1 y 1.000.000.</FieldHint>
					<FieldError id="target-error">{error}</FieldError>
				</Field>
				{saved ? (
					<p aria-live="polite" className="text-m5 text-xs">
						Validación local correcta. No se guardaron cambios.
					</p>
				) : null}
				<div className="flex flex-wrap justify-end gap-2">
					<Button type="submit">Guardar meta</Button>
				</div>
			</section>
		</form>
	);
}

Props

Field

PropTipoDefaultDescripción
...propsReact.ComponentProps<"div">-Contenedor vertical para label, input y mensajes.

FieldLabel

PropTipoDefaultDescripción
htmlForstring-ID del control asociado.
requiredbooleanfalseAgrega el asterisco visual. El control también necesita required.
childrenReact.ReactNode-Nombre visible del campo.

FieldHint

PropTipoDefaultDescripción
idstring-Referencia para aria-describedby del control.
childrenReact.ReactNode-Ayuda breve, con color de texto secundario.

FieldError

PropTipoDefaultDescripción
childrenReact.ReactNode-Error concreto; sin contenido no renderiza nada.
idstring-Referencia para aria-describedby del control.
roleReact.AriaRole"alert"Anuncia el mensaje cuando aparece.

FieldLegend

PropTipoDefaultDescripción
requiredbooleanfalseAgrega el asterisco a la pregunta de un fieldset.
...propsReact.ComponentProps<"legend">-Legend nativo para agrupar opciones relacionadas.

Cuándo sí y cuándo no

Hacé
  • Validá al enviar y limpiá el error cuando se corrige el campo.
  • Explicá cómo corregir el dato.
  • Enfocá el primer campo inválido al enviar.
Evitá
  • No creas que required en FieldLabel valida el input.
  • No muestres errores sin asociarlos al control.
  • No confundas la confirmación del ejemplo con una acción en el servidor.

Accesibilidad

  • Tab recorre los controles en orden; Enter puede enviar el formulario.
  • Conectá label y control con htmlFor e id.
  • Usá aria-invalid y aria-describedby en el control. FieldError anuncia el mensaje con role alert.