Audience: Low‑code builders (Dynamic Content Editor) and developers wiring dynamic actions / elements.
App composer components often need lightweight shared state for itself, boardlet to boardlet communication (app scope) or app to app communication (global). In earlier iterations we used "global parameters" (added to the URL / shared via route state) for cross‑element exchange. That approach has limits:
| Legacy Global Parameters Limitation | Impact |
|---|---|
| Mostly primitive / flat values | Hard to pass rich objects (forced JSON stringify hacks) |
| Coupled to URL (exposed) | Internal keys visible & bookmarkable unintentionally |
| Risk of leaking internal / sensitive names | Needed obfuscation / naming discipline |
| Hard to scope (effectively global only) | Accidental cross‑app collisions |
| Navigation side‑effects | Changing a value could dirty browser history |
Data Hub solves these by offering an in‑memory, non‑URL, structured store with three explicit scopes and predictable precedence.
The Data Hub provides:
{{ someKey}} just works with local override semantics)ON_DATA_HUB_CHANGE so elements can react declaratively when relevant data changesUse it whenever you need temporary data sharing between actions or elements—especially for richer values you do NOT want in the page URL.
| Situation | Use Data Hub? | Scope | Rationale |
|---|---|---|---|
| Store last clicked table row for the same widget or filters used on by one boardlet | Yes | Local | Highest precedence, isolated by Content Id |
| Share a filter across several widgets within one micro‑app | Yes | App | Limited blast radius; survives page navigation inside the app |
| Share a settings across two apps (navigate to shared dashboard in another app) | Yes | Global | Survive page navigation between app |
| Large dataset / pagination cache | No | — | Use dedicated data service / API caching |
| Long‑term preferences | No | — | Persist via backend or local storage layer |
Rule of thumb: Start as Local, promote to App only when two or more contents collaborate, promote to Global only if truly cross‑app.
Three maps exist simultaneously:
appKey)contentId)When something on the page runs, the system builds a usable "context" made from Global, then App, then Local data (Local beats App, App beats Global if the same key exists). So if selection exists in Local, that one shows; otherwise it falls back to App, then Global. You can still explicitly read each scope: local.selection, app.selection, global.selection.
Additional always‑present helper keys:
| Key | Description |
|---|---|
contentId |
Current dynamic content instance id |
appKey |
Current application key (micro‑app) |
global |
Full global map |
app |
App map for current appKey |
local |
Local map for current contentId |
Chain Dynamic Actions:
| Action (Editor) | Scope Impact |
|---|---|
| Local Data Hub: Set Value | Mutates Local map for target contentId (calculated automatically) |
| App Data Hub: Set Value | Mutates App map for inferred appKey |
| Global Data Hub: Set Value | Mutates Global map |
Each Set action defines a Parameters Map (rows). For each row you specify: Target Key, Source Type, Value/Expression.
| Source Type | Example Input | What You Get |
|---|---|---|
| Plain text / number / boolean | true |
true |
| Text with placeholders | User: {{global.currentUser.name}} |
Replaced with actual user name |
| Existing Value | New Value | Result |
|---|---|---|
| (absent) | any | Created |
| Plain object | Plain object | Shallow merge (new props overwrite) |
| Anything else | anything | Replacement |
Tip: Replacing an object but keeping stale nested fields? First set the key to null, then set the new object.
{{ selection.rowId }} or explicit {{ local.selection.rowId }}ON_DATA_HUB_CHANGE lets an element automatically fire an action chain whenever any Data Hub value changes (Local, App, or Global). Add it only where you need automatic reactions.
Whenever a Set/Remove/Clear action actually changes a value (not just writing the same thing again).
Inside the chain you can directly use the same placeholders as in templates. All flattened keys are available (with Local taking precedence). You can still reach the exact scope using local.<key>, app.<key>, or global.<key>.
selection changes elsewhererowDetails after selection set)Because the event fires for ANY change, always gate expensive logic:
lastProcessedSelection and compare.Example condition idea: Only continue if the row actually changed. You could store lastProcessedSelection locally and compare {{ selection.rowId !== (lastProcessedSelection?.rowId) }}; then update lastProcessedSelection at the end.
selection, selectionMeta).ON_DATA_HUB_CHANGE; the rendering system already re‑supplies updated context. Use the event only for side effects (API calls, further Set actions, navigation, dialogs, etc.).Row Click → Local Set (selection = JSON Interpolated: { "rowId":"{{clickedRow.id}}", "ts":"{{timestamp}}" })
Button Click chain:
selection)selection){{selection.rowId}})rowDetails = response)Input Change → App Set (activeFilter = user input). All boardlet reference activeFilter or app.activeFilter. Boardlet can also listen for On Data Hub Change
Add event ON_DATA_HUB_CHANGE to a panel element; first action checks if selection actually changed; next action fetches details and stores them.
currentRow = { "id": "{{clickedRow.id}}", "name": "{{clickedRow.name}}" }.Selected: {{currentRow.name}}.Result: Label updates automatically after the Set action without needing ON_DATA_HUB_CHANGE.
currentRow = { "name": "Loading..." }.currentRow again with real data.If both global.currentUser and Local currentUser exist:
Welcome {{currentUser.name || global.currentUser.name}} ensures Local override but falls back gracefully.
All other dynamic components work the same: use {{ someKey }} (flattened), or explicitly {{ app.filterX }}, {{ global.tenant.id }}.
| Pattern | Guidance |
|---|---|
| Generic key reused widely | Add prefix (appNameShortcut.filters, appNameShortcut.selection) |
| Temporary chain scratch values | Prefix with underscore (_tempPayload) and keep Local |
On destroy event| Symptom | Cause | Fix |
|---|---|---|
| Blank interpolation | Key not set yet | Ensure Set runs first; default {{ myKey }} |
| Stale nested data | Shallow merge kept old props | Set to null then replace OR overwrite all fields |
| Unexpected override | Same key at Local/App | Namespace or explicit global. / app. access |
| Label not updating | Using custom text set via component action only once | Ensure you update via Set Value or re‑invoke component action; or rely on context interpolation instead |
| Too many ON_DATA_HUB_CHANGE triggers | Large object changes frequently | Split object into smaller keys / gate actions with Condition |
| Goal | Recommended Action |
|---|---|
| Share something only within a widget | Local Set Value |
| Share between several widgets in same app | App Set Value |
| Share across different apps | Global Set Value |
| React to a changed value with more actions | Add ON_DATA_HUB_CHANGE + Condition |
| Avoid key clashes | Prefix or namespace keys (table.selection) |
| Reset state | Set to empty string, null or {} depending on expected shape |
Performance (plain language): keep keys focused, split large objects, only enable ON_DATA_HUB_CHANGE where you truly need automatic follow‑up actions.