Summary
Custom Drapo components written in TypeScript often maintain internal state objects that must stay in sync with Drapo's data store. Currently there is no clean API for a component to react when a specific data key (or field path) changes. This leads to fragile workarounds where components manually re-read from the data store at serialization time, risking stale state.
Problem
When a Drapo component is loaded, it may deserialize data from the store into a typed internal object (e.g. _process: WorkflowProcessVO). The component owns that internal object and uses it for all its business logic.
class WorkflowProcess {
private _process: WorkflowProcessVO;
public async Initialize(): Promise<void> {
// Load from Drapo's store into internal object
const raw: any = await this.Application.Storage.RetrieveData('workflowprocess', this.Sector);
this._process = this.Serializer.Deserialize(raw);
}
}
Later, the user interacts with a panel where bindings like d-model="{{workflowprocess.Parameters}}" update Drapo's store directly. At that point, _process.Parameters (the internal copy) is out of sync with what Drapo has stored.
When the component needs to save, it must manually sync the two copies:
// ⚠️ Current workaround
private async SaveInternal(): Promise<boolean> {
const process: WorkflowProcessVO = this.Storage.GetProcessRoot(); // internal copy
// Drapo stores a separate copy of workflowprocess, so parameters updated via
// d-model bindings in the panel are not reflected in _process.Parameters.
// Sync from the Drapo data key before serializing to capture any panel changes.
const workflowProcessDrapo: any = await this.Application.Storage.RetrieveData('workflowprocess', this.Sector);
if (workflowProcessDrapo?.Parameters != null)
process.Parameters = workflowProcessDrapo.Parameters;
const serialized: string = this.Serializer.Serialize(process);
// ... POST to API
}
This works, but has drawbacks:
- Fragile: any new field editable via Drapo bindings must be added to the sync block manually.
- Imperative: the component must "remember" to sync before every serialization point.
- Error-prone: if a future developer adds a new bindable field and forgets to sync it, data is silently lost on save.
Existing API — Observer.SubscribeComponent
Drapo already has an internal mechanism used by the built-in component system:
// From DrapoObserver.ts
public SubscribeComponent(value: string, el: HTMLElement, notifyFunction: Function, elFocus?: HTMLElement): void
An example from the labelinputsubscribe component:
this._app.Observer.SubscribeComponent(dataKey, this._el, async () => {
await instance.Notify(); // must call RetrieveData() again inside Notify
});
This API partially addresses the problem, but has limitations for typed-state scenarios:
- Requires an
HTMLElement as the observer identity — awkward for state sync that is not tied to a specific DOM element.
- Callback receives
(el, app, dataFields), not the new value — the component still must call Storage.RetrieveData to get the updated data.
- Fires for the whole
dataKey — if subscribed to workflowprocess, it fires on any change to the object, even unrelated fields.
- No unsubscribe token — there is no clean way for a component to stop listening (e.g. on destroy).
Proposed Solution
Add a higher-level API to DrapoStorage (or as a new method) designed specifically for component authors:
// Proposed signature
public Watch(mustache: string, sector: string, callback: (newValue: any) => Promise<void>): () => void
Usage from a component:
public async Initialize(): Promise<void> {
const raw: any = await this.Application.Storage.RetrieveData('workflowprocess', this.Sector);
this._process = this.Serializer.Deserialize(raw);
// Subscribe — callback receives the new value automatically
this._unwatch = this.Application.Storage.Watch(
'{{workflowprocess.Parameters}}',
this.Sector,
async (newParameters: WorkflowProcessParameterVO[]) => {
this._process.Parameters = newParameters;
}
);
}
public Destroy(): void {
if (this._unwatch) this._unwatch();
}
Behavior
| Concern |
Expected |
mustache |
Any valid Drapo mustache path, e.g. {{key}} or {{key.field}} |
sector |
The sector scope for resolving the data key |
callback(newValue) |
Called whenever the referenced path changes; receives the current value |
| Return value |
An unsubscribe function; calling it stops all future notifications |
| Granularity |
Should fire only when the specific field path changes, not on all changes to the data key |
Implementation Notes
Internally, Watch would reuse the existing Observer.SubscribeComponent infrastructure:
public Watch(mustache: string, sector: string, callback: (newValue: any) => Promise<void>): () => void {
const sentinel: HTMLElement = document.createElement('span');
this.Application.Observer.SubscribeComponent(mustache, sentinel, async (_el, _app, _fields) => {
const newValue: any = await this.RetrieveDataValue(sector, mustache);
await callback(newValue);
return false; // prevent Drapo from calling ResolveComponentUpdate on the sentinel
});
return () => {
// Requires UnsubscribeComponent to be made public — see secondary gap below
};
}
This reveals a secondary gap: UnsubscribeComponent is currently private. To support a proper unsubscribe token, it (or a variant accepting an HTMLElement) should be made public.
Acceptance Criteria
Summary
Custom Drapo components written in TypeScript often maintain internal state objects that must stay in sync with Drapo's data store. Currently there is no clean API for a component to react when a specific data key (or field path) changes. This leads to fragile workarounds where components manually re-read from the data store at serialization time, risking stale state.
Problem
When a Drapo component is loaded, it may deserialize data from the store into a typed internal object (e.g.
_process: WorkflowProcessVO). The component owns that internal object and uses it for all its business logic.Later, the user interacts with a panel where bindings like
d-model="{{workflowprocess.Parameters}}"update Drapo's store directly. At that point,_process.Parameters(the internal copy) is out of sync with what Drapo has stored.When the component needs to save, it must manually sync the two copies:
This works, but has drawbacks:
Existing API —
Observer.SubscribeComponentDrapo already has an internal mechanism used by the built-in component system:
An example from the
labelinputsubscribecomponent:This API partially addresses the problem, but has limitations for typed-state scenarios:
HTMLElementas the observer identity — awkward for state sync that is not tied to a specific DOM element.(el, app, dataFields), not the new value — the component still must callStorage.RetrieveDatato get the updated data.dataKey— if subscribed toworkflowprocess, it fires on any change to the object, even unrelated fields.Proposed Solution
Add a higher-level API to
DrapoStorage(or as a new method) designed specifically for component authors:Usage from a component:
Behavior
mustache{{key}}or{{key.field}}sectorcallback(newValue)Implementation Notes
Internally,
Watchwould reuse the existingObserver.SubscribeComponentinfrastructure:This reveals a secondary gap:
UnsubscribeComponentis currentlyprivate. To support a proper unsubscribe token, it (or a variant accepting anHTMLElement) should be made public.Acceptance Criteria
Application.Storage.Watch(mustache, sector, callback)is available to custom component authors.callbackreceives the new resolved value directly — no need to callRetrieveDatainside the callback.Watchreturns an unsubscribe function.{{key.field}}is given.UnsubscribeComponent(or a public variant) is exposed so components can clean up subscriptions on destroy.