diff --git a/docs/guides/choice-lists.md b/docs/guides/choice-lists.md index 2bee3b0..7962c83 100644 --- a/docs/guides/choice-lists.md +++ b/docs/guides/choice-lists.md @@ -68,6 +68,20 @@ Example structure: Use **snake_case** names for lists (`yes_no`, `region_list`, `priority_level`). +### How shared lists render on device + +By default, a plain `oneOf` / `$ref` field renders as a **native HTML ``** (keyboard-free, reliable in Formulus WebViews). Opt back into the searchable Autocomplete with `"autocomplete": true`. + +| Option | Values | Applies to | +|--------|--------|------------| +| `display` | `"radio"` \| `"buttons"` | Single-select | +| `display` | `"checkboxes"` \| `"buttons"` | Multi-select (`type: array` with `uniqueItems`) | +| `orientation` | `"vertical"` (default) \| `"horizontal"` \| `"flow"` | Radio, checkbox, and button modes | +| `buttonGroup` | `"segmented"` (default) \| `"separated"` | Button modes | +| `autocomplete` | `true` | Single-select — use searchable Autocomplete instead of native select | +| `placeholder` | string | Placeholder for native select (e.g. `"Select…"`) | + +**Tap-to-clear:** In `radio` and `buttons` single-select modes, tapping the already-selected option clears the answer. + +**Example — horizontal Yes/No buttons:** + +```json +{ + "type": "Control", + "scope": "#/properties/consent", + "options": { + "display": "buttons", + "orientation": "horizontal" + } +} +``` + +### Sticky fields + +Set `"sticky": true` to remember the last submitted scalar value for that field. Values are stored in device `localStorage`, keyed by form type + version + field path, and applied as the **lowest-precedence** default on **new** observations only (not edits, drafts, or sub-observation sessions). + +```json +{ + "type": "Control", + "scope": "#/properties/interviewer_id", + "options": { "sticky": true } +} +``` + +### Dynamic schema defaults + +For new observations, Formplayer resolves a small set of **default tokens** in `schema.json` property `default` values (only when the field is still empty after params / `defaultData`): + +| Token | Resolves to | +|-------|-------------| +| `$today` | Local calendar date `YYYY-MM-DD` (`format: "date"`) | +| `$now` | ISO 8601 date-time (`format: "date-time"`) | + +Static `default` literals are unchanged. + +### Deferred validation + +New observations start with validation errors hidden (`ValidateAndHide`). Errors appear after the user navigates forward or reaches Finalize. Edits and draft resumes show validation immediately. Override via `params.validationMode`: `"ValidateAndShow"`, `"ValidateAndHide"`, or `"NoValidation"`. + +### Number bounds + +`minimum` / `maximum` are **validation constraints**, not per-keystroke clamps. Users can type out-of-range values; AJV surfaces errors on blur and at finalize. Stepper +/- buttons still respect bounds. ## Working with Media & Special Field Types @@ -934,6 +1010,28 @@ Show field when boolean is true: } ``` +#### Multi-select choice check + +For **array** multi-select fields, use `contains` with `const` (not `const` on the array itself): + +```json +{ + "type": "Control", + "scope": "#/properties/other_symptoms", + "rule": { + "effect": "SHOW", + "condition": { + "scope": "#/properties/symptoms", + "schema": { + "contains": { "const": "other" } + } + } + } +} +``` + +Custom question types (`format` renderers) and the root `SwipeLayout` element also honour `SHOW` / `HIDE` rules. + ### Unsafe Patterns #### ❌ Rule Referencing Missing Field diff --git a/docs/reference/formplayer-contract.md b/docs/reference/formplayer-contract.md index 4271dc2..69b2d36 100644 --- a/docs/reference/formplayer-contract.md +++ b/docs/reference/formplayer-contract.md @@ -303,6 +303,21 @@ addFormats(ajv); // Standard format validators (date, email, etc.) - Each element in `elements` becomes a page - Progress indicator shows current page - Navigation buttons (Previous/Next) provided automatically +- Root `SwipeLayout` honours `SHOW` / `HIDE` rules (same as nested pages) +- Default `labelLayout` is `"inline"` (compact two-column rows); set `"stacked"` for classic layout +- Optional `headerFields` (up to two schema keys) show read-only context under the progress bar +- `showInnerTitle` defaults to `false` (form title lives in Formulus chrome unless opted in) + +**SwipeLayout `options`:** + +| Option | Values | Notes | +|--------|--------|-------| +| `labelLayout` | `"inline"` \| `"stacked"` | Default `"inline"` | +| `headerFields` | `string[]` | Max 2 field keys | +| `showInnerTitle` | `boolean` | Default `false` | +| `autoFocusFirstInput` | `boolean` | Default `true` | +| `nextButtonLabel` | `string` | Custom Next label | +| `finalizeButtonLabel` | `string` | Last content page label | #### VerticalLayout @@ -394,9 +409,22 @@ addFormats(ajv); // Standard format validators (date, email, etc.) **Optional:** - `label`: String or `false` (to hide label) -- `options`: Object with renderer-specific options +- `options`: Object with renderer-specific options (see below) - `rule`: Conditional display/enable rule +**Common `options`:** + +| Option | Values | Description | +|--------|--------|-------------| +| `labelLayout` | `"inline"` \| `"stacked"` | Per-field layout override (inherits SwipeLayout default) | +| `sticky` | `true` | Remember last submitted scalar value for new observations | +| `display` | `"radio"` \| `"buttons"` \| `"checkboxes"` | Choice layout (single- or multi-select) | +| `orientation` | `"vertical"` \| `"horizontal"` \| `"flow"` | Choice layout direction | +| `buttonGroup` | `"segmented"` \| `"separated"` | Button-group styling | +| `autocomplete` | `true` | Searchable Autocomplete for single-select (default is native `` dropdown by default for `oneOf` / `$ref` lists; optional Autocomplete (`options.autocomplete`), radio, or button groups +- **Multi Select**: Vertical checkboxes by default; optional checkbox or button groups with `options.display` + +### Form UX (2026) + +- **Inline layout**: SwipeLayout forms default to compact two-column rows (`labelLayout: "inline"`) +- **Sticky fields**: Opt-in per-control value memory (`options.sticky`) +- **Deferred validation**: New forms hide errors until first forward navigation +- **Sub-observation fast path**: `skipFinalize` skips the Finalize page for nested child forms +- **Dynamic defaults**: Schema `default: "$today"` / `"$now"` for new observations + +See [Form design guide](../guides/form-design) for `ui.json` examples. ### Boolean diff --git a/docs/reference/formulus.md b/docs/reference/formulus.md index 30cc057..bbfe08d 100644 --- a/docs/reference/formulus.md +++ b/docs/reference/formulus.md @@ -181,15 +181,62 @@ const observations = await api.getObservationsByQuery({ **Returns:** Promise resolving to an array of observations -#### sync() +#### sync(options?) Trigger manual synchronization. ```javascript -await api.sync(); +const { version } = await api.sync(); +// Optional: include attachments (slower) +await api.sync({ includeAttachments: true }); ``` -**Returns:** Promise that resolves when sync completes +**Returns:** `Promise<{ version: number }>` — the server's data revision after sync completes. + +#### getConnectivityStatus() + +Probe whether the configured Synkronus server answers `GET /health`. Never rejects for offline devices — returns `{ online: false }`. + +```javascript +const status = await api.getConnectivityStatus(); +// { online: boolean, serverUrl: string | null, checkedAt: number } +``` + +Use for "verify when online, fall back when offline" workflows in custom apps. + +#### getCurrentDataRevisionCount() + +Read the device's last-known Synkronus data revision (`current_version` from the most recent successful sync). + +```javascript +const revision = await api.getCurrentDataRevisionCount(); // number, 0 if never synced +``` + +Reflects **server-stream alignment only** — not unsynced local edits. Poll after `sync()` or on an interval to detect remote changes from other devices. + +#### persistObservation(input) + +Persist an observation **without opening Formplayer** (headless write). Uses the same path as a Formplayer submit. + +```javascript +const result = await api.persistObservation({ + formType: 'survey', + finalData: { name: 'Ada', age: 30 }, + observationId: null, // omit or null to create; provide id to update +}); +// { observationId, formData } +``` + +#### openFormplayer options + +When opening forms programmatically, `openFormplayer` accepts: + +| Option | Description | +|--------|-------------| +| `subObservationMode` | Nested child form for embedded sub-observations | +| `skipFinalize` | Skip Finalize page; auto-submit from last content page | + +**Form init `params` reserved keys** (not persisted as observation data): `defaultData`, `theme`, `darkMode`, `themeColors`, `context` (read-only session context exposed in Formplayer as `window.formulusSessionContext`), `validationMode`. ## Database Schema