base-form-tanstack

PreviousNext
Docs
reuiui

Preview

Loading preview…
registry/default/ui/base-form-tanstack.tsx
'use client';

import * as React from 'react';
import { Label } from '@/registry/default/ui/base-label';
import { useRender } from '@base-ui-components/react/use-render';
import { createFormHookContexts, createFormHook as createTanstackFormHook } from '@tanstack/react-form';
import { cn } from '@/lib/utils';

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

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>
  );
}

const createFormHook = (args?: Parameters<typeof createTanstackFormHook>[0]) => {
  const formHook = createTanstackFormHook({
    fieldComponents: {
      ...args?.fieldComponents,
      Label: FieldLabel,
      Control: FieldControl,
      Description: FieldDescription,
      Message: FieldMessage,
    },
    formComponents: { ...args?.formComponents, Item: FormItem },
    fieldContext,
    formContext,
  });

  return formHook;
};

export { createFormHook };

Installation

npx shadcn@latest add @reui/base-form-tanstack

Usage

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