Documentation Index
Fetch the complete documentation index at: https://docs.joyfill.io/llms.txt
Use this file to discover all available pages before exploring further.
Decorators are tappable indicators (icon + label) attached to a field, column, row, or cell. Taps are delivered through onFocus so your app can run custom logic — navigation, uploads, etc.
Decorator model
import JoyfillModel
var d = Decorator()
d.action = "openHelp" // required, unique within scope
d.icon = "circle-info" // optional, see Supported icons
d.label = "Help" // optional
d.color = "#3B82F6" // optional, must be #RRGGBB
| Property | Type | Notes |
|---|
action | String | Required. Non-empty. Unique within its scope (path). |
icon | String? | See Supported icons. |
label | String? | Text. |
color | String? | 6-digit hex (#RRGGBB). |
A decorator renders only when it has a non-empty icon or label. Action-only entries are stored but not displayed.
Constructing a path
Every path starts with pageId/fieldPositionId. What you append after that determines what gets decorated.
Reserved keywords. The path grammar uses three reserved tokens — schemas, rows, and columns. Anything else in a path slot is treated as an id (page id, field-position id, row id, column id, or schema key). Don’t use these keywords as ids.
Field decorators
Just the two ids. Applies to the field’s header.
let fieldPath = "\(pageId)/\(fpId)"
Table — /rows, /columns/colId, or specific rowId / rowId/colId
A table has four decorator scopes, two common (defaults applied everywhere) and two specific (overrides for one row or cell):
| What you want | Append | Example |
|---|
| Common decorators on every row | /rows | pageId/fpId/rows |
| Decorators on one specific row | /rowId | pageId/fpId/row_42 |
| Common decorators on every cell in a column | /columns/colId | pageId/fpId/columns/col_status |
| Decorators on one specific cell | /rowId/colId | pageId/fpId/row_42/col_status |
Specific paths inherit from the matching common path on the first write — anything you set on /rows shows on row_42 until you write to row_42 directly.
Collection — same as table, plus /schemas/schemaKey/… for nested rows
A collection’s root rows behave like a table — the four scopes above use the exact same path shapes.
Take a “People” collection where each person row holds a nested “Addresses” schema:
schema "people" (root, children: [addresses])
schema "addresses" (nested under people)
Rows:
p_alice ← row in "people"
addresses → [ addr_home, addr_work ] ← rows in "addresses", under p_alice
p_bob
addresses → [ addr_apt ]
Common rows / columns of any schema — root or nested — are schema-level defaults. Address them directly with schemas/schemaKey/…, no parent walk needed:
| What you want | Path shape | Example |
|---|
| Common rows in any schema | pageId/fpId/schemas/sk/rows | pageId/fpId/schemas/addresses/rows |
| Common columns in any schema | pageId/fpId/schemas/sk/columns/colId | pageId/fpId/schemas/addresses/columns/col_zip |
A specific nested row or cell lives under a particular parent. Walk through that parent’s row id, then schemas/sk/, then the nested row id:
| What you want | Path shape | Example |
|---|
| Specific nested row | …/rowId/schemas/sk/nestedRowId | pageId/fpId/p_alice/schemas/addresses/addr_home |
| Specific nested cell | …/rowId/schemas/sk/nestedRowId/colId | pageId/fpId/p_alice/schemas/addresses/addr_home/col_zip |
If addresses itself had children, you’d chain another schemas/.../rowId/… after addr_home — the same pattern repeats for every level.
Schema keys come from the field’s schema map. The schema marked root: true holds top-level rows; its children array names the nested schemas reachable from a row in this schema.
API
Four methods, all on DocumentEditor. Errors are reported via onError.
let fieldPath = "\(pageId)/\(fieldPositionId)"
editor.getDecorators(path: fieldPath) // -> [Decorator]
editor.addDecorators(path: fieldPath, decorators: [d])
editor.updateDecorator(path: fieldPath, action: "openHelp", decorator: updated)
editor.removeDecorator(path: fieldPath, action: "openHelp")
Same shape for every path scope. A few examples:
// Common row decorators on a table — applied to every row
editor.addDecorators(path: "\(pageId)/\(fpId)/rows", decorators: [duplicate])
// Override on a specific row
editor.addDecorators(path: "\(pageId)/\(fpId)/\(rowId)", decorators: [archive])
// Cell-specific decorator
editor.addDecorators(path: "\(pageId)/\(fpId)/\(rowId)/\(colId)", decorators: [upload])
// Nested collection row
let nestedPath = "\(pageId)/\(fpId)/\(parentRowId)/schemas/\(nestedSK)/\(nestedRowId)"
editor.addDecorators(path: nestedPath, decorators: [comment])
Behavior to know
-
Copy-on-write seed. First write to a row-self / cell scope seeds from the matching common scope, so existing common decorators stay visible on that row alongside your override. Subsequent writes diverge freely.
-
Collection license gating. Writes against a collection field require a license that enables collection features. Without it, the call emits
decoratorError and is rejected.
Handling taps
Decorator taps come through onFocus with the decorator’s action exposed on the field event’s type / target. rowIds / columnId / parentPath on FieldIdentifier tell you where the user tapped.
func onFocus(event: Joyfill.Event) {
guard let field = event.fieldEvent else { return }
if let action = field.type, !action.isEmpty {
// Decorator tap
print("Decorator:", action,
"field:", field.fieldID,
"rows:", field.rowIds ?? [],
"column:", field.columnId ?? "-")
} else {
// Ordinary field focus
}
}
See Event handling for the full focus/blur flow.
Errors
All four APIs report through onError as JoyfillError.decoratorError(DecoratorError):
- Path didn’t resolve (bad ids, deleted row, malformed grammar)
- Validation (
action empty, color not #RRGGBB)
- Duplicate
action in batch or against an existing entry
removeDecorator / updateDecorator with an unknown action
- Collection write without a valid license
Reads (getDecorators) on an unresolvable path also emit onError and return [].
Display limits
DecoratorConfig, passed to DocumentEditor at init, controls how many decorators render inline before the rest collapse into a kebab menu.
let config = DecoratorConfig(
visibleLimitInFields: 2, // field + column scopes; default 2
visibleLimitInRows: 1 // row scopes; default 1
)
let editor = DocumentEditor(document: doc, decoratorConfig: config)
Supported icons
The SDK maps common names to bundled artwork or SF Symbols, including: camera, import, paperclip, image, file, comment, comments, upload, download, rotate, cloud, filter, share, paper-plane, folder, folder-open, magnet, eye, circle-info, add, plus, print, flag. Unknown names fall back to a default symbol.