Monorepo: See also ../AGENTS.md for the full ODE map and cross-package contracts.
This file gives AI assistants and developers enough context to work effectively in this repo. For user-facing docs, see README.md.
- Formulus Formplayer is a web app that renders and submits forms using the JSON Forms spec. It is not a standalone site: it runs inside a WebView in the Formulus React Native app.
- The RN app (sibling repo
../formulus) loads the formplayer bundle from file:// (e.g.file:///android_asset/formplayer_dist/on Android). The formplayer is the “form UI”; the RN app handles sync (Synkronus), storage, and native capabilities (camera, GPS, etc.). - The formplayer exposes a JavaScript API (
window.formulus.formplayer) that the host and custom apps use to add or edit observations. The formplayer is initialized and configured by the Formulus app (renderers, cells, formSpecs, etc.); custom apps only call the API.
- Parent repo:
formulus-formplayerlives under a monorepo (e.g.ODE). Sibling projects include:formulus— React Native app that hosts the formplayer WebView and provides the native bridge.packages/tokens—@ode/tokens(design tokens; Style Dictionary).packages/components—@ode/components(shared UI; may use@ode/tokens).
- Install order (from repo root):
cd packages/tokens && pnpm installthencd formulus-formplayer && pnpm install && pnpm start.
Installing only in formulus-formplayer can break the tokenspreparescript.
- Scripts (from
formulus-formplayer/):pnpm run build—sync-interface→tsc→vite build(output:build/).pnpm run build:copy— build then copybuild/into Formulus (Android + iOS formplayer assets) and ODE Desktop (../desktop/public/formplayer_dist/). Alternatively, fromdesktop/only:pnpm copy:formplayer(requires an existingformulus-formplayer/build/).- Android:
../formulus/android/app/src/main/assets/formplayer_dist/ - iOS:
../formulus/ios/formplayer_dist/
- Android:
- Interface sync:
scripts/sync-interface.jscopies one shared TypeScript file from the Formulus app into the formplayer:
formulus/src/webview/FormulusInterfaceDefinition.ts→formulus-formplayer/src/types/FormulusInterfaceDefinition.ts.
So the single source of truth for the bridge contract is in formulus; formplayer consumes a copy. Runpnpm run sync-interface(orpnpm run build) when that file changes. - Vite:
vite.config.tsis tuned for WebView:base: './'so assets resolve under file://.- Single bundle (
inlineDynamicImports: true) so the WebView doesn’t fail loading multiple chunks. - No
crossoriginon script/link so file:// loading works. - Source maps enabled for debugging.
| Area | Purpose |
|---|---|
src/App.tsx |
Main app: JsonForms setup, renderer/cell registration, theme, init from FormInitData. |
src/index.tsx |
Entry: mounts React app; exposes React and MaterialUI on window for custom question type renderers. |
src/renderers/* |
JSON Forms renderers (e.g. signature, photo, sub-observation, file, GPS, swipe layout, finalize). Each has a tester (when to use) and a component. |
src/theme/ |
MUI theme from @ode/tokens via tokens-adapter.ts; material wrappers for consistent look. |
src/services/ |
FormulusInterface.ts (bridge client), DraftService, ExtensionsLoader, custom question type/validator loaders and registries. |
src/types/ |
FormulusInterfaceDefinition.ts (synced from formulus), CustomQuestionTypeContract.ts, etc. |
src/components/ |
Shared UI (e.g. QuestionShell, FormLayout, DraftSelector). |
src/builtinExtensions.ts |
Built-in extension functions (e.g. getDynamicChoiceList) used in forms. |
src/mocks/ |
webview-mock.ts and DevTestbed for local dev without RN. |
scripts/ |
sync-interface.js, copy-to-rn.js, clean-rn-assets.js. |
- WebView environment: No real
window.ReactNativeWebViewin browser dev; use the mock. Assume file:// and single JS bundle when changing Vite/build. - Bridge: Communication with RN is via postMessage and the contract in
FormulusInterfaceDefinition.ts. The formplayer usesFormulusClient(singleton) inFormulusInterface.tsto call native (camera, signature, submit, etc.). - Custom question types: Loaded from a manifest (source strings) from the RN app, evaluated in a sandbox with
ReactandMaterialUIonwindow. They use format in the schema (e.g."format": "signature"), not onlytype. Contract:src/types/CustomQuestionTypeContract.ts. - Design tokens: Use
@ode/tokensviasrc/theme/tokens-adapter.tsand the theme insrc/theme/theme.ts; avoid hardcoding colors/spacing that exist in tokens. - Attachment-backed builtins (
photo,audio,video,select_file): Observation JSON stores basename-onlyfilenameplus portable metadata; RN writes files underattachments/draft/(etc.). Resolve previews withgetAttachmentUriwhere applicable.select_fileshows the chosen name only—no file preview.
- New question type (built-in)
Add a renderer insrc/renderers/with a tester (e.g.schemaMatches/rankWithonformat) and component; register it inApp.tsx(customRenderers). Register a matching AJVaddFormatwhen the schema uses a non-standardformatstring (seesub-observationinSubObservationQuestionRenderer.tsx+App.tsx). Sub-observation schema useslinkedForm(required), optionalparentKey(parent id injection on add), optionalitemLabel(singular entity name for add-button copy),subObservationInitValues(optional add prefill), andsubObservationEditInitValues(optional merge-on-edit). UI schemaoptions.addButtonLabeloverrides the composed add button. UseFormulusClientinFormulusInterface.tsfor bridge calls (e.g.openFormplayerfor nested sessions). - Custom validators
Loaded from bundlevalidators/viaCustomValidatorLoader. Validators referenced inui.jsonoptions.customValidatorsmay mutatedatain place;runCustomValidatorsAndRefreshDatainsrc/services/customValidatorDataRefresh.tsre-dispatches form state when mutations are detected (handleDataChangeandhandleFinalizeForminApp.tsx). Scope: validators apply to the current Formplayer session only — nested sub-observation child forms do not inherit parent validators (authoring: Custom Extensions — nested sessions). - Sub-observation (
SubObservationQuestionRenderer.tsx):skipFinalizeskips the Finalize page; child still validates on Done beforeformDatamerges into the parent array. See validation and skipFinalize. - Draft selector bypass
FormInitData.skipDraftSelection/openFormplayer(..., { skipDraftSelection: true })— seeshouldOfferDraftSelectorinsrc/utils/formObservationData.ts. - New question type (custom / from Synkronus)
Custom types are loaded byCustomQuestionTypeLoaderfrom the manifest; they must comply withCustomQuestionTypeContract.tsand export a default component. No change in formplayer code needed for new custom types that follow the contract. - New native capability (e.g. new “requestX” from RN)
- Extend the contract in formulus (
FormulusInterfaceDefinition.ts). - Run
pnpm run sync-interfacein formulus-formplayer. - Implement the client side in
FormulusInterface.tsand use it in the relevant renderer or service.
- Extend the contract in formulus (
- Build / bundle issues
Keep one main bundle; avoid dynamic imports that create extra chunks unless you’ve verified loading under file:// in the RN WebView. Keepbase: './'and the no-crossorigin plugin.
pnpm start— Vite dev server; useswebview-mockand (optionally)DevTestbedso you can test without the RN app.- Tests:
pnpm test(Vitest). Lint/format:pnpm run lint,pnpm run lint:fix,pnpm run format,pnpm run format:check.
Run from formulus-formplayer/ (CI runs the same steps):
pnpm run lint
pnpm run format
pnpm run format:check
pnpm run test run
pnpm run buildReact renderers: never put if (visible === false) return null (or any early return) before hooks. Call all hooks unconditionally at the top of the component, then return null when hidden. ESLint react-hooks/rules-of-hooks fails CI; this pattern is easy to miss when adding visible guards to renderers.
Imports: remove unused symbols (@typescript-eslint/no-unused-vars is an error).
If the PR also touches Formulus (../formulus/), run pnpm run lint there too (errors block; warnings are capped with --max-warnings 9999).
- Commit messages must follow Conventional Commits (e.g.
feat(scope): add X,fix(scope): resolve Y). - Before opening a PR, run the pre-flight block above (including
pnpm run formatso CIformat:checkdoes not fail on unformatted files). - PRs should use the following template:
- Bug Fix
- New Feature / Enhancement
- Refactor / Code Cleanup
- Documentation Update
- Maintenance / Chore
- Other (please specify):
- formulus (React Native mobile app)
- formulus-formplayer (React web app)
- synkronus (Go backend server)
- synkronus-cli (Command-line utility)
- Documentation
- DevOps / CI/CD
- Other:
Closes/Fixes/Resolves:
- Unit tests added/updated
- Integration tests added/updated
- Manually tested
- Tested on multiple platforms (if applicable)
- Not applicable
- This PR introduces breaking changes
- This PR does NOT introduce breaking changes
If breaking changes, please describe migration steps:
- Documentation has been updated
- Documentation update is not required
- Code follows project style guidelines
- All existing tests pass
- New tests added for new functionality
- PR title follows Conventional Commits format
Thank you for contributing to Open Data Ensemble (ODE)!
- Form init: RN sends
FormInitData(e.g. viaonFormInit); seeFormulusInterfaceDefinition.ts. - Submit: Use
FormulusClient.submitObservationWithContext(formInitData, finalData)so create vs update is correct. - Renderers: Use
QuestionShellfor consistent layout and use the theme/tokens (e.g.tokensfromtheme/tokens-adapter) for spacing and colors where applicable.
Using this file, an AI or new developer can reason about the formplayer’s role in the monorepo, where to change code for new features, and what not to break (single bundle, file://, bridge contract, tokens).