Complete technical reference for the Formplayer form rendering component.
:::info[Authoritative Reference Available]
For the complete, authoritative contract defining all supported JSON Schema features, UI schema structure, validation rules, and behavioral guarantees, see the Formplayer Contract.
:::
Formplayer is a React web application that renders JSON Forms and provides the dynamic form interface for data collection. It runs within WebViews in the Formulus mobile app and communicates with the native app through a JavaScript bridge.
Critical: ODE Formplayer intentionally supports a safe, predictable subset of JSON Schema and JSON Forms. Forms outside this profile may load but are not guaranteed to work.
| Feature | Supported | Notes |
|---|---|---|
type |
✅ | string, number, integer, boolean, object, array |
properties |
✅ | Object property definitions |
required |
✅ | Array of required property names |
minimum / maximum |
✅ | Literal numbers only (e.g., "minimum": 0) |
minLength / maxLength |
✅ | String length constraints |
pattern |
✅ | Regular expression patterns |
format |
✅ | email, date, date-time, uri, uuid |
enum |
✅ | Array of allowed values |
enumNames |
✅ | Display names for enum values |
oneOf |
✅ | Recommended for single-choice selections |
title |
✅ | Field display title |
description |
✅ | Field description/help text |
default |
✅ | Default values |
const |
✅ | Constant values (used in rules) |
$data |
❌ | Will crash - not supported |
if / then / else |
❌ | Not supported in rule conditions |
$ref |
⚠️ | Limited support - use with caution |
allOf / anyOf |
⚠️ | Limited support - use with caution |
| Element | Required Fields | Notes |
|---|---|---|
| SwipeLayout | type, elements[] |
Root layout (required or auto-wrapped) |
| VerticalLayout | type, elements[] |
Vertical field arrangement |
| HorizontalLayout | type, elements[] |
Horizontal field arrangement |
| Group | type, label, elements[] |
Grouped fields with label |
| Control | type, scope |
Field control (scope must exist in schema) |
| Label | type, text |
Text label element |
❌ JSON Schema:
$data references (dynamic values)if/then/else conditional schemas$ref resolutionallOf/anyOf (limited support)❌ UI Schema:
elements array in layoutsscope paths (referencing non-existent schema properties)Categorization layout (not recommended)⚠️ Common Pitfalls:
$data in minimum/maximum (use literal numbers)elements array in SwipeLayout or other layouts✅ Recommended Structure:
{
"schema": {
"type": "object",
"properties": {
"field1": { "type": "string", "title": "Field 1" },
"field2": { "type": "integer", "minimum": 0, "maximum": 100 }
},
"required": ["field1"]
},
"uischema": {
"type": "SwipeLayout",
"elements": [
{
"type": "VerticalLayout",
"elements": [
{
"type": "Control",
"scope": "#/properties/field1"
},
{
"type": "Control",
"scope": "#/properties/field2"
}
]
}
]
}
}
❌ Unsafe Patterns:
{
"schema": {
"properties": {
"field1": { "type": "string" }
}
},
"uischema": {
"type": "Control",
"scope": "#/properties/missingField" // ❌ Field doesn't exist
}
}
{
"schema": {
"properties": {
"value": {
"type": "number",
"minimum": { "$data": "#/minValue" } // ❌ $data not supported
}
}
}
}
formulus-formplayer/
├── src/
│ ├── App.tsx # Main application component
│ ├── FormLayout.tsx # Form layout renderer
│ ├── QuestionShell.tsx # Question wrapper component
│ ├── *QuestionRenderer.tsx # Question type renderers
│ ├── FormulusInterface.ts # Bridge interface definition
│ └── theme.ts # Theming configuration
├── public/
│ └── formulus-load.js # API loading script
└── build/ # Production build output
Formplayer is responsible for:
Formplayer is initialized by the Formulus app with:
┌─────────────────┐ ┌──────────────────┐
│ Formulus │ │ Formplayer │
│ (Native) │◄───────►│ (WebView) │
│ │ │ │
│ • Database │ │ • Form Render │
│ • Sync Engine │ │ • Validation │
│ • API Bridge │ │ • User Input │
└─────────────────┘ └──────────────────┘
Formplayer exposes methods to custom applications and receives configuration from Formulus.
Open a form to create a new observation.
window.formulus.formplayer.addObservation('survey', {
participantId: '123',
location: 'Field Site A'
});
Parameters:
formType (string): Form type identifierinitializationData (object): Optional pre-population dataOpen a form to edit an existing observation.
window.formulus.formplayer.editObservation('survey', 'obs-123');
Parameters:
formType (string): Form type identifierobservationId (string): Observation ID to editDelete an observation.
window.formulus.formplayer.deleteObservation('survey', 'obs-123');
Parameters:
formType (string): Form type identifierobservationId (string): Observation ID to deleteFormplayer supports various question types through custom renderers:
<select> dropdown by default for oneOf / $ref lists; optional Autocomplete (options.autocomplete), radio, or button groupsoptions.displaylabelLayout: "inline")options.sticky)skipFinalize omits the Finalize page; child still validates on Done before returning data to the parentparentKey: Sub-observation arrays require only linkedForm; parent id injection is optionaldata updates from bundle validators refresh the UI automaticallyopenFormplayer(..., { skipDraftSelection: true }) for orchestrated root sessionsdefault: "$today" / "$now" for new observationsSee Form design guide for ui.json examples.
Formplayer processes JSON schemas to:
Forms can use different layout strategies:
Formplayer validates form responses against:
Custom validation rules can be added:
ui.json → options.customValidators referencing validators/<name>/ modules in the app bundle (see Custom Extensions)data (for example auto-numbering repeat rows); Formplayer refreshes state when mutations are detectedFormplayer can save incomplete forms as drafts:
When opening a new root observation, Formplayer may show a draft selector if local drafts exist. Custom apps can bypass this with openFormplayer(formType, params, savedData, { skipDraftSelection: true }) when they orchestrate the session (for example after preparing defaultData programmatically). Sub-observation sessions and edits with savedData never show the picker.
When editing an observation:
Formplayer uses a theme system for styling:
Themes can be customized:
# Install dependencies (build @ode/tokens first)
cd ../packages/tokens && pnpm install && pnpm run build && cd ../formulus-formplayer
pnpm install
# Start development server
pnpm start
# Opens at http://localhost:3000 (or Vite's printed port)
# Build and copy into Formulus (and ODE Desktop)
pnpm run build:copy
# Build for web only
pnpm run build
The build process:
Custom applications can use Formplayer:
<script src="formulus-load.js"></script>
<script>
async function openForm() {
const api = await getFormulus();
await api.addObservation('survey', {});
}
</script>
The Formplayer API is injected into custom app WebViews:
// Access Formplayer methods
window.formulus.formplayer.addObservation('survey', {});
Formplayer includes core question type renderers:
TextQuestionRenderer: Text input fieldsNumberQuestionRenderer: Numeric input fieldsDateQuestionRenderer: Date pickerSelectQuestionRenderer: Dropdown selectionsPhotoQuestionRenderer: Camera captureGPSQuestionRenderer: Location captureSignatureQuestionRenderer: Digital signatureAudioQuestionRenderer: Voice recordingVideoQuestionRenderer: Video recordingFileQuestionRenderer: Generic file attachment (format: select_file, type: object) — document picker, basename persistence like photos; filename-only UI (no preview)SubObservationQuestionRenderer: Embedded sub-observation repeats (format: sub-observation) — nested openFormplayer with subObservationModeCustom renderers can be added:
Formplayer displays validation errors:
Error handling for:
pnpm starthttp://localhost:3000