Skip to content

Latest commit

Β 

History

156 Commits

Folders and files

Repository files navigation

form0-react-native

NPM Version NPM Downloads CI NPM License Docs Website NPM Last Update Socket

Note

form0 is in active development and is available to use today. Its schema format and core concepts are stable in practice, but releases before 1.0 may include breaking changes. Pin your versions and review the release notes when upgrading. A formally stable release is coming.

form0-react-native is the React Native UI layer for the form0 ecosystem. It combines form0-core with platform-native field renderers for iOS and Android while allowing an application to provide its own components, theme, media handling, and image resolution.

πŸš€ Start with the CLI

For a new project, install form0-cli and follow the quickstart. Choose the mobile application option to start from the maintained Expo template.

Install form0-react-native directly when integrating it into an existing React Native project.

πŸ“¦ Installation

npm install form0-react-native

Install compatible versions of the package's React Native peer dependencies. In an Expo project, use npx expo install so Expo selects compatible native versions.

⚑ Quick example

import { FormRenderer } from 'form0-react-native';

const schema = {
  form: {
    name: 'Inspection',
    status_field: null,
    elements: [
      {
        type: 'TextField',
        key: 'site_name',
        data_name: 'site_name',
        label: 'Site name',
        display: 'default',
        description: null,
        description_mode: null,
        required: true,
        required_conditions: null,
        visible: true,
        visible_conditions: null,
        read_only: false,
        read_only_conditions: null,
        default_value: null,
        pattern: null,
        pattern_description: null,
        supporting_image: false,
        supporting_image_path: null,
        supporting_image_display: null,
      },
    ],
  },
};

export function InspectionForm() {
  return (
    <FormRenderer
      schema={schema}
      engineOptions={{ events: { recordKind: 'new' } }}
      onSubmit={(record) => console.log(record)}
    />
  );
}

Renderer overrides

FormRenderer accepts a renderers prop so applications can replace or extend field components without forking the package:

const renderers = {
  PhotoField: CustomPhotoField,
  VideoField: CustomVideoField,
};

<FormRenderer
  schema={schema}
  engineOptions={{ events: { recordKind: 'new' } }}
  renderers={renderers}
/>;

Important

PhotoField and VideoField use placeholder-backed defaults. Production applications should provide renderers that own capture, storage, upload, and permission handling for those fields.

SignatureField has a built-in native renderer and can also be overridden. The package includes a mobile navigation and validation sheet, repeatable-section drilldown, theme overrides, and an image resolver integration point.

Compatibility note for 0.3.0

Repeatable-row editors no longer dispatch load-record, edit-record, or change automatically on their row engine. Earlier versions dispatched them when opening/editing a row or changing a row field. Automatic main-record dispatch is preserved: the root engine still receives load-record when ready, edit-record when entering edit mode, and change after a root field edit.

If your application relied on the previous automatic row-engine dispatch, review its handlers before upgrading. Explicit engineOptions.events.lifecycle: false preserves that legacy behavior. Use the managed integration described below for the portable repeatable-event contract tracked in core #98 and #102.

βœ… Requirements

  • Node.js 22 or newer
  • React 18 or 19
  • React Native 0.72 or newer
  • Compatible lucide-react-native, react-native-svg, and optional safe-area dependencies

See the React parity tracker for current renderer coverage and planned work.

πŸ“š Documentation

πŸ”’ Security

Schema expressions are evaluated by form0-core. Only use schemas from trusted authors and review the form0-core security policy. Report vulnerabilities according to this repository's security policy.

🀝 Support and contributing

See SUPPORT.md for help and CONTRIBUTING.md to contribute.

πŸ“„ License

MIT

Engine options and calculation diagnostics

engineOptions forwards helpers, security, onWarning, onUpdate, onOperations, and calculation diagnostic controls to root and repeatable-row engines. Helpers and security are engine-construction settings: remount FormRenderer to replace them predictably. Callbacks and the console preference stay current in already-open editors without an extra evaluation.

const findingsByScope = new Map();

<FormRenderer
  schema={schema}
  engineOptions={{
    events: { recordKind: 'existing' },
    diagnostics: { console: false },
    onDiagnostics(snapshot, meta) {
      if (meta.kind === 'scope-ended') findingsByScope.delete(meta.scopeId);
      else findingsByScope.set(meta.scopeId, { snapshot, meta });
    },
  }}
/>;

Console reporting is enabled by default. diagnostics.console: false suppresses calculation details, recovery and scope-ended console notices; collection and callbacks remain active. Providing onDiagnostics does not implicitly disable console reporting. Core codes, severity, messages, phases, suggestions and context are preserved. Developer console headings use English, as with the existing binding messages.

onDiagnostics(snapshot, meta) receives a defensive array after each accepted completed evaluation, including the first, identical findings and [] after recovery. Console output suppresses identical repeats and reports new/changed findings, recovery and recurrence. Errors from diagnostic observers, including rejected promises, cannot interrupt evaluation or operation processing.

Metadata contains scopeId, instancePath, evaluationId and kind. A scope identifies one transient engine lifetime; evaluation IDs increase within that scope. Root paths are []. Each row path segment contains repeatableKey, instanceId, zero-based index, and identity: "id" | "position". Existing id or record_id values are used, with an explicitly positional fallback when absent. Findings carry the same path in context.instancePath; reporting never creates record IDs.

Closing/deleting an editor, replacing its engine/schema, or unmounting retires the old scope with onDiagnostics([], { ...meta, kind: "scope-ended" }). This means the scope is no longer observed, not that the saved row recovered. Only engines that actually evaluate are represented: the callback is not a complete submission-health report and does not re-evaluate closed rows.

Calculation diagnostics remain outside state.errors, saved snapshots and record payloads. They do not change validation display timing, submission decisions or draft saving, and do not create automatic alerts or a diagnostics panel.

onOperations(operations, meta, applyDefault) receives non-alert operations. Delegate selected operations with applyDefault(selected, meta); without a callback, existing default processing remains active. Renderer-owned ALERT dialogs appear once. Operation batches follow the existing platform processing; callbacks must handle every supplied batch. onWarning remains the legacy warning route, including its existing throttling, and is not a replacement for diagnostic snapshots. onUpdate(state, engine) retains the execution-specific engine/facade argument; use onDiagnostics for portable diagnostic consumption.

Managed events and save preparation

The managed event integration uses form0-core 0.8.1 or later. Lifecycle events default on. The host must supply events.recordKind; missing classification is a configuration error. Set lifecycle: false explicitly to preserve legacy root load/edit/change behavior.

<FormRenderer
  schema={schema}
  engineOptions={{
    events: {
      recordKind: 'existing', // explicitly 'new' or 'existing'; never inferred from values
    },
    onEventController: (controller) => {
      controllerRef.current = controller;
    },
  }}
/>

onEventController(controller | null) receives the current engine generation after initialization. Callbacks can be replaced while editors are open. Lifecycle settings and record classification are fixed for the editor session; remount to replace them, helpers or security configuration. Observer exceptions and rejected promises are isolated.

Both controller methods return promises in every execution mode:

const detail = await controller.dispatch({
  type: 'validate-record',
  metadata: { isDraft: false, isValid: true },
});
// { status: 'completed' | 'failed', operations, validationIssues, issues }

const prepared = await controller.prepareSave({ intent: 'submit' });
if (prepared.eligible) await persistRecord(prepared.payload, prepared.meta);
// prepareSave never persists, reports persistence success, or closes an editor.

prepareSave waits for accepted updates, validates, runs the pre-save hook once when policy permits, awaits delegated operations, revalidates if editable values changed, then serializes that same final snapshot. Built-in actions use this path when lifecycle integration is enabled; application buttons can use the controller directly. Requests are serialized; concurrent preparation for the same scope returns save_in_progress. Applications own deduplication of their subsequent persistence requests. A failure returns eligible: false, payload: null and structured issues, preserving editable values for correction. Normal submit and row acceptance respect validation failures. Root draft preparation (intent: 'draft') remains allowed with validationSummary.hasErrors: true; row drafts are unsupported. Root submit returns the structured record, root draft the existing raw snapshot, and row preparation the existing row shape. meta retains submission/draft metadata.

ON('validate-record', function (event) {
  if ($amount < 4) INVALID('Amount must be at least four.');
});
ON('save-record', function (event) {
  SETVALUE('note', event.isDraft ? 'Draft reviewed' : 'Ready');
});

INVALID(nonemptyMessage) is synchronous and validation-only. Findings are scope-level messages in the existing validation summary/navigation; they have no invented field association and never enter intrinsic state.errors, calculation diagnostics, saved values or record/media metadata. Every validation attempt replaces custom findings; editable changes or scope retirement clear them. Validation and cancel/unload handlers reject value/UI operations and promises. isDraft describes the current attempted action; isValid describes intrinsic validity, with combined validity in the returned summary. Calculation diagnostics alone never block saving.

Managed opening evaluates effective values, runs load, then new/edit when entering editing; readonly opening runs load only. Rows use their actual new/existing classification and repeatable events. User field edits notify change after updates settle; accepted row membership changes carry action (add, edit, remove) and affectedInstancePath on the owning repeatable listener. Enabled status fields notify change-status for user changes only. Defaults, SETVALUE and calculations emit no additional automatic change. Interaction subscriptions default on; set fieldInteractions: [] to opt out. Built-in text/numeric inputs emit focus and blur; choice controls use change. Automatic click is reserved for supported activation fields. There is currently no HyperlinkField and no built-in control automatically emits click. Explicit host controller dispatch remains available.

For a row request, supply instancePath or the active scopeId from diagnostics metadata. Full paths use schema keys and existing row IDs, with explicit positional fallback; positional paths require that observed active scopeId. Old controllers, closed/deleted editors and obsolete generations are rejected. Open row handlers may read current main/ancestor values but SETVALUE writes only within that row. Collections and closed-row freshness/aggregation remain outside this contract.

Confirmed renderer cancellation sends cancel then unload; supported successful close paths send unload before releasing the engine. Host-owned closing must dispatch unload before unmounting. Forced disposal, resets, rerenders and Strict Mode cleanup only retire scopes. save-record and save-repeatable are pre-save hooks; there are no saved-* schema events. The application owns persistence, success notifications and closing. Async onOperations work is awaited; its selective applyDefault callback is valid only during that dispatch. Renderer alerts remain owned by the binding and are delivered once.

Native File/Blob media handles retain standard metadata and immutable binary content. Custom properties on these handles are outside native serialization; attach custom metadata to the surrounding plain record object. Async schema handlers remain unsupported. The form0-core 0.5.1 minimum includes the media compatibility correction.

Repeatable entry opening mode

repeatableEntryMode accepts 'readonly' (default) or 'edit' for existing rows at every nesting level. View mode offers Edit when the containing record permits editing. New rows start in edit mode. Read-only parents allow browsing descendants but prevent editing, adding and removing them; repeatableEntryMode="edit" does not override that restriction. This is a renderer option, with no schema or saved-record change.

View mode displays supplied accepted calculations and evaluates conditions and intrinsic validation without recalculating or applying missing defaults. Edit starts calculations using current permitted ancestor values. For accepted child totals 110/220 and root 330, viewing keeps those values; editing line 1 computes 120, saving it produces root 340, and cancelling keeps root 330. Unsaved selections after persistence failure remain working values when switching back to View.

Managed opening awaits load, then new/edit for direct editing. View β†’ Edit runs edit without replaying load. Handlers receive event.mode: 'readonly' | 'edit' for their executing scope. View permits reads, ALERT and engine-lifetime ON/OFF, but rejects a complete operation batch containing record writes before host delegation. Guard editable-load setters or move writable initialization to new/edit. Validation/close phases retain stricter restrictions. These capabilities require the coordinated core release containing engine mode control and readonly scoped evaluation; no schema or record migration is introduced.

Programmatic value updates

Official setters and default SETVALUE operation application share core normalization. SingleChoice and Boolean accept a string choice ID or { choice, other }; MultiChoice accepts string IDs in an array or { choices, other }. Full live objects preserve labels and metadata. Schema choices and submitted record formats remain unchanged. Updates do not implicitly dispatch form events.

Malformed calls reject before applying a value batch. Input aliases and numeric/boolean choice-ID coercion are intentionally no longer supported. Calculated/display/container targets, cross-scope writes and setter/event writes in record readonly mode are rejected. Host restoration and authoritative overrides remain separate loading paths. See the value-update contract.

Save failures and submission logging

Save failures are reported to console.error by default, with the failure stage and original error. Failed persistence callbacks keep edits available for retry and are not acknowledged as successful saves. Your application owns user-facing feedback: after catching a failed save to show a message, rethrow the error. A fulfilled callback counts as success.

<FormRenderer
  schema={schema}
  engineOptions={{ events: { recordKind: 'new' } }}
  submissionDiagnostics={{ saveFailures: true, validationBlocks: false }}
  onSubmit={saveRecord}
/>

Set saveFailures: false if your application already reports failures. Set validationBlocks: true to log blocked Submit and repeatable-row Save attempts, such as an empty required field. Validation feedback remains visible with logging off. These preferences stay current without rebuilding the engine, including during a pending save; they are separate from debug, calculation diagnostics and lifecycle settings. Reports do not attach form values, snapshots or record paths. Preparation failures include issue codes/messages; cancelling a confirmation stays quiet. Exceptions from your callback, including a rejected cancellation, are reported as callback failures. Existing core event messages have their own behavior.

The settings apply to built-in renderer actions. Applications using prepareSave() directly must inspect and report the returned result themselves.

The same controls cover draft onSave failures. Invalid drafts remain saveable and successful draft saves do not produce validation-block messages.

Managed event failure reporting

eventDiagnostics controls managed event failure reporting separately from submissionDiagnostics:

<FormRenderer
  schema={schema}
  engineOptions={{ events: { recordKind: 'existing' } }}
  eventDiagnostics={{ failures: true }}
  submissionDiagnostics={{ saveFailures: true, validationBlocks: false }}
/>

Failed managed events report once to console.error by default, including rejected SETVALUE() operations and rejected onOperations promises. Reports contain the event type, triggering field, root/row scope and issue codes/messages, without attaching values, operation arguments, snapshots or record IDs. Set failures: false when your application handles these reports. The preference stays current in open editors and pending operations without rebuilding or evaluating the engine.

Normal field validation and INVALID() findings remain separate. Cancelled or retired work stays quiet. Built-in Submit and row Save preparation use the submission reporter instead of emitting a second event report. Hosts calling prepareSave() directly still inspect its returned result. dispatch() continues returning its structured result even when console reporting is disabled. Existing core warnings and legacy lifecycle: false console behavior are unchanged.

Numeric fields still accept numbers: SETVALUE("quantity", 1) is valid. Choice fields require string IDs: SETVALUE("rating", "1") for single selection and SETVALUE("tags", ["1"]) for multiple selection. A rejected call leaves its operation batch unapplied; reporting does not change values, validation, submitted records or evaluation counts.

Releases

Sponsor this project

Packages

Used by

Contributors

Languages