# Utilities

URL: https://docs.forms.saastro.io/docs/utilities
> Component detection, install commands, Tailwind class generation, test data, and field transform utilities.

# Utilities

`@saastro/forms` exports utility functions for component detection, Tailwind CSS class generation, test data generation, and field value transforms. All are tree-shakeable — import only what you need.

---

## Component Utilities

These functions help you determine which UI components a form needs and generate install commands for missing ones.

### getRequiredComponents(config)

Analyzes a `FormConfig` and returns all component names needed to render it.

```tsx

const config = FormBuilder.create('contact')
  .addField('name', (f) => f.type('text').label('Name').required())
  .addField('email', (f) => f.type('email').label('Email').required().email())
  .addField('date', (f) => f.type('date').label('Preferred Date').required())
  .addStep('main', ['name', 'email', 'date'])
  .build();

const needed = getRequiredComponents(config);
// ['Button', 'Field', 'FieldLabel', 'FieldDescription', 'FieldError',
//  'Input', 'Label', 'FormField', 'FormControl',
//  'Calendar', 'Popover', 'PopoverTrigger', 'PopoverContent']
```

The function always includes **core components** (`Button`, `Field`, `FieldLabel`, `FieldDescription`, `FieldError`) plus any components required by the field types in the config. If a field uses `tooltip`, tooltip components are included automatically.

### getMissingComponents(required, provided)

Compares required components against what's in your registry.

```tsx

// Pass your imported UI components as the registry
const missing = getMissingComponents(needed, {
  Input,
  Button,
  Label,
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FormField,
  FormControl,
});
// ['Calendar', 'Popover', 'PopoverTrigger', 'PopoverContent']
```

### getInstallCommand(missing)

Generates a `npx shadcn@latest add` command for missing components.

```tsx

const cmd = getInstallCommand(missing);
// "npx shadcn@latest add calendar popover"
```

Components are grouped by their shadcn package — `Calendar` maps to `calendar`, `Popover`/`PopoverTrigger`/`PopoverContent` all map to `popover`.

### groupMissingByPackage(missing)

Groups missing component names by their shadcn package name:

```tsx

const grouped = groupMissingByPackage(missing);
// { calendar: ['Calendar'], popover: ['Popover', 'PopoverTrigger', 'PopoverContent'] }
```

### fieldTypeComponents

The raw mapping of field type to required components:

```tsx

fieldTypeComponents['date'];
// ['Calendar', 'Popover', 'PopoverTrigger', 'PopoverContent', 'Button',
//  'FormField', 'FormControl', 'Field', 'FieldLabel', 'FieldDescription', 'FieldError']

fieldTypeComponents['text'];
// ['Input', 'Label', 'FormField', 'FormControl', 'Field', 'FieldLabel', 'FieldDescription', 'FieldError']

fieldTypeComponents['html'];
// [] — no components needed
```

### coreComponents

Components that every form needs regardless of field types:

```tsx
// ['Button', 'Field', 'FieldLabel', 'FieldDescription', 'FieldError']
```

---

## Tailwind Utilities

These functions generate Tailwind CSS classes for the form grid system. The form runtime uses them internally — [`useFormLayout`](/docs/use-form-layout) wraps `getFormGridClass`, and the field renderer applies `getFieldClass` / `getHiddenClasses` — but all are exported for custom implementations. Reach for them when you render fields outside the standard `<Form>` component and want your markup to match the form grid.

### getFormGridClass(layout?)

Generates the grid container classes and inline styles for the form.

```tsx

// Default (1 column, gap-4)
getFormGridClass();
// { className: 'grid grid-cols-1 gap-4', style: {} }

// Manual mode with 3 columns
getFormGridClass({ mode: 'manual', columns: 3, gap: 6 });
// { className: 'grid gap-6 grid-cols-3', style: {} }

// Auto mode (uses CSS grid auto-fit)
getFormGridClass({ mode: 'auto', minFieldWidth: 300, columns: 4, gap: 4 });
// {
//   className: 'grid gap-4',
//   style: {
//     gridTemplateColumns: 'repeat(auto-fit, minmax(max(18.75rem, calc((100% - 3rem) / 4)), 1fr))'
//   }
// }
```

In **manual mode**, it returns a `grid-cols-{n}` class. In **auto mode**, it uses inline `gridTemplateColumns` with `auto-fit` + `minmax()` because dynamic column counts can't be detected by Tailwind at build time. The gap defaults to `4`; a gap of `0` adds no gap class.

### getFieldClass(formLayout?, fieldLayout?)

Generates responsive `col-span-*` and `order-*` classes for a field.

```tsx

// Default
getFieldClass();
// 'col-span-1'

// Responsive columns
getFieldClass({ mode: 'manual', columns: 12 }, { columns: { default: 12, md: 6, lg: 4 } });
// 'col-span-12 md:col-span-6 lg:col-span-4'

// With order
getFieldClass({ mode: 'manual' }, { columns: { default: 6 }, order: { default: 2, lg: 1 } });
// 'col-span-6 order-2 lg:order-1'

// Auto mode — returns empty string (col-span not used with auto-fit)
getFieldClass({ mode: 'auto' }, { columns: { default: 6 } });
// ''
```

### getHiddenClasses(hidden?)

Generates responsive visibility classes.

```tsx

getHiddenClasses({ default: 'hidden', md: 'visible' });
// 'hidden md:block'

getHiddenClasses({ lg: 'hidden' });
// 'lg:hidden'
```

### pxToRem(px)

Converts pixels to rem (base 16px). Used internally for auto-mode grid calculations.

```tsx
pxToRem(240); // 15
```

---

## Test Data Generation

Lightweight utilities for generating realistic form data. No external dependencies (no faker.js). Supports locale detection and seeded randomness.

### generateTestData(fields, options?)

Generates realistic test values for all fields in a form config, skipping non-input fields (`html`, `button`, `submit`, `next`, `back`). Two locales are supported, English and Spanish; the locale is auto-detected from field names and labels (English unless two or more Spanish patterns match) — pass `options.locale` to force one.

```tsx

const config = FormBuilder.create('contact')
  .addField('firstName', (f) => f.type('text').label('First Name').required())
  .addField('email', (f) => f.type('email').label('Email').required().email())
  .addField('phone', (f) => f.type('tel').label('Phone').optional())
  .addField('plan', (f) =>
    f
      .type('select')
      .label('Plan')
      .required()
      .options([
        { label: 'Basic', value: 'basic' },
        { label: 'Pro', value: 'pro' },
      ]),
  )
  .addStep('main', ['firstName', 'email', 'phone', 'plan'])
  .build();

const data = generateTestData(config.fields);
// {
//   firstName: "James",            ← label heuristic
//   email: "test.user@example.com",
//   phone: "+1 555 123 4567",
//   plan: "pro"                    ← random option picked
// }
```

Forms written in Spanish are detected automatically and get Spanish-flavored data:

```tsx
const data = generateTestData({
  nombre: { type: 'text', label: 'Nombre', schema: { required: true } },
  correo: { type: 'email', label: 'Correo electrónico', schema: { required: true } },
});
// { nombre: "Carlos", correo: "usuario.test@example.com" }   ← Spanish locale detected
```

#### Options

```tsx
interface TestDataOptions {
  locale?: 'en' | 'es'; // Force locale (default: auto-detect)
  seed?: number; // Seed for deterministic output
}

// Deterministic output (same seed = same values)
const data = generateTestData(config.fields, { seed: 42 });
```

### generateFieldValue(name, config, options?)

Generates a realistic value for a single field. Uses a two-layer strategy:

1. **Label heuristics** — For text-like fields (`text`, `email`, `tel`, `url`, `password`, `number`, `input-group`, `currency`), matches the field name/label against known patterns (`firstName` → `"James"`, `email` → `"test.user@example.com"`)
2. **Type fallback** — If no pattern matches, generates appropriate data for the field type

When called standalone, the locale defaults to `'en'` — auto-detection only happens in `generateTestData`.

```tsx

generateFieldValue(
  'firstName',
  { type: 'text', label: 'First Name', schema: { required: true } },
  { locale: 'en' },
);
// "James"

generateFieldValue('rating', { type: 'slider', min: 1, max: 5, schema: {} }, { locale: 'en' });
// [3]

generateFieldValue('terms', { type: 'checkbox', label: 'Accept', schema: {} }, { locale: 'en' });
// true
```

Skips non-input types (`html`, `button`, `submit`, `next`, `back`) by returning `undefined`. `file` fields also return `undefined` — `File` objects can't be fabricated.

### detectLocale(fields)

Auto-detects whether form fields use Spanish or English based on field names and labels.

```tsx

detectLocale({
  nombre: { type: 'text', label: 'Nombre completo', schema: { required: true } },
  correo: { type: 'email', label: 'Correo electrónico', schema: { required: true } },
});
// 'es'

detectLocale({
  name: { type: 'text', label: 'Full Name', schema: { required: true } },
  email: { type: 'email', label: 'Email', schema: { required: true } },
});
// 'en'
```

Returns `'es'` if 2+ Spanish patterns are found in field names/labels, otherwise `'en'`.

**Recognized Spanish patterns** (24 total, matched as substrings of lowercased field names and labels): `nombre`, `apellido`, `correo`, `telefono`, `teléfono`, `dirección`, `direccion`, `ciudad`, `provincia`, `código`, `codigo`, `mensaje`, `empresa`, `comentario`, `fecha`, `edad`, `género`, `genero`, `contraseña`, `enviar`, `siguiente`, `atrás`, `acepto`, `términos`. Note that only `telefono`, `direccion`, `codigo`, and `genero` have unaccented variants — the rest must match as written.

---

## Field Transforms

### applyFieldTransforms(fields, values)

Applies per-field `transform` properties to form values before submission. Returns a new object (does not mutate input).

The form runtime applies these transforms automatically before submit actions run — the function is exported so custom submit flows can do the same.

```tsx

const fields = {
  email: { type: 'email', label: 'Email', transform: 'lowercase' },
  name: { type: 'text', label: 'Name', transform: ['trim', 'uppercase'] },
  notes: { type: 'textarea', label: 'Notes' }, // no transform
};

const values = {
  email: '  User@Example.COM  ',
  name: '  john doe  ',
  notes: 'Hello world',
};

const result = applyFieldTransforms(fields, values);
// {
//   email: "  user@example.com  ",
//   name: "JOHN DOE",
//   notes: "Hello world"          ← unchanged
// }
```

Supports three transform formats:

- **Single built-in:** `'trim'`
- **Chain:** `['trim', 'lowercase']` — applied left to right
- **Custom function:** `(value) => String(value).trim()`

See [Field-Level Transforms](/docs/submit#field-level-transforms) for the full list of built-in transforms.

---

## Dirty Tracking

Helpers over React Hook Form's `dirtyFields` mirror — they report what the **user** edited (machine writes from computed fields and hidden-field resolvers use `shouldDirty: false` and stay out):

```tsx

getDirtyPaths(dirtyFields);            // ['email', 'contacts.0.name'] — leaf dot-paths
getDirtyFieldNames(dirtyFields);       // ['email', 'contacts'] — top-level names
getDirtyValues(methods);               // { email, contacts } — dense, serializable payload
pickByPaths(anyObject, paths);         // nested partial picked from any object
```

Notes:

- `getDirtyValues` is deliberately top-level-grained (arrays go whole, never sparse). For leaf grain compose `pickByPaths(methods.getValues(), getDirtyPaths(...))`.
- RHF semantics apply: arrays report per item; a value reverted to its default stops being dirty.
- RHF only *computes* `dirtyFields` when something subscribes to that state slice. `<Form>` guarantees it internally; in headless `useFormState` usage, mount [`useFormFlags`](/docs/use-form-state#dirty--validating-flags--use-useformflags) somewhere.
- The same data is available on the Form ref (`getDirtyValues` / `getDirtyPaths` / `isDirty`) and to plugins via [`FieldChangeMeta`](/docs/plugins#lifecycle-hooks).

---

## Related

- [Component System](/docs/components) — How component injection works
- [Layout System](/docs/layout) — Grid configuration
- [Submit & Actions](/docs/submit) — Field mapping and transforms
- [Types Reference](/docs/types) — Full type definitions
