Ir al contenido
Formularios

Form

Un form nativo que junta los valores por name y reparte los errores del servidor a cada campo.

import { Form } from "sebs7n-ui/form"

Ejemplos

Los valores llegan juntados por `name`

Sin `FormData`, sin un `useState` por campo y sin librería de formularios. Cada `Field` aporta su `name` y el submit recibe el objeto armado.

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 Valores() {
  const [enviado, setEnviado] = useState<Record<string, unknown> | null>(null)

  return (
    <Form className="w-full max-w-sm" onFormSubmit={(valores) => setEnviado(valores)}>
      <Field name="nombre">
        <FieldLabel required>Nombre</FieldLabel>
        <Input required />
        {/* El mensaje se escribe siempre: el del navegador sale en el idioma
            del navegador, no en el de la página. */}
        <FieldError match="valueMissing">Falta el nombre</FieldError>
      </Field>
      <Field name="empresa">
        <FieldLabel>Empresa</FieldLabel>
        <Input />
      </Field>
      <Button type="submit">Enviar</Button>
      {enviado && (
        <pre className="rounded-control bg-fill-1 p-3 text-mono-callout text-label-secondary">
          {JSON.stringify(enviado, null, 2)}
        </pre>
      )}
    </Form>
  )
}

Validar con un schema

`sebs7n-ui/lib/schema` traduce cualquier schema que implemente Standard Schema —Zod, Valibot, ArkType— a los errores que espera `Form`. El paquete no depende de ninguna de las tres: habla la interfaz, no la librería.

import { Button } from "sebs7n-ui/button"
import { Field, FieldDescription, FieldError, FieldLabel } from "sebs7n-ui/field"
import { Form } from "sebs7n-ui/form"
import { Input } from "sebs7n-ui/input"
import { useState } from "react"
import { validate, type StandardSchemaV1 } from "sebs7n-ui/lib/schema"

function ConSchema() {
  const [errors, setErrors] = useState({})

  // En una app esto sería `z.object({ ... })`. Acá va a mano para que el sitio
  // de documentación no tenga que instalar Zod solo para un ejemplo.
  const esquema: StandardSchemaV1<unknown, { email: string; edad: number }> = {
    "~standard": {
      version: 1,
      vendor: "demo",
      validate: (valores) => {
        const { email, edad } = valores as { email: string; edad: string }
        const issues = []
        if (!email?.includes("@")) issues.push({ message: "Revisá el email", path: ["email"] })
        if (!Number(edad)) issues.push({ message: "Poné un número", path: ["edad"] })
        else if (Number(edad) < 18) issues.push({ message: "Tenés que ser mayor de edad", path: ["edad"] })
        return issues.length > 0 ? { issues } : { value: { email, edad: Number(edad) } }
      },
    },
  }

  return (
    <Form
      className="w-full max-w-sm"
      errors={errors}
      onFormSubmit={async (valores) => {
        const resultado = await validate(esquema, valores)
        // Si pasa, `resultado.value` ya viene parseado por el schema: `edad` es
        // un número, no el string que devuelve el input.
        setErrors(resultado.ok ? {} : resultado.errors)
      }}
    >
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input type="email" />
        <FieldError />
      </Field>
      <Field name="edad">
        <FieldLabel>Edad</FieldLabel>
        <Input inputMode="numeric" />
        <FieldDescription>Probá con 15 para ver el error.</FieldDescription>
        <FieldError />
      </Field>
      <Button type="submit">Validar</Button>
    </Form>
  )
}

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

Form

Hereda las props de Form.

PropTipoPor defectoDescripción
classNamestring—Se fusiona con las clases del componente vía cn() (tailwind-merge): lo que pongas gana.
errorsFormContext['errors']—Heredada de Base UI. Objeto { nombreDelCampo: mensaje }. Es para los errores que solo conoce el servidor.
onFormSubmit((formValues: FormValues, eventDetails: Form.SubmitEventDetails) => void)—Heredada de Base UI. Recibe los valores juntados por name. Ya hace preventDefault().
validationMode"onBlur" | "onChange" | "onSubmit"—Heredada de Base UI. Cuándo se validan los campos que no lo definan por su cuenta.

Teclado

Enter
Envía el formulario desde cualquier campo de texto, como cualquier <form>.
Tab
Recorre los campos y llega al submit.

Accesibilidad

  • Es un <form> de verdad: lo entiende el navegador y sigue funcionando sin JavaScript.
  • Al fallar la validación, el foco va al primer campo con error en vez de quedarse en el botón.
  • Cada error aparece en su campo, no en un cartel arriba de todo: quien navega con lector de pantalla lo encuentra donde tiene que arreglarlo.
  • Escribí siempre el mensaje del error, con `match` o con `validate`. El del navegador sale en el idioma del navegador, no en el de la página: un formulario en español puede terminar diciendo «Please fill out this field». Es el error más fácil de no ver, porque en la máquina de quien lo programó el navegador está en español.

Reglas de uso

  • `errors` es para lo que el navegador no puede saber: que un email ya está usado, que el cupón venció, que el CUIT no existe en AFIP. Se limpia solo cuando el campo cambia.
  • onFormSubmit recibe los valores ya juntados por name: no hace falta FormData ni un useState por campo.
  • Para validar todo contra un schema está validate() de sebs7n-ui/lib/schema, que devuelve el valor parseado o los errores con la forma que espera esta prop.
  • El submit se deshabilita mientras se envía (<Button loading>), o el mismo formulario se manda dos veces.

Relacionados