Form

Puente entre react-hook-form y los primitivos. FormField/FormControl.

Forms@base-ui/reactreact-hook-form

Installation

npx shadcn@latest add @saastro/form

Preview

Form preview

Source Code

/**
 * Minimal shadcn-style form primitives — just enough surface for
 * @saastro/forms to wire react-hook-form fields. Standard shadcn ships
 * this file via `shadcn add form`, but at the time of writing the
 * registry doesn't expose it under that name from this project's
 * `components.json`. Hand-rolling it keeps the bundle predictable.
 *
 * Exposes:
 *   - Form: el `FormProvider` de react-hook-form, re-exportado con el nombre
 *     que usa shadcn. No es un primitivo de Base UI y no lo pretende: es el
 *     contexto que `FormField` necesita para encontrar el formulario. Faltaba,
 *     y sin él los consumidores tienen que importarlo de react-hook-form por su
 *     cuenta, que es justo la dependencia que este fichero existe para tapar.
 *   - FormField: Controller wrapper that exposes `field` props to children
 *   - FormControl: reenvía id/aria-* al input subyacente. Con Radix esto era
 *     `Slot`; en Base UI el equivalente es el hook `useRender`, que fusiona las
 *     props sobre el hijo. Se mantiene hecho a mano A PROPÓSITO: el `FormControl`
 *     oficial de shadcn usa `useFormField()` y exige un `FormItem` alrededor,
 *     que @saastro/forms no monta — usarlo reventaría en runtime.
 *     NO regenerar este fichero con `shadcn add form`.
 *
 * The remaining named primitives @saastro/forms looks up (Field,
 * FieldLabel, FieldDescription, FieldError) come from the standard
 * shadcn `field.tsx`.
 */
import * as React from 'react';
import {
  Controller,
  FormProvider,
  type ControllerProps,
  type FieldPath,
  type FieldValues,
} from 'react-hook-form';
import { useRender } from '@base-ui/react/use-render';

/** El contexto de react-hook-form, con el nombre que espera shadcn. */
export const Form = FormProvider;

/**
 * EL SEGUNDO GENÉRICO NO ES ADORNO: SIN ÉL, `field.value` MIENTE.
 *
 * Faltaba `TName`, y sin él `ControllerProps` no puede estrechar por el `name=`
 * de cada campo: `field.value` sale como la UNIÓN de todos los campos del
 * formulario. En un diálogo que mezcla textos con un booleano, eso es
 * `string | boolean`, y el error aparece lejos de aquí — en el `value` del
 * `Input`, del `Select` y del `Switch`, ocho a la vez y todos en el fichero del
 * consumidor. Medido: con el genérico, cero.
 *
 * Es la firma que trae el shadcn oficial.
 */
export function FormField<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>(props: ControllerProps<TFieldValues, TName>) {
  return <Controller {...props} />;
}

export interface FormControlProps extends React.ComponentProps<'div'> {
  children?: React.ReactElement;
}

export const FormControl = React.forwardRef<HTMLElement, FormControlProps>(
  function FormControl({ children, ...props }, ref) {
    return useRender({
      render: children as React.ReactElement,
      props,
      ref: ref as React.Ref<HTMLElement>,
    });
  },
);

export const FormItem = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'>>(
  function FormItem({ className, ...props }, ref) {
    return <div ref={ref} className={className} {...props} />;
  },
);

export const FormLabel = React.forwardRef<HTMLLabelElement, React.ComponentProps<'label'>>(
  function FormLabel({ className, ...props }, ref) {
    return <label ref={ref} className={className} {...props} />;
  },
);

export const FormMessage = React.forwardRef<HTMLParagraphElement, React.ComponentProps<'p'>>(
  function FormMessage({ className, children, ...props }, ref) {
    if (!children) return null;
    return (
      <p ref={ref} className={className} {...props}>
        {children}
      </p>
    );
  },
);

export const FormDescription = FormMessage;

Registry Setup

Add the Saastro registry to your components.json:

{
  "registries": {
    "@saastro": "https://ui.saastro.io/r/{name}.json"
  }
}