# Custom Fields

URL: https://docs.forms.saastro.io/docs/custom-fields
> Register your own field types via plugins — full access to the DI registry, conditional logic, and the FieldWrapper scaffolding (label, errors, helper text) that built-in fields use.

# Custom Fields

When the 34 built-in field types aren't enough — a numeric stepper, a signature pad, a rating widget — you can register your own field types through the plugin system. Custom fields are first-class citizens: they live in the same config, participate in validation and conditional logic, and receive the same rendering context as built-in fields.

## How it works

1. You write a React component that receives `CustomFieldProps`.
2. A plugin registers it under a type string via `registerFields()`.
3. Any field config using that type string renders your component.

```tsx

  name: 'stepper-field',
  version: '1.0.0',
  registerFields: () => ({
    stepper: StepperField, // type string → component
  }),
});
```

## What your component receives

Your component gets the full renderer context — the same one built-in fields use:

```tsx

function MyField(props: CustomFieldProps) {
  props.name; //            field name (react-hook-form path)
  props.fieldConfig; //     your config, with any bespoke props you added
  props.control; //         react-hook-form control
  props.colSpanItem; //     pre-computed wrapper class (responsive col-span)
  props.components; //      the DI component registry (Button, Input, ...)
  props.shouldDisable; //   `disabled` already evaluated (boolean | fn | ConditionGroup)
  props.shouldReadOnly; //  `readOnly` already evaluated
  props.renderLabel; //     renders the label (HTML + tooltip support)
  props.renderFieldIcon; // renders the configured icon
}
```

Conditional `hidden` is handled for you *before* your component renders — a hidden custom field is simply not mounted.

## Complete example: a stepper field

A quantity stepper with −/+ buttons, composed with the exported `FieldWrapper` so label, helper text, and error messages work exactly like built-in fields:

```tsx

type StepperConfig = {
  min?: number;
  max?: number;
  step?: number;
};

  const { name, fieldConfig, control, colSpanItem } = props;
  const { components, shouldDisable, renderLabel, renderFieldIcon } = props;

  const { Button } = components;
  const { min = 0, max = Infinity, step = 1 } = fieldConfig as StepperConfig;

  return (
    <FieldWrapper
      control={control}
      name={name}
      fieldConfig={fieldConfig}
      formItemClassName={colSpanItem}
      renderLabel={renderLabel}
      renderFieldIcon={renderFieldIcon}
    >
      {({ field }) => {
        const value = typeof field.value === 'number' ? field.value : min;
        return (
          <div className="flex items-center gap-3">
            <Button
              type="button"
              variant="outline"
              size="icon"
              disabled={shouldDisable || value <= min}
              onClick={() => field.onChange(value - step)}
              aria-label="Decrease"
            >
              −
            </Button>
            <span className="min-w-8 text-center font-medium tabular-nums">{value}</span>
            <Button
              type="button"
              variant="outline"
              size="icon"
              disabled={shouldDisable || value >= max}
              onClick={() => field.onChange(value + step)}
              aria-label="Increase"
            >
              +
            </Button>
          </div>
        );
      }}
    </FieldWrapper>
  );
}
```

`FieldWrapper` gives you the standard field chrome: label (with tooltip support), `FormControl` wiring, `helperText`, and validation errors — all through the user's injected components.

## Using a custom field

Register the plugin and use the type string. `FieldBuilder.type()` accepts any string, and `.prop()` sets your bespoke config props:

```tsx

const pm = new PluginManager();
pm.register(stepperPlugin);

const config = FormBuilder.create('order')
  .usePlugins(pm)
  .addField('guests', (f) =>
    f
      .type('stepper')
      .label('Guests')
      .prop('min', 1)
      .prop('max', 10)
      .defaultValue(2)
      .schema(z.number().min(1)),
  )
  .addStep('main', ['guests'])
  .buttons({ submit: { type: 'submit', label: 'Send' } })
  .build();

<Form config={config} components={uiComponents} onSubmit={console.log} />;
```

Raw JSON config works too — bespoke props travel verbatim:

```json
{
  "guests": {
    "type": "stepper",
    "label": "Guests",
    "min": 1,
    "max": 10,
    "defaultValue": 2
  }
}
```

## Validation and defaults

- **Schema is optional** for custom types: `build()` can't know your value shape, so validation is opt-in. Pass a Zod schema (`.schema(z.number().min(1))`) or serializable rules when you want it.
- **Default values**: the form seeds custom fields from `value` ?? `defaultValue` ?? `''`. For non-string values (like the stepper's number), set one of them explicitly.
- **`customValidators`** from plugins work on custom fields as long as the field has a schema to chain onto.

## Conditional logic

Everything from the [Conditional Logic](/docs/conditional-logic) guide applies:

```tsx
f.type('stepper')
  .label('Children')
  .prop('min', 0)
  .hidden({ operator: 'AND', conditions: [{ field: 'hasChildren', operator: 'equals', value: false }] })
  .schema(z.number())
```

`hidden` unmounts the field before your component runs; `disabled`/`readOnly` arrive pre-evaluated as `shouldDisable`/`shouldReadOnly`.

## TypeScript notes

- `CustomFieldProps`, `FieldWrapperProps`, and `CustomFieldConfig` are exported.
- `fieldConfig` is typed as `AnyFieldConfig` — cast to your own shape for bespoke props (see `StepperConfig` above).
- Custom types stay **out of** the `FieldConfig` discriminated union on purpose; `Fields` accepts both built-in and custom configs (`AnyFieldConfig`).

## Limitations

- The visual form builder doesn't know about your custom types — custom fields are a code-level feature.
- Type-specific `FieldBuilder` methods don't exist for your props; use `.prop(key, value)` or raw config.
