Form

PreviousNext

Renders a form built with Tanstack Form.

Docs
basecnui

Preview

Loading preview…
registry/components/ui/form-tanstack.tsx
"use client";

import { useRender } from "@base-ui/react/use-render";
import { createFormHookContexts, createFormHook } from "@tanstack/react-form";
import * as React from "react";

import { cn } from "@/lib/utils";
import { Label } from "@/registry/components/ui/label";

const { fieldContext, formContext, useFieldContext } = createFormHookContexts();

const { useAppForm } = createFormHook({
  fieldComponents: {
    Label: FieldLabel,
    Control: FieldControl,
    Description: FieldDescription,
    Message: FieldMessage,
  },
  formComponents: {
    Item: FormItem,
  },
  fieldContext,
  formContext,
});

const useFormField = () => {
  const itemContext = React.useContext(FormItemContext);
  const fieldContext = useFieldContext();

  if (!fieldContext) {
    throw new Error("useFormField should be used within <field.Container>");
  }

  const { id } = itemContext;

  return {
    id,
    name: fieldContext.name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    ...fieldContext.state.meta,
  };
};

type FormItemContextValue = {
  id: string;
};

const FormItemContext = React.createContext<FormItemContextValue>(
  {} as FormItemContextValue
);

function FormItem({ className, ...props }: React.ComponentProps<"div">) {
  const id = React.useId();

  return (
    <FormItemContext.Provider value={{ id }}>
      <div
        data-slot="form-item"
        className={cn("grid gap-2", className)}
        {...props}
      />
    </FormItemContext.Provider>
  );
}

function FieldLabel({
  className,
  ...props
}: React.ComponentProps<typeof Label>) {
  const { formItemId, isValid } = useFormField();

  return (
    <Label
      data-slot="field-label"
      data-error={!isValid}
      className={cn("data-[error=true]:text-destructive", className)}
      htmlFor={formItemId}
      {...props}
    />
  );
}

function FieldControl({
  children = <div />,
}: {
  children?: useRender.RenderProp;
}) {
  const { formItemId, isValid, formDescriptionId, formMessageId } =
    useFormField();

  return useRender({
    render: children,
    props: {
      "data-slot": "field-control",
      id: formItemId,
      "aria-describedby": isValid
        ? `${formDescriptionId}`
        : `${formDescriptionId} ${formMessageId}`,
      "aria-invalid": !isValid,
    },
  });
}

function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
  const { formDescriptionId } = useFormField();

  return (
    <p
      data-slot="field-description"
      id={formDescriptionId}
      className={cn("text-muted-foreground text-sm", className)}
      {...props}
    />
  );
}

function FieldMessage({ className, ...props }: React.ComponentProps<"p">) {
  const { formMessageId, isValid, errors } = useFormField();

  if (props.children) return props.children;

  const body = isValid
    ? props.children
    : String(errors.map((error) => error.message).join(", ") ?? "");

  if (!body) return null;

  return (
    <p
      data-slot="field-message"
      id={formMessageId}
      className={cn("text-destructive text-sm", className)}
      {...props}
    >
      {body}
    </p>
  );
}

export { useAppForm };

Installation

npx shadcn@latest add @basecn/form-tanstack

Usage

import { FormTanstack } from "@/components/ui/form-tanstack"
<FormTanstack />