Utilities

Component detection, install commands, Tailwind class generation, test data, and field transform utilities.

View as Markdown

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.

import { getRequiredComponents, FormBuilder } from '@saastro/forms';

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.

import { getMissingComponents } from '@saastro/forms';

// 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.

import { getInstallCommand } from '@saastro/forms';

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:

import { groupMissingByPackage } from '@saastro/forms';

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

fieldTypeComponents

The raw mapping of field type to required components:

import { fieldTypeComponents } from '@saastro/forms';

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:

import { coreComponents } from '@saastro/forms';
// ['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 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.

import { getFormGridClass } from '@saastro/forms';

// 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.

import { getFieldClass } from '@saastro/forms';

// 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.

import { getHiddenClasses } from '@saastro/forms';

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.

import { pxToRem } from '@saastro/forms';
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.

import { generateTestData, FormBuilder } from '@saastro/forms';

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:

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

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.

import { generateFieldValue } from '@saastro/forms';

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 undefinedFile objects can’t be fabricated.

detectLocale(fields)

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

import { detectLocale } from '@saastro/forms';

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.

import { applyFieldTransforms } from '@saastro/forms';

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 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):

import { getDirtyPaths, getDirtyFieldNames, getDirtyValues, pickByPaths } from '@saastro/forms';

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 somewhere.
  • The same data is available on the Form ref (getDirtyValues / getDirtyPaths / isDirty) and to plugins via FieldChangeMeta.