Ir al contenido
Formularios

Field

Un campo: la etiqueta, la ayuda y el error atados al control, sin un solo id escrito a mano.

import { Field, FieldControl, FieldDescription, FieldError, … } from "sebs7n-ui/field"

Ejemplos

Un campo completo

La etiqueta, la ayuda y el error atados al control sin un solo `id` escrito a mano. Comparalo con armar lo mismo suelto: `useId`, `htmlFor`, `aria-describedby` condicional y `aria-invalid`.

import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"

function Basico() {
  return (
    <Field className="w-full max-w-sm" name="cuit">
      <FieldLabel required>CUIT</FieldLabel>
      <Input placeholder="30712345678" required />
      <FieldDescription>Once dígitos, sin guiones.</FieldDescription>
      <FieldError />
    </Field>
  )
}

Cuándo se valida

`validationMode="onBlur"` marca el error recién al salir del campo. En `onChange` el rojo aparece a la segunda letra del email, cuando todavía falta todo: sirve para un campo que se puede evaluar entero mientras se escribe, como el largo de una contraseña.

import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"

function Validacion() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-6">
      <Field
        name="email"
        validate={(valor) => (String(valor).includes("@") ? null : "Falta el @: revisá el email")}
        validationMode="onBlur"
      >
        <FieldLabel>Email</FieldLabel>
        <Input placeholder="vos@empresa.com" type="email" />
        <FieldError />
      </Field>

      <Field
        name="password"
        validate={(valor) => (String(valor).length >= 8 ? null : "Mínimo 8 caracteres")}
        validationMode="onChange"
      >
        <FieldLabel>Contraseña</FieldLabel>
        <Input type="password" />
        <FieldDescription>Se valida mientras escribís porque acá sí se puede saber desde el primer carácter.</FieldDescription>
        <FieldError />
      </Field>
    </div>
  )
}

El error que solo conoce el servidor

Que un email ya esté usado no se puede validar en el navegador. `Form` recibe un objeto `{ campo: mensaje }` y cada `FieldError` muestra el suyo, en el campo que corresponde y no en un cartel arriba de todo.

import { Button } from "sebs7n-ui/button"
import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { useState } from "react"

function ErroresDelServidor() {
  const [errors, setErrors] = useState<Record<string, string>>({})
  const [enviando, setEnviando] = useState(false)

  return (
    <Form
      className="w-full max-w-sm"
      errors={errors}
      onFormSubmit={async (valores) => {
        setEnviando(true)
        await new Promise((r) => setTimeout(r, 600))
        setEnviando(false)
        // El servidor de mentira: cualquier email de este dominio ya está tomado.
        setErrors(
          String(valores.email).endsWith("@acme.com") ? { email: "Ese email ya tiene una cuenta" } : {}
        )
      }}
    >
      <Field name="email">
        <FieldLabel required>Email</FieldLabel>
        <Input placeholder="probá con algo@acme.com" required type="email" />
        <FieldError />
      </Field>
      <Button loading={enviando} type="submit">
        Crear cuenta
      </Button>
    </Form>
  )
}

Obligatorio que valida el servidor

`FieldLabel indicator` dibuja el asterisco y el lector dice «Razón social, obligatorio», sin `required` en el control: el formulario no corta el envío y el error llega del servidor por `Form`.

import { Field, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Input } from "sebs7n-ui/input"

function IndicadorSinValidar() {
  return (
    <Field className="w-full max-w-sm" name="businessName">
      <FieldLabel indicator>Razón social</FieldLabel>
      <Input placeholder="Estudio Ruiz S.R.L." />
      <FieldError />
    </Field>
  )
}

Props

Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».

Field

Hereda las props de Field.Root.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.
namestring—Heredada de Base UI. Identifica al campo en los valores del submit y en el objeto errors de Form.
validate((value: unknown, formValues: Form.Values) => string | string[] | void | Promise<string | string[] | void>)—Heredada de Base UI. Devolvé el mensaje si el valor está mal, o null si está bien. Puede ser asíncrona.
validationMode"onBlur" | "onChange" | "onSubmit"—Heredada de Base UI. onSubmit (default), onBlur u onChange. Tiene precedencia sobre el del Form.

FieldControl

Hereda las props de Field.Control.

Sin props propias: pasa todo al primitivo.

FieldDescription

Hereda las props de Field.Description.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

FieldError

Hereda las props de Field.Error.

PropTipoPor defectoDescripción
alertbooleanfalserole="alert" para que el error se anuncie al aparecer. Solo con validationMode="onChange".
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

FieldLabel

Hereda las props de Field.Label.

PropTipoPor defectoDescripción
indicatorbooleanfalseEl asterisco más «, obligatorio» para el lector (labels.field.required), sin la restricción nativa: para un campo que valida el servidor. No va con un control required o aria-required, que lo diría dos veces.
requiredbooleanfalseDibuja el asterisco. No hace obligatorio al campo: eso es el required del control.
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.

FieldValidity

Hereda las props de Field.Validity.

Sin props propias: pasa todo al primitivo.

Teclado

Tab
Entra y sale del control. La etiqueta no recibe foco: al clickearla, lo recibe el control.

Accesibilidad

  • Esto es lo que resuelve el componente. La etiqueta nombra al control, la ayuda y el error lo describen, y el error pone aria-invalid: todo por anidar las partes, sin useId, sin htmlFor y sin armar un aria-describedby condicional que es justo lo que se olvida.
  • El asterisco de required es aria-hidden: nadie escucha "Razón social asterisco". Que el campo sea obligatorio lo anuncia el required del control.
  • Si lo valida el servidor y el control no lleva required, FieldLabel indicator: el mismo asterisco, y el nombre del control pasa a ser «Razón social, obligatorio» (el texto es labels.field.required). Con required en la etiqueta también, ese texto se calla: lo anuncia el required del control. El del control solo no se detecta: indicator va sin required ni aria-required en el control, o se diría dos veces.
  • FieldError no ocupa lugar mientras el campo está bien, y cuando aparece ya está referenciado: no hace falta mover el foco para que se lea.
  • Escribí siempre el mensaje, con `match` o con `validate`. El del navegador sale en el idioma del navegador y no en el de la página: con Chrome en inglés, abajo de «Razón social» aparece «Please fill out this field». match="valueMissing" y compañía además hablan del dato —«Falta la razón social»— en vez del input.
  • El mensaje se lee al enfocar el campo, porque el campo lo referencia con aria-describedby. Con validationMode="onChange" eso no alcanza: el error aparece con el foco ya adentro y nada lo anuncia. Para ese caso está alert, que le pone role="alert". Es opt-in porque role="alert" interrumpe: en el camino de enviar duplicaría el anuncio y cortaría el del nombre del campo, que es la mitad que da contexto.
  • Input, Textarea y Select se enganchan solos. Para cualquier otro control va FieldControl con render.
  • Un control propio se engancha solo si reenvía lo que recibe. FieldControl le pasa id, name, aria-describedby, aria-invalid y una ref; un componente que declara id y name como props propias y no hace spread del resto se queda sin nada, y la etiqueta del campo apunta al vacío. Pasa seguido con un date picker o un autocomplete hechos con un <input type="hidden"> más un botón: el arreglo va adentro de ese componente, no en cada uso.

Reglas de uso

  • El `name` es la bisagra con `Form`: es la clave de los valores del submit y la del objeto errors que devuelve el servidor. Para datos anidados se usa punto (domicilio.calle), que es lo que devuelve el adaptador de schemas.
  • `validationMode="onSubmit"` (el default) casi siempre. Marcar el email en rojo mientras se escribe es castigar a alguien por no haber terminado. onBlur para un dato que recién se puede juzgar completo; onChange solo cuando se puede evaluar desde el primer carácter, como el largo de una contraseña.
  • La ayuda va visible en FieldDescription, no en un tooltip: una ayuda que hay que descubrir no ayuda a quien más la necesita.
  • Un mensaje propio para un motivo puntual se escribe con match (<FieldError match="valueMissing">Falta el email</FieldError>): habla del dato, no del input.
  • Validar contra Zod, Valibot o ArkType: fieldValidator de sebs7n-ui/lib/schema.
  • `FieldError` es una fuente o la otra, no las dos. Sin match se muestra ante *cualquier* invalidez —la del navegador y la que vino del servidor—, así que puesto al lado de uno con match imprime el mensaje dos veces. Si escribís mensajes propios con match, renderizá el genérico solo cuando el servidor devolvió algo para ese campo.
  • Un mensaje propio por match no es adorno: el del navegador sale en el idioma del navegador, no en el de la página, y habla del input ("complete este campo") en vez del dato que se pide.

Relacionados