S

Hidden Fields

Capture metadata, tracking data, and computed values with invisible fields that resolve dynamically at runtime.

Hidden Fields

Hidden fields capture contextual data without any visible UI. They use resolvers — declarative configs that compute a value when the form mounts.


When to Use Hidden Fields

Use CaseExample Resolver
Track form submission time{ $resolver: 'timestamp' }
Capture UTM parameters{ $resolver: 'urlParam', param: 'utm_source' }
Record the referring page{ $resolver: 'referrer' }
Log visitor IP address{ $resolver: 'ip' }
Know which page the form is on{ $resolver: 'pageUrl' }

Quick Start

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

const config = FormBuilder.create('contact')
  .addField('email', (f) => f.type('email').label('Email').required())
  // Invisible — resolved at form mount
  .addField('submitted_at', (f) => f.type('hidden').resolver({ $resolver: 'timestamp' }))
  .addField('utm_source', (f) =>
    f.type('hidden').resolver({
      $resolver: 'urlParam',
      param: 'utm_source',
      fallback: 'direct',
    }),
  )
  .addStep('main', ['email', 'submitted_at', 'utm_source'])
  .build();

On submit, the data includes:

{
  "email": "user@example.com",
  "submitted_at": "2026-02-25T12:00:00.000Z",
  "utm_source": "google"
}

Built-in Resolvers

timestamp

Returns the current date/time as an ISO 8601 string.

f.type('hidden').resolver({ $resolver: 'timestamp' });
// → "2026-02-25T12:34:56.789Z"

hostname

Returns window.location.hostname.

f.type('hidden').resolver({ $resolver: 'hostname' });
// → "forms.saastro.io"

pageUrl

Returns the full page URL (window.location.href).

f.type('hidden').resolver({ $resolver: 'pageUrl' });
// → "https://forms.saastro.io/contact?ref=nav"

referrer

Returns document.referrer — the URL of the page that linked here.

f.type('hidden').resolver({ $resolver: 'referrer' });
// → "https://google.com/search?q=saastro"

userAgent

Returns the browser’s user agent string.

f.type('hidden').resolver({ $resolver: 'userAgent' });
// → "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ..."

urlParam

Reads a query parameter from the current URL.

f.type('hidden').resolver({
  $resolver: 'urlParam',
  param: 'utm_source', // required — param name
  fallback: 'direct', // optional — default if param not found
});
// URL: ?utm_source=google → "google"
// URL: (no param)         → "direct"

ip

Fetches the visitor’s IP address from an external API. This is the only async resolver.

f.type('hidden').resolver({
  $resolver: 'ip',
  endpoint: 'https://api.ipify.org?format=json', // optional — custom API
  fallback: 'unknown', // optional — if fetch fails
});
// → "203.0.113.42"

How Resolvers Execute

The useHiddenFieldResolvers hook runs inside the Form component:

  1. On mount, it filters all fields with type: 'hidden'
  2. Runs all resolvers in parallel via Promise.all
  3. Sets each value with react-hook-form’s setValue(name, value, { shouldDirty: false })
  4. Cleanup function prevents stale writes if the component unmounts

Most resolvers are synchronous (timestamp, hostname, etc.). The ip resolver uses fetch and is async. All resolvers execute concurrently regardless.


Resolvers vs Field Mapping Inject

Both resolvers and field mapping inject can add computed values. Use the right tool:

FeatureHidden Field ResolverField Mapping Inject
When it runsForm mount (once)Before submit action
Visible in form stateYes (via setValue)No (transform only)
Configurable per-fieldYesPer submit action
Available in debug panelYesYes

Use resolvers when the value should be part of the form state (visible in debug panel, available to conditional logic).

Use inject when you only need the value at submit time and it doesn’t need to be in form state.


JSON Configuration

Hidden fields are fully serializable — no functions required:

{
  "fields": {
    "submitted_at": {
      "type": "hidden",
      "resolver": { "$resolver": "timestamp" }
    },
    "utm_source": {
      "type": "hidden",
      "resolver": {
        "$resolver": "urlParam",
        "param": "utm_source",
        "fallback": "direct"
      }
    },
    "visitor_ip": {
      "type": "hidden",
      "resolver": {
        "$resolver": "ip",
        "fallback": "unknown"
      }
    }
  }
}

This makes hidden fields compatible with the visual form builder, which stores config as JSON.


Custom Resolvers (Code Only)

For programmatic configs (not JSON-serializable), you can use the custom resolver:

// Only works in code — not serializable to JSON
const resolver: FieldResolver = {
  $resolver: 'custom',
  fn: () => crypto.randomUUID(),
};

Note: Custom resolvers cannot be used in the visual form builder since they require runtime functions. Use SerializableFieldResolver (which excludes custom) for builder-compatible configs.


TypeScript Types

import type { HiddenFieldProps, FieldResolver, SerializableFieldResolver } from '@saastro/forms';

// Full resolver union (includes 'custom')
type FieldResolver =
  | { $resolver: 'timestamp' }
  | { $resolver: 'hostname' }
  | { $resolver: 'pageUrl' }
  | { $resolver: 'referrer' }
  | { $resolver: 'userAgent' }
  | { $resolver: 'urlParam'; param: string; fallback?: string }
  | { $resolver: 'ip'; endpoint?: string; fallback?: string }
  | { $resolver: 'custom'; fn: () => unknown };

// Serializable subset (for form builder / JSON configs)
type SerializableFieldResolver = Exclude<FieldResolver, { $resolver: 'custom' }>;

// The hidden field config
interface HiddenFieldProps extends BaseFieldProps {
  type: 'hidden';
  resolver: SerializableFieldResolver;
}

API Reference

resolveValue(resolver)

Async function that executes a resolver and returns its value.

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

const value = await resolveValue({ $resolver: 'timestamp' });
// → "2026-02-25T12:34:56.789Z"

resolveValueSync(resolver)

Synchronous version for debug panel dry-runs. Returns placeholder strings for async resolvers (ip returns "[async: ip]").

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

const value = resolveValueSync({ $resolver: 'timestamp' });
// → "2026-02-25T12:34:56.789Z"

const ipValue = resolveValueSync({ $resolver: 'ip' });
// → "[async: ip]"

useHiddenFieldResolvers(methods, fields)

React hook that resolves all hidden field values on mount.

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

// Used internally by Form component — you don't need to call this directly
useHiddenFieldResolvers(formMethods, formConfig.fields);

BUILTIN_RESOLVERS

Constant array with metadata for all 7 built-in resolvers (used by the form builder UI).

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

// [
//   { id: 'timestamp', label: 'Timestamp', description: 'Current date/time in ISO format' },
//   { id: 'hostname', label: 'Hostname', description: 'Current page hostname' },
//   ...
// ]