FesaComponents

Recetas de formularios

Los patrones completos — schema, server action, wizard y listas repetibles.

Formulario completo con zod + server action

"use client";

const schema = z.object({
  name: z.string().min(1, "El nombre es requerido"),
  price: z.number().min(0, "El precio no puede ser negativo"),
  category: z.string().min(1, "La categoría es requerida"),
  active: z.boolean(),
});

export function ProductForm() {
  const [isPending, startTransition] = useTransition();
  const form = useForm<z.infer<typeof schema>>({
    resolver: zodResolver(schema),
    defaultValues: { name: "", price: 0, category: "", active: true },
  });

  const onSubmit = form.handleSubmit((values) => {
    startTransition(async () => {
      const result = await createProductAction(values);
      if (result.error) return toast.error(result.error);
      toast.success(result.success);
    });
  });

  return (
    <Form {...form}>
      <form onSubmit={onSubmit}>
        <FormSection title="Datos del producto">
          <FormTextField control={form.control} name="name" label="Nombre" required />
          <FormFieldRow>
            <FormCurrencyField control={form.control} name="price" label="Precio" required />
            <FormSelectField control={form.control} name="category" label="Categoría" options={CATEGORIES} required />
          </FormFieldRow>
          <FormSwitchField control={form.control} name="active" label="Activo" />
        </FormSection>
        <FormButtonGroup>
          <FormCancelButton onClick={() => router.back()}>Cancelar</FormCancelButton>
          <FormSubmitButton loading={isPending}>Crear producto</FormSubmitButton>
        </FormButtonGroup>
      </form>
    </Form>
  );
}

Wizard multipaso

const STEPS = ["Datos", "Dirección", "Confirmación"];

<FormStepIndicator steps={STEPS} currentStep={step} />
{step === 0 && <DatosSection control={form.control} />}
{step === 1 && <FormAddressField control={form.control} name="address" />}
{step === 2 && <FormValidationSummary control={form.control} />}

Valida por paso con form.trigger(["name", "email"]) antes de avanzar.

Lista repetible (items de una orden)

<FormArrayField
  control={form.control}
  name="items"
  addLabel="Agregar producto"
  renderItem={(index) => (
    <FormFieldRow>
      <FormTextField control={form.control} name={`items.${index}.name`} label="Producto" />
      <FormNumberField control={form.control} name={`items.${index}.qty`} label="Cantidad" min={1} />
    </FormFieldRow>
  )}
/>

Campo condicional

<FormSwitchField control={form.control} name="hasDelivery" label="¿Requiere entrega?" />
<FormConditionalField control={form.control} watch="hasDelivery" condition={(v) => v === true}>
  <FormAddressField control={form.control} name="deliveryAddress" label="Dirección de entrega" />
</FormConditionalField>

Búsqueda async contra el servidor

<FormSearchableCombobox
  control={form.control}
  name="clientId"
  label="Cliente"
  searchAction={searchClientsAction}
  placeholder="Buscar cliente…"
/>

searchAction es una server action (query: string) => Promise<ComboboxOption[]> — la librería pone el debounce.

En esta pagina