Skip to main content
Decorators are clickable indicators (icon + label) attached to a field, column, row, or cell. Clicks are delivered through onFocus so your app can run custom logic — navigation, uploads, help modals, etc.

Decorator model

PropertyTypeNotes
actionstringRequired. Non-empty. Unique within its scope (path).
iconstring?See Supported icons.
labelstring?Text label.
colorstring?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.

Setup

Import DecoratorManager from @joyfill/components and pass an instance to <JoyDoc> via the decoratorManager prop:
import { JoyDoc, DecoratorManager } from '@joyfill/components';

const decoratorManager = new DecoratorManager();

<JoyDoc
  decoratorManager={decoratorManager}
  doc={doc}
  mode="fill"
/>
All four API methods are called directly on the decoratorManager instance.

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.
const fieldPath = `${pageId}/${fieldPositionId}`;

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 wantPath suffixExample
Common decorators on every row/rowspageId/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:
What you wantPath shapeExample
Common rows in any schemapageId/fpId/schemas/sk/rowspageId/fpId/schemas/addresses/rows
Common columns in any schemapageId/fpId/schemas/sk/columns/colIdpageId/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 wantPath shapeExample
Specific nested row…/rowId/schemas/sk/nestedRowIdpageId/fpId/p_alice/schemas/addresses/addr_home
Specific nested cell…/rowId/schemas/sk/nestedRowId/colIdpageId/fpId/p_alice/schemas/addresses/addr_home/col_zip
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 on DecoratorManager:
const path = `${pageId}/${fieldPositionId}`;

decoratorManager.getDecorators(path);
// → Decorator[]

decoratorManager.addDecorators(path, [
  { action: 'help', icon: 'circle-info', color: '#3B82F6' }
]);

decoratorManager.updateDecorator(path, 'help', {
  action: 'help',
  icon: 'star',
  color: '#3B82F6'
});

decoratorManager.removeDecorator(path, 'help');
The same four methods work for every path scope. A few examples:
// Common row decorators on a table — applied to every row
decoratorManager.addDecorators(`${pageId}/${fpId}/rows`, [
  { action: 'duplicate', icon: 'copy', label: 'Duplicate' }
]);

// Override on a specific row
decoratorManager.addDecorators(`${pageId}/${fpId}/${rowId}`, [
  { action: 'archive', icon: 'folder', color: '#6B7280' }
]);

// Cell-specific decorator
decoratorManager.addDecorators(`${pageId}/${fpId}/${rowId}/${colId}`, [
  { action: 'upload', icon: 'paperclip', label: 'Attach' }
]);

// Nested collection row
const nestedPath = `${pageId}/${fpId}/${parentRowId}/schemas/${nestedSchemaKey}/${nestedRowId}`;
decoratorManager.addDecorators(nestedPath, [
  { action: 'comment', icon: 'comment', color: '#10B981' }
]);

Handling clicks

Decorator clicks come through onFocus. When params.type is non-empty (and not 'fieldPositionFocus'), the focus event is a decorator tap — the value of params.type is the decorator’s action. Use params.fieldRowId and params.fieldColumnId to know exactly where the user clicked.
<JoyDoc
  decoratorManager={decoratorManager}
  doc={doc}
  mode="fill"
  onFocus={(params, e) => {
    if (params.type && params.type !== 'fieldPositionFocus') {
      // Decorator tap
      console.log('Decorator action:', params.type);
      console.log('Field position:', params.fieldPositionId);
      console.log('Row:', params.fieldRowId);
      console.log('Column:', params.fieldColumnId);
    } else {
      // Ordinary field focus
    }
  }}
/>
See Event handling for the full onFocus parameter reference.

Behavior to know

  • Collection license gating. Writes against a collection field require a license that enables collection features. Without it, the call emits decoratorError and is rejected.
  • Decorator visibility in PDFs. Decorators are hidden in readonly and pdf modes and are only clickable in fill mode.

Errors

All four APIs report errors through the onError event handler:
  • 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 <JoyDoc> via the decoratorManager, controls how many decorators render inline before the rest collapse into a kebab menu.
const decoratorManager = new DecoratorManager({
  visibleLimitInFields: 2,  // field + column scopes; default 2
  visibleLimitInRows: 1     // row scopes; default 1
});

<JoyDoc decoratorManager={decoratorManager} doc={doc} mode="fill" />

Supported icons

The SDK supports the following named icons: 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, pencil, pen-to-square. Unknown names fall back to a default icon.