Components API Reference

Form Components

Form

The main wrapper component for forms. Provides form context to child components.

interface FormProps<T extends FieldValues> extends Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'> {
  children: ReactNode;
  form: FormContextApi<T>;
  onSubmit?: () => void;
}

Props:

  • children - Child components (Fields, Rules, etc.)
  • form - Form context from useForm
  • onSubmit - Custom submit handler
  • All standard HTML form attributes except onSubmit

Example:

function MyForm() {
  const form = useForm<FormValues>();

  const handleSubmit = () => {
    const values = form.getFormValues();
    console.log('Form submitted:', values);
  };

  return (
    <Form form={form} onSubmit={handleSubmit}>
      <Field
        name="email"
        render={({ value, onChange }) => (
          <input
            type="email"
            value={value || ''}
            onChange={(e) => onChange(e.target.value)}
          />
        )}
      />
      <button type="submit">Submit</button>
    </Form>
  );
}

Field

A generic field component that uses the render prop pattern for maximum flexibility.

interface FieldProps<T extends FieldValues, K extends keyof T> {
  name: K;
  children?: ReactNode; // Rules
  render(fieldProps: FieldRenderProps<T, K>, fieldState: FieldRenderState): ReactNode | null;
  data?: AnyRecord;
  defaultValue?: T[K];
}

interface FieldRenderProps<T, K> {
  name: K;
  value: T[K] | undefined;
  onFocus(): void;
  onBlur(): void;
  onChange(value: T[K] | undefined): void;
}

interface FieldRenderState {
  isPristine: boolean;
  validationStatus: ValidationStatus;
}

Props:

  • name - Field name (must match form type)
  • render - Render function that receives field props and state
  • children - Validation rules (Rule components)
  • data - Additional data passed to onUpdateAfterBlur callback
  • defaultValue - Default value for the field

Example:

<Field
  name="username"
  defaultValue="admin"
  data={{ userId: 123 }}
  render={({ value, onChange, onBlur, onFocus }, { isPristine, validationStatus }) => (
    <div>
      <input
        value={value || ''}
        onChange={(e) => onChange(e.target.value)}
        onBlur={onBlur}
        onFocus={onFocus}
        className={validationStatus.status === 'INVALID' ? 'error' : ''}
      />
      {!isPristine && validationStatus.status === 'INVALID' && (
        <span className="error-message">{validationStatus.message}</span>
      )}
    </div>
  )}
>
  <Rule validationFn={isRequired} message="Username is required" />
</Field>

Rule

A validation rule component that defines field validation logic.

interface RuleProps {
  validationFn: Validator;
  message: string;
  isDebounced?: boolean;
}

type Validator = (value: FieldValue) => boolean | Promise<boolean>;

Props:

  • validationFn - Validation function that returns true if valid
  • message - Error message to display when validation fails
  • isDebounced - Whether to debounce validation (useful for async validation)

Example:

// Sync validation
const isRequired = (value: unknown): boolean => {
  return value != null && value !== '';
};

// Async validation
const isUniqueEmail = async (email: string): Promise<boolean> => {
  const response = await fetch('/api/check-email', {
    method: 'POST',
    body: JSON.stringify({ email })
  });
  return response.ok;
};

<Field name="email" render={...}>
  <Rule validationFn={isRequired} message="Email is required" />
  <Rule
    validationFn={isUniqueEmail}
    message="Email already exists"
    isDebounced
  />
</Field>

Wizard Components

Step

A component representing a single step in a wizard flow.

interface StepProps<WizardValues extends Record<string, FieldValues>, Step extends keyof WizardValues> {
  children: ReactNode;
  name: Step;
  onNext?: StepValidator<WizardValues, Step>;
  noFooter?: boolean;
  title?: string;
}

type StepValidator<WizardValues, Step> = (
  stepValues: WizardValues[Step] | undefined
) => boolean | Promise<boolean>;

Props:

  • children - Form fields and other content for the step
  • name - Step identifier (must match wizard type)
  • onNext - Custom validation function called before proceeding to next step
  • noFooter - Disable the default navigation footer
  • title - Step title (displayed in step header)

Example:

function PersonalInfoStep() {
  const { form } = useStepForm<WizardData, 'personal'>();

  const validatePersonalInfo = async (values: WizardData['personal']) => {
    return values?.firstName && values?.lastName;
  };

  return (
    <Step
      name="personal"
      title="Personal Information"
      onNext={validatePersonalInfo}
    >
      <Field
        name="firstName"
        render={({ value, onChange }) => (
          <input
            placeholder="First Name"
            value={value || ''}
            onChange={(e) => onChange(e.target.value)}
          />
        )}
      >
        <Rule validationFn={isRequired} message="First name is required" />
      </Field>
      
      <Field
        name="lastName"
        render={({ value, onChange }) => (
          <input
            placeholder="Last Name"
            value={value || ''}
            onChange={(e) => onChange(e.target.value)}
          />
        )}
      >
        <Rule validationFn={isRequired} message="Last name is required" />
      </Field>
    </Step>
  );
}

Custom Step Navigation

When using noFooter={true}, you can implement custom navigation:

<Step name="custom" title="Custom Step" noFooter>
  <div className="step-content">
    {/* Your form fields */}
  </div>
  
  <div className="custom-footer">
    <button
      onClick={wizard.goPrevious}
      disabled={wizard.isFirstStep}
      className="btn-secondary"
    >
      Back
    </button>
    
    <button
      onClick={wizard.goNext}
      disabled={!wizard.isStepReady}
      className="btn-primary"
    >
      {wizard.isLastStep ? 'Complete' : 'Continue'}
    </button>
  </div>
</Step>

Context Providers

WizardContext

Provides wizard context to child components. Usually used with WizardContext.Provider.

import { WizardContext } from 'graneet-form';

function MyWizard() {
  const wizard = useWizard<WizardData>(onFinish, onQuit);

  return (
    <WizardContext.Provider value={wizard}>
      <PersonalInfoStep />
      <ContactInfoStep />
      <SummaryStep />
    </WizardContext.Provider>
  );
}

Component Patterns

Reusable Field Components

Create reusable field components by wrapping the Field component:

interface TextInputProps<T extends FieldValues, K extends keyof T> {
  name: K;
  placeholder?: string;
  type?: string;
  required?: boolean;
}

function TextInput<T extends FieldValues, K extends keyof T>({
  name,
  placeholder,
  type = 'text',
  required = false
}: TextInputProps<T, K>) {
  return (
    <Field
      name={name}
      render={({ value, onChange, onBlur, onFocus }, { validationStatus, isPristine }) => (
        <div className="form-field">
          <input
            type={type}
            placeholder={placeholder}
            value={value || ''}
            onChange={(e) => onChange(e.target.value)}
            onBlur={onBlur}
            onFocus={onFocus}
            className={
              !isPristine && validationStatus.status === 'INVALID' ? 'error' : ''
            }
          />
          {!isPristine && validationStatus.status === 'INVALID' && (
            <span className="error-message">{validationStatus.message}</span>
          )}
        </div>
      )}
    >
      {required && <Rule validationFn={isRequired} message={`${String(name)} is required`} />}
    </Field>
  );
}

// Usage
<TextInput<FormValues, 'email'> name="email" type="email" required />

Conditional Fields

Show/hide fields based on other field values:

function ConditionalFields() {
  const form = useFormContext<FormValues>();
  const { accountType } = useOnChangeValues(form, ['accountType']);

  return (
    <>
      <Field name="accountType" render={...} />
      
      {accountType === 'business' && (
        <Field name="companyName" render={...} />
      )}
      
      {accountType === 'personal' && (
        <Field name="dateOfBirth" render={...} />
      )}
    </>
  );
}