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 Case | Example 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:
- On mount, it filters all fields with
type: 'hidden' - Runs all resolvers in parallel via
Promise.all - Sets each value with
react-hook-form’ssetValue(name, value, { shouldDirty: false }) - 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:
| Feature | Hidden Field Resolver | Field Mapping Inject |
|---|---|---|
| When it runs | Form mount (once) | Before submit action |
| Visible in form state | Yes (via setValue) | No (transform only) |
| Configurable per-field | Yes | Per submit action |
| Available in debug panel | Yes | Yes |
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 excludescustom) 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' },
// ...
// ]