@furo/open-models 1.14.0 → 1.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,229 @@
1
+ // Symbol keys for class metadata
2
+ const FIELD_EVENTS = Symbol.for("__fieldEvents__");
3
+ // Symbol keys for instance storage
4
+ const FIELD_LISTENERS = Symbol.for("__fieldListeners__");
5
+ const FIELD_PATCHED = Symbol.for("__fieldPatched__");
6
+ const CURRENT_MODEL = Symbol.for("__currentModel__");
7
+ const MODEL_WRITE_FN = Symbol.for("__modelWriteFn__");
8
+ const MODEL_READ_FN = Symbol.for("__modelReadFn__");
9
+ // ─────────────────────────────────────────────────────────────────
10
+ // fieldBindings - Decorators for Reusable Bindable Components
11
+ // ─────────────────────────────────────────────────────────────────
12
+ /**
13
+ * ### fieldBindings
14
+ *
15
+ * Decorators for creating reusable components that bind to FieldNode models.
16
+ *
17
+ * The component provides `modelReaders` and `modelWriters` maps keyed by
18
+ * `__meta.typeName`. The decorator handles:
19
+ * - Binding/unbinding on model change
20
+ * - Calling the correct reader when model value changes
21
+ * - Providing `writeToModel()` method that calls the correct writer
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * export class MyInput extends LitElement implements BindableComponent {
26
+ * @fieldBindings.model()
27
+ * model: STRING | XString | undefined;
28
+ *
29
+ * declare writeToModel: () => void;
30
+ *
31
+ * modelReaders = new Map([
32
+ * ["primitives.STRING", () => { this.value = (this.model as STRING).value ?? ""; }],
33
+ * ["furo.fat.String", () => { this.value = (this.model as XString).value.value ?? ""; }],
34
+ * ]);
35
+ *
36
+ * modelWriters = new Map([
37
+ * ["primitives.STRING", () => { (this.model as STRING).value = this.value; }],
38
+ * ["furo.fat.String", () => { (this.model as XString).value.value = this.value; }],
39
+ * ]);
40
+ *
41
+ * private _onInput(e: Event) {
42
+ * this.value = (e.target as HTMLInputElement).value;
43
+ * this.writeToModel();
44
+ * }
45
+ * }
46
+ * ```
47
+ */
48
+ export const fieldBindings = {
49
+ /**
50
+ * Decorator for the `model` property.
51
+ *
52
+ * Handles:
53
+ * - Binding/unbinding when model changes
54
+ * - Resolving reader/writer functions based on model type
55
+ * - Calling reader on model value changes
56
+ * - Providing `writeToModel()` method
57
+ */
58
+ model() {
59
+ return function modelDecorator(target, propertyKey) {
60
+ const ctor = target.constructor;
61
+ // Patch lifecycle
62
+ patchLifecycle(ctor);
63
+ // Add writeToModel helper method
64
+ if (!Object.prototype.hasOwnProperty.call(target, "writeToModel")) {
65
+ Object.defineProperty(target, "writeToModel", {
66
+ value: function writeToModel() {
67
+ const writeFn = this[MODEL_WRITE_FN];
68
+ if (writeFn) {
69
+ try {
70
+ writeFn();
71
+ }
72
+ catch (e) {
73
+ // eslint-disable-next-line no-console
74
+ console.error("Failed to write to model:", e);
75
+ }
76
+ }
77
+ },
78
+ writable: false,
79
+ enumerable: false,
80
+ configurable: true,
81
+ });
82
+ }
83
+ // Create getter/setter for the model property
84
+ Object.defineProperty(target, propertyKey, {
85
+ get() {
86
+ return this[CURRENT_MODEL];
87
+ },
88
+ set(value) {
89
+ const oldModel = this[CURRENT_MODEL];
90
+ if (value === oldModel)
91
+ return;
92
+ // Unbind from old model
93
+ if (oldModel) {
94
+ unbindFromModel(this, oldModel);
95
+ }
96
+ // Store new model
97
+ this[CURRENT_MODEL] = value;
98
+ // Resolve reader/writer functions based on type
99
+ if (value) {
100
+ const typeName = value.__meta?.typeName ?? "primitives.STRING";
101
+ // Resolve reader
102
+ const reader = this.modelReaders?.get(typeName);
103
+ if (reader) {
104
+ this[MODEL_READ_FN] = reader.bind(this);
105
+ }
106
+ else {
107
+ // eslint-disable-next-line no-console
108
+ console.warn(`No modelReader for type "${typeName}". Available: ${[...(this.modelReaders?.keys() ?? [])].join(", ")}`);
109
+ this[MODEL_READ_FN] = undefined;
110
+ }
111
+ // Resolve writer
112
+ const writer = this.modelWriters?.get(typeName);
113
+ if (writer) {
114
+ this[MODEL_WRITE_FN] = writer.bind(this);
115
+ }
116
+ else {
117
+ // eslint-disable-next-line no-console
118
+ console.warn(`No modelWriter for type "${typeName}". Available: ${[...(this.modelWriters?.keys() ?? [])].join(", ")}`);
119
+ this[MODEL_WRITE_FN] = undefined;
120
+ }
121
+ }
122
+ else {
123
+ this[MODEL_READ_FN] = undefined;
124
+ this[MODEL_WRITE_FN] = undefined;
125
+ }
126
+ // Bind to new model (if connected)
127
+ if (value && this.isConnected) {
128
+ bindToModel(this, value);
129
+ }
130
+ // Trigger Lit update
131
+ this.requestUpdate();
132
+ },
133
+ enumerable: true,
134
+ configurable: true,
135
+ });
136
+ };
137
+ },
138
+ /**
139
+ * Binds a method to an event on the model.
140
+ * When the event fires, the method is called with the event detail.
141
+ *
142
+ * @param eventType - The event type to listen for
143
+ */
144
+ onEvent(eventType) {
145
+ return function onEventDecorator(target, propertyKey, descriptor) {
146
+ const originalMethod = descriptor.value;
147
+ const ctor = target.constructor;
148
+ let events = ctor[FIELD_EVENTS];
149
+ if (!events) {
150
+ events = [];
151
+ ctor[FIELD_EVENTS] = events;
152
+ }
153
+ events.push({ propertyKey, eventType, method: originalMethod });
154
+ patchLifecycle(ctor);
155
+ };
156
+ },
157
+ };
158
+ /**
159
+ * Bind to the model - set up event listeners and call initial read.
160
+ */
161
+ function bindToModel(component, model) {
162
+ const ctor = component.constructor;
163
+ const events = ctor[FIELD_EVENTS] ?? [];
164
+ const listeners = new Map();
165
+ component[FIELD_LISTENERS] = listeners;
166
+ // Get the pre-resolved read function
167
+ const readFn = component[MODEL_READ_FN];
168
+ // Set up value change listener - calls the reader
169
+ if (readFn) {
170
+ const valueListener = () => {
171
+ readFn();
172
+ };
173
+ listeners.set("value", { eventType: "this-field-value-changed", listener: valueListener });
174
+ model.__addEventListener("this-field-value-changed", valueListener);
175
+ // Initial read
176
+ readFn();
177
+ }
178
+ // Set up event bindings from @fieldBindings.onEvent decorators
179
+ events.forEach(({ propertyKey, eventType, method }) => {
180
+ const listener = (e) => {
181
+ method.call(component, e.detail);
182
+ };
183
+ listeners.set(`event:${propertyKey}`, { eventType, listener });
184
+ model.__addEventListener(eventType, listener);
185
+ });
186
+ }
187
+ /**
188
+ * Unbind all listeners from the model.
189
+ */
190
+ function unbindFromModel(component, model) {
191
+ const listeners = component[FIELD_LISTENERS];
192
+ if (!listeners)
193
+ return;
194
+ listeners.forEach(({ eventType, listener }) => {
195
+ model.__removeEventListener(eventType, listener);
196
+ });
197
+ listeners.clear();
198
+ }
199
+ // ─────────────────────────────────────────────────────────────────
200
+ // Lifecycle Patching
201
+ // ─────────────────────────────────────────────────────────────────
202
+ /**
203
+ * Patch connectedCallback/disconnectedCallback to handle model binding.
204
+ */
205
+ function patchLifecycle(ctor) {
206
+ if (ctor[FIELD_PATCHED]) {
207
+ return;
208
+ }
209
+ ctor[FIELD_PATCHED] = true;
210
+ const originalConnected = ctor.prototype.connectedCallback;
211
+ const originalDisconnected = ctor.prototype.disconnectedCallback;
212
+ ctor.prototype.connectedCallback = function connectedCallback() {
213
+ originalConnected?.call(this);
214
+ // Bind to model if already set
215
+ const model = this[CURRENT_MODEL];
216
+ if (model) {
217
+ bindToModel(this, model);
218
+ }
219
+ };
220
+ ctor.prototype.disconnectedCallback = function disconnectedCallback() {
221
+ // Unbind from model
222
+ const model = this[CURRENT_MODEL];
223
+ if (model) {
224
+ unbindFromModel(this, model);
225
+ }
226
+ originalDisconnected?.call(this);
227
+ };
228
+ }
229
+ //# sourceMappingURL=FieldBindings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FieldBindings.js","sourceRoot":"","sources":["../../src/decorators/FieldBindings.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable no-param-reassign */\nimport { LitElement, ReactiveElement } from \"lit\";\n\nimport type { ModelEventType } from \"./ModelDecorators\";\n\n// ─────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Interface for FieldNode-like objects that support event listening.\n */\nexport interface FieldNodeLike {\n __addEventListener(type: string, listener: (e: CustomEvent) => void): void;\n __removeEventListener(type: string, listener: (e: CustomEvent) => void): void;\n __meta?: { typeName?: string };\n value?: unknown;\n}\n\n/**\n * Interface for components that bind to FieldNode models.\n *\n * Components implement `modelReaders` and `modelWriters` maps\n * keyed by `__meta.typeName` (e.g., \"primitives.STRING\", \"furo.fat.String\").\n *\n * @example\n * ```typescript\n * export class MyInput extends LitElement implements BindableComponent {\n * @fieldBindings.model()\n * model: STRING | XString | undefined;\n *\n * // Provided by decorator\n * declare writeToModel: () => void;\n *\n * modelReaders = new Map<string, () => void>([\n * [\"primitives.STRING\", () => { this.value = (this.model as STRING).value ?? \"\"; }],\n * [\"furo.fat.String\", () => { this.value = (this.model as XString).value.value ?? \"\"; }],\n * ]);\n *\n * modelWriters = new Map<string, () => void>([\n * [\"primitives.STRING\", () => { (this.model as STRING).value = this.value; }],\n * [\"furo.fat.String\", () => { (this.model as XString).value.value = this.value; }],\n * ]);\n * }\n * ```\n */\nexport interface BindableComponent extends LitElement {\n model: FieldNodeLike | undefined;\n\n /** Map of typeName → reader function (model → component) */\n modelReaders: Map<string, () => void>;\n\n /** Map of typeName → writer function (component → model) */\n modelWriters: Map<string, () => void>;\n\n /** Helper to write current value to model (provided by decorator) */\n writeToModel: () => void;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Metadata Storage\n// ─────────────────────────────────────────────────────────────────\n\ninterface FieldEventMeta {\n propertyKey: string;\n eventType: ModelEventType;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n method: Function;\n}\n\n// Symbol keys for class metadata\nconst FIELD_EVENTS = Symbol.for(\"__fieldEvents__\");\n\n// Symbol keys for instance storage\nconst FIELD_LISTENERS = Symbol.for(\"__fieldListeners__\");\nconst FIELD_PATCHED = Symbol.for(\"__fieldPatched__\");\nconst CURRENT_MODEL = Symbol.for(\"__currentModel__\");\nconst MODEL_WRITE_FN = Symbol.for(\"__modelWriteFn__\");\nconst MODEL_READ_FN = Symbol.for(\"__modelReadFn__\");\n\n// ─────────────────────────────────────────────────────────────────\n// fieldBindings - Decorators for Reusable Bindable Components\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * ### fieldBindings\n *\n * Decorators for creating reusable components that bind to FieldNode models.\n *\n * The component provides `modelReaders` and `modelWriters` maps keyed by\n * `__meta.typeName`. The decorator handles:\n * - Binding/unbinding on model change\n * - Calling the correct reader when model value changes\n * - Providing `writeToModel()` method that calls the correct writer\n *\n * @example\n * ```typescript\n * export class MyInput extends LitElement implements BindableComponent {\n * @fieldBindings.model()\n * model: STRING | XString | undefined;\n *\n * declare writeToModel: () => void;\n *\n * modelReaders = new Map([\n * [\"primitives.STRING\", () => { this.value = (this.model as STRING).value ?? \"\"; }],\n * [\"furo.fat.String\", () => { this.value = (this.model as XString).value.value ?? \"\"; }],\n * ]);\n *\n * modelWriters = new Map([\n * [\"primitives.STRING\", () => { (this.model as STRING).value = this.value; }],\n * [\"furo.fat.String\", () => { (this.model as XString).value.value = this.value; }],\n * ]);\n *\n * private _onInput(e: Event) {\n * this.value = (e.target as HTMLInputElement).value;\n * this.writeToModel();\n * }\n * }\n * ```\n */\nexport const fieldBindings = {\n /**\n * Decorator for the `model` property.\n *\n * Handles:\n * - Binding/unbinding when model changes\n * - Resolving reader/writer functions based on model type\n * - Calling reader on model value changes\n * - Providing `writeToModel()` method\n */\n model() {\n return function modelDecorator<T extends FieldNodeLike>(target: object, propertyKey: string) {\n const ctor = target.constructor as typeof ReactiveElement;\n\n // Patch lifecycle\n patchLifecycle(ctor);\n\n // Add writeToModel helper method\n if (!Object.prototype.hasOwnProperty.call(target, \"writeToModel\")) {\n Object.defineProperty(target, \"writeToModel\", {\n value: function writeToModel(this: LitElement & Record<symbol, (() => void) | undefined>) {\n const writeFn = this[MODEL_WRITE_FN];\n if (writeFn) {\n try {\n writeFn();\n } catch (e) {\n // eslint-disable-next-line no-console\n console.error(\"Failed to write to model:\", e);\n }\n }\n },\n writable: false,\n enumerable: false,\n configurable: true,\n });\n }\n\n // Create getter/setter for the model property\n Object.defineProperty(target, propertyKey, {\n get(this: LitElement & Record<symbol, T | undefined>): T | undefined {\n return this[CURRENT_MODEL];\n },\n set(this: LitElement & BindableComponent & Record<symbol, T | (() => void) | undefined>, value: T | undefined) {\n const oldModel = this[CURRENT_MODEL] as T | undefined;\n if (value === oldModel) return;\n\n // Unbind from old model\n if (oldModel) {\n unbindFromModel(this, oldModel);\n }\n\n // Store new model\n this[CURRENT_MODEL] = value;\n\n // Resolve reader/writer functions based on type\n if (value) {\n const typeName = value.__meta?.typeName ?? \"primitives.STRING\";\n\n // Resolve reader\n const reader = this.modelReaders?.get(typeName);\n if (reader) {\n this[MODEL_READ_FN] = reader.bind(this);\n } else {\n // eslint-disable-next-line no-console\n console.warn(`No modelReader for type \"${typeName}\". Available: ${[...(this.modelReaders?.keys() ?? [])].join(\", \")}`);\n this[MODEL_READ_FN] = undefined;\n }\n\n // Resolve writer\n const writer = this.modelWriters?.get(typeName);\n if (writer) {\n this[MODEL_WRITE_FN] = writer.bind(this);\n } else {\n // eslint-disable-next-line no-console\n console.warn(`No modelWriter for type \"${typeName}\". Available: ${[...(this.modelWriters?.keys() ?? [])].join(\", \")}`);\n this[MODEL_WRITE_FN] = undefined;\n }\n } else {\n this[MODEL_READ_FN] = undefined;\n this[MODEL_WRITE_FN] = undefined;\n }\n\n // Bind to new model (if connected)\n if (value && this.isConnected) {\n bindToModel(this, value);\n }\n\n // Trigger Lit update\n this.requestUpdate();\n },\n enumerable: true,\n configurable: true,\n });\n };\n },\n\n /**\n * Binds a method to an event on the model.\n * When the event fires, the method is called with the event detail.\n *\n * @param eventType - The event type to listen for\n */\n onEvent(eventType: ModelEventType) {\n return function onEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let events = (ctor as unknown as Record<symbol, FieldEventMeta[]>)[FIELD_EVENTS];\n if (!events) {\n events = [];\n (ctor as unknown as Record<symbol, FieldEventMeta[]>)[FIELD_EVENTS] = events;\n }\n events.push({ propertyKey, eventType, method: originalMethod });\n\n patchLifecycle(ctor);\n };\n },\n};\n\n// ─────────────────────────────────────────────────────────────────\n// Binding Logic\n// ─────────────────────────────────────────────────────────────────\n\ninterface ListenerEntry {\n eventType: string;\n listener: (e: CustomEvent) => void;\n}\n\ntype ComponentWithListeners = LitElement & BindableComponent & Record<symbol, Map<string, ListenerEntry> | FieldNodeLike | (() => void) | undefined>;\n\n/**\n * Bind to the model - set up event listeners and call initial read.\n */\nfunction bindToModel(component: ComponentWithListeners, model: FieldNodeLike): void {\n const ctor = component.constructor as typeof ReactiveElement;\n const events = (ctor as unknown as Record<symbol, FieldEventMeta[]>)[FIELD_EVENTS] ?? [];\n\n const listeners = new Map<string, ListenerEntry>();\n component[FIELD_LISTENERS] = listeners;\n\n // Get the pre-resolved read function\n const readFn = component[MODEL_READ_FN] as (() => void) | undefined;\n\n // Set up value change listener - calls the reader\n if (readFn) {\n const valueListener = () => {\n readFn();\n };\n\n listeners.set(\"value\", { eventType: \"this-field-value-changed\", listener: valueListener });\n model.__addEventListener(\"this-field-value-changed\", valueListener);\n\n // Initial read\n readFn();\n }\n\n // Set up event bindings from @fieldBindings.onEvent decorators\n events.forEach(({ propertyKey, eventType, method }) => {\n const listener = (e: CustomEvent) => {\n method.call(component, e.detail);\n };\n\n listeners.set(`event:${propertyKey}`, { eventType, listener });\n model.__addEventListener(eventType, listener);\n });\n}\n\n/**\n * Unbind all listeners from the model.\n */\nfunction unbindFromModel(component: ComponentWithListeners, model: FieldNodeLike): void {\n const listeners = component[FIELD_LISTENERS] as Map<string, ListenerEntry> | undefined;\n if (!listeners) return;\n\n listeners.forEach(({ eventType, listener }) => {\n model.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Lifecycle Patching\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Patch connectedCallback/disconnectedCallback to handle model binding.\n */\nfunction patchLifecycle(ctor: typeof ReactiveElement): void {\n if ((ctor as unknown as Record<symbol, boolean>)[FIELD_PATCHED]) {\n return;\n }\n (ctor as unknown as Record<symbol, boolean>)[FIELD_PATCHED] = true;\n\n const originalConnected = ctor.prototype.connectedCallback;\n const originalDisconnected = ctor.prototype.disconnectedCallback;\n\n ctor.prototype.connectedCallback = function connectedCallback(this: ComponentWithListeners) {\n originalConnected?.call(this);\n\n // Bind to model if already set\n const model = this[CURRENT_MODEL] as FieldNodeLike | undefined;\n if (model) {\n bindToModel(this, model);\n }\n };\n\n ctor.prototype.disconnectedCallback = function disconnectedCallback(this: ComponentWithListeners) {\n // Unbind from model\n const model = this[CURRENT_MODEL] as FieldNodeLike | undefined;\n if (model) {\n unbindFromModel(this, model);\n }\n\n originalDisconnected?.call(this);\n };\n}\n"]}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Event types dispatched by FieldNode models.
3
+ */
4
+ export type ModelEventType = "update" | "field-value-changed" | "this-field-value-changed" | "field-value-updated" | "this-state-changed" | "state-changed" | "validity-changed" | "array-changed" | "this-array-changed" | "map-changed" | "this-map-changed" | "parent-readonly-set" | "parent-readonly-unset" | "model-injected";
5
+ /**
6
+ * Event map for FieldNode model events.
7
+ * Most events don't have detail - they just notify that something changed.
8
+ */
9
+ export interface ModelEventMap {
10
+ update: undefined;
11
+ "field-value-changed": unknown;
12
+ "this-field-value-changed": undefined;
13
+ "field-value-updated": unknown;
14
+ "this-state-changed": undefined;
15
+ "state-changed": unknown;
16
+ "validity-changed": undefined;
17
+ "array-changed": unknown;
18
+ "this-array-changed": undefined;
19
+ "map-changed": unknown;
20
+ "this-map-changed": undefined;
21
+ "parent-readonly-set": undefined;
22
+ "parent-readonly-unset": undefined;
23
+ "model-injected": undefined;
24
+ }
25
+ /**
26
+ * Interface for FieldNode-like objects that support event listening and path navigation.
27
+ */
28
+ interface FieldNodeLike {
29
+ __addEventListener(type: string, listener: (e: CustomEvent) => void): void;
30
+ __removeEventListener(type: string, listener: (e: CustomEvent) => void): void;
31
+ __getFieldNodeByPath?(path: string): FieldNodeLike | undefined;
32
+ value?: unknown;
33
+ }
34
+ /**
35
+ * ### modelBindings Factory
36
+ *
37
+ * Creates type-safe decorators bound to a specific FieldNode model.
38
+ * Use this to bind component properties and methods to model events.
39
+ *
40
+ * Usage:
41
+ * ```typescript
42
+ * import { modelBindings } from "@x/furo/open-models/ModelDecorators";
43
+ * import { CubeEntityModel } from "./CubeEntityModel";
44
+ *
45
+ * const cubeModel = modelBindings(CubeEntityModel.model);
46
+ *
47
+ * class MyComponent extends LitElement {
48
+ * // Bind to a nested field value - updates when cube.length changes
49
+ * @cubeModel.bind("cube.length")
50
+ * @state()
51
+ * private cubeLength: number = 0;
52
+ *
53
+ * // Bind to model validity
54
+ * @cubeModel.bind("__isValid", "validity-changed")
55
+ * @state()
56
+ * private isValid: boolean = true;
57
+ *
58
+ * // React to any field value change on the model
59
+ * @cubeModel.onEvent("field-value-changed")
60
+ * private onAnyFieldChanged() {
61
+ * console.log("Something changed!");
62
+ * }
63
+ *
64
+ * // React to a specific field's changes
65
+ * @cubeModel.onFieldEvent("cube.length", "this-field-value-changed")
66
+ * private onLengthChanged() {
67
+ * console.log("Length changed!");
68
+ * }
69
+ * }
70
+ * ```
71
+ *
72
+ * @param model - The FieldNode model to bind to
73
+ * @returns Object with `bind`, `onEvent`, and `onFieldEvent` decorator factories
74
+ */
75
+ export declare function ModelBindings<TEventMap extends ModelEventMap = ModelEventMap>(model: FieldNodeLike): {
76
+ /**
77
+ * Binds a component property to a model field value.
78
+ * When the field changes, the property is automatically updated.
79
+ *
80
+ * @param path - Path to the field (e.g., "cube.length", "__isValid")
81
+ * @param eventType - Event to listen for (defaults to "this-field-value-changed")
82
+ */
83
+ bind(path: string, eventType?: ModelEventType): (target: object, propertyKey: string) => void;
84
+ /**
85
+ * Binds a method to an event on the root model.
86
+ * When the event fires, the method is called.
87
+ *
88
+ * @param eventType - The event type to listen for
89
+ */
90
+ onEvent<K extends keyof TEventMap & ModelEventType>(eventType: K): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => void;
91
+ /**
92
+ * Binds a method to an event on a specific field.
93
+ * When the event fires on that field, the method is called.
94
+ *
95
+ * @param path - Path to the field (e.g., "cube.length")
96
+ * @param eventType - The event type to listen for
97
+ */
98
+ onFieldEvent<K extends keyof TEventMap & ModelEventType>(path: string, eventType: K): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => void;
99
+ };
100
+ export {};
@@ -0,0 +1,227 @@
1
+ const bindingsMetadata = new WeakMap();
2
+ // Symbol keys for instance storage
3
+ const MODEL_BIND_LISTENERS = Symbol.for("__modelBindListeners__");
4
+ const MODEL_EVENT_LISTENERS = Symbol.for("__modelEventListeners__");
5
+ const MODEL_BIND_PATCHED = Symbol.for("__modelBindPatched__");
6
+ const MODEL_EVENT_PATCHED = Symbol.for("__modelEventPatched__");
7
+ const MODEL_EVENT_METHODS = Symbol.for("__modelEventMethods__");
8
+ // ─────────────────────────────────────────────────────────────────
9
+ // modelBindings Factory
10
+ // ─────────────────────────────────────────────────────────────────
11
+ /**
12
+ * ### modelBindings Factory
13
+ *
14
+ * Creates type-safe decorators bound to a specific FieldNode model.
15
+ * Use this to bind component properties and methods to model events.
16
+ *
17
+ * Usage:
18
+ * ```typescript
19
+ * import { modelBindings } from "@x/furo/open-models/ModelDecorators";
20
+ * import { CubeEntityModel } from "./CubeEntityModel";
21
+ *
22
+ * const cubeModel = modelBindings(CubeEntityModel.model);
23
+ *
24
+ * class MyComponent extends LitElement {
25
+ * // Bind to a nested field value - updates when cube.length changes
26
+ * @cubeModel.bind("cube.length")
27
+ * @state()
28
+ * private cubeLength: number = 0;
29
+ *
30
+ * // Bind to model validity
31
+ * @cubeModel.bind("__isValid", "validity-changed")
32
+ * @state()
33
+ * private isValid: boolean = true;
34
+ *
35
+ * // React to any field value change on the model
36
+ * @cubeModel.onEvent("field-value-changed")
37
+ * private onAnyFieldChanged() {
38
+ * console.log("Something changed!");
39
+ * }
40
+ *
41
+ * // React to a specific field's changes
42
+ * @cubeModel.onFieldEvent("cube.length", "this-field-value-changed")
43
+ * private onLengthChanged() {
44
+ * console.log("Length changed!");
45
+ * }
46
+ * }
47
+ * ```
48
+ *
49
+ * @param model - The FieldNode model to bind to
50
+ * @returns Object with `bind`, `onEvent`, and `onFieldEvent` decorator factories
51
+ */
52
+ export function ModelBindings(model) {
53
+ return {
54
+ /**
55
+ * Binds a component property to a model field value.
56
+ * When the field changes, the property is automatically updated.
57
+ *
58
+ * @param path - Path to the field (e.g., "cube.length", "__isValid")
59
+ * @param eventType - Event to listen for (defaults to "this-field-value-changed")
60
+ */
61
+ bind(path, eventType = "this-field-value-changed") {
62
+ return function bindDecorator(target, propertyKey) {
63
+ let metadata = bindingsMetadata.get(target);
64
+ if (!metadata) {
65
+ metadata = new Map();
66
+ bindingsMetadata.set(target, metadata);
67
+ }
68
+ metadata.set(propertyKey, { model, path, eventType });
69
+ patchBindLifecycle(target.constructor);
70
+ };
71
+ },
72
+ /**
73
+ * Binds a method to an event on the root model.
74
+ * When the event fires, the method is called.
75
+ *
76
+ * @param eventType - The event type to listen for
77
+ */
78
+ onEvent(eventType) {
79
+ return function onEventDecorator(target, propertyKey, descriptor) {
80
+ const originalMethod = descriptor.value;
81
+ const ctor = target.constructor;
82
+ let methods = ctor[MODEL_EVENT_METHODS];
83
+ if (!methods) {
84
+ methods = [];
85
+ ctor[MODEL_EVENT_METHODS] = methods;
86
+ }
87
+ methods.push({ propertyKey, model, path: null, eventType, method: originalMethod });
88
+ patchEventLifecycle(ctor);
89
+ };
90
+ },
91
+ /**
92
+ * Binds a method to an event on a specific field.
93
+ * When the event fires on that field, the method is called.
94
+ *
95
+ * @param path - Path to the field (e.g., "cube.length")
96
+ * @param eventType - The event type to listen for
97
+ */
98
+ onFieldEvent(path, eventType) {
99
+ return function onFieldEventDecorator(target, propertyKey, descriptor) {
100
+ const originalMethod = descriptor.value;
101
+ const ctor = target.constructor;
102
+ let methods = ctor[MODEL_EVENT_METHODS];
103
+ if (!methods) {
104
+ methods = [];
105
+ ctor[MODEL_EVENT_METHODS] = methods;
106
+ }
107
+ methods.push({ propertyKey, model, path, eventType, method: originalMethod });
108
+ patchEventLifecycle(ctor);
109
+ };
110
+ },
111
+ };
112
+ }
113
+ // ─────────────────────────────────────────────────────────────────
114
+ // Lifecycle Patching
115
+ // ─────────────────────────────────────────────────────────────────
116
+ /**
117
+ * Get the field node for a path, handling both direct properties and nested paths.
118
+ */
119
+ function getFieldForPath(model, path) {
120
+ // Check if it's a direct property (starts with __ or no dots)
121
+ if (path.startsWith("__") || !path.includes(".")) {
122
+ return model;
123
+ }
124
+ // Navigate to nested field (we trust the field exists per user guarantee)
125
+ if (model.__getFieldNodeByPath) {
126
+ return model.__getFieldNodeByPath(path);
127
+ }
128
+ return model;
129
+ }
130
+ /**
131
+ * Get the value for a path from the model.
132
+ */
133
+ function getValueForPath(model, path) {
134
+ if (path.startsWith("__")) {
135
+ // Direct property access for internal properties
136
+ return model[path];
137
+ }
138
+ if (!path.includes(".")) {
139
+ // Direct child field
140
+ const field = model[path];
141
+ return field?.value ?? field;
142
+ }
143
+ // Nested path - get the field and return its value
144
+ if (model.__getFieldNodeByPath) {
145
+ const field = model.__getFieldNodeByPath(path);
146
+ return field?.value ?? field;
147
+ }
148
+ return undefined;
149
+ }
150
+ /**
151
+ * Patch connectedCallback/disconnectedCallback for bind decorators.
152
+ */
153
+ function patchBindLifecycle(ctor) {
154
+ if (ctor[MODEL_BIND_PATCHED]) {
155
+ return;
156
+ }
157
+ ctor[MODEL_BIND_PATCHED] = true;
158
+ const originalConnected = ctor.prototype.connectedCallback;
159
+ const originalDisconnected = ctor.prototype.disconnectedCallback;
160
+ ctor.prototype.connectedCallback = function connectedCallback() {
161
+ originalConnected?.call(this);
162
+ const metadata = bindingsMetadata.get(Object.getPrototypeOf(this));
163
+ if (!metadata)
164
+ return;
165
+ const listeners = new Map();
166
+ this[MODEL_BIND_LISTENERS] = listeners;
167
+ metadata.forEach(({ model, path, eventType }, propKey) => {
168
+ const field = getFieldForPath(model, path);
169
+ // Set initial value
170
+ this[propKey] = getValueForPath(model, path);
171
+ const listener = () => {
172
+ this[propKey] = getValueForPath(model, path);
173
+ };
174
+ listeners.set(propKey, { listener, field, eventType });
175
+ field.__addEventListener(eventType, listener);
176
+ });
177
+ };
178
+ ctor.prototype.disconnectedCallback = function disconnectedCallback() {
179
+ const listeners = this[MODEL_BIND_LISTENERS];
180
+ if (listeners) {
181
+ listeners.forEach(({ listener, field, eventType }) => {
182
+ field.__removeEventListener(eventType, listener);
183
+ });
184
+ listeners.clear();
185
+ }
186
+ originalDisconnected?.call(this);
187
+ };
188
+ }
189
+ /**
190
+ * Patch connectedCallback/disconnectedCallback for event decorators.
191
+ */
192
+ function patchEventLifecycle(ctor) {
193
+ if (ctor[MODEL_EVENT_PATCHED]) {
194
+ return;
195
+ }
196
+ ctor[MODEL_EVENT_PATCHED] = true;
197
+ const originalConnected = ctor.prototype.connectedCallback;
198
+ const originalDisconnected = ctor.prototype.disconnectedCallback;
199
+ ctor.prototype.connectedCallback = function connectedCallback() {
200
+ originalConnected?.call(this);
201
+ const methods = this.constructor[MODEL_EVENT_METHODS];
202
+ if (!methods)
203
+ return;
204
+ const listeners = new Map();
205
+ this[MODEL_EVENT_LISTENERS] = listeners;
206
+ methods.forEach(({ propertyKey, model, path, eventType, method }) => {
207
+ // If path is specified, listen on that field; otherwise listen on root model
208
+ const field = path ? getFieldForPath(model, path) : model;
209
+ const listener = (e) => {
210
+ method.call(this, e.detail);
211
+ };
212
+ listeners.set(propertyKey, { listener, field, eventType });
213
+ field.__addEventListener(eventType, listener);
214
+ });
215
+ };
216
+ ctor.prototype.disconnectedCallback = function disconnectedCallback() {
217
+ const listeners = this[MODEL_EVENT_LISTENERS];
218
+ if (listeners) {
219
+ listeners.forEach(({ listener, field, eventType }) => {
220
+ field.__removeEventListener(eventType, listener);
221
+ });
222
+ listeners.clear();
223
+ }
224
+ originalDisconnected?.call(this);
225
+ };
226
+ }
227
+ //# sourceMappingURL=ModelDecorators.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ModelDecorators.js","sourceRoot":"","sources":["../../src/decorators/ModelDecorators.ts"],"names":[],"mappings":"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","sourcesContent":["/* eslint-disable no-param-reassign */\nimport { LitElement, ReactiveElement } from \"lit\";\n\n// ─────────────────────────────────────────────────────────────────\n// Types\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Event types dispatched by FieldNode models.\n */\nexport type ModelEventType =\n | \"update\"\n | \"field-value-changed\"\n | \"this-field-value-changed\"\n | \"field-value-updated\"\n | \"this-state-changed\"\n | \"state-changed\"\n | \"validity-changed\"\n | \"array-changed\"\n | \"this-array-changed\"\n | \"map-changed\"\n | \"this-map-changed\"\n | \"parent-readonly-set\"\n | \"parent-readonly-unset\"\n | \"model-injected\";\n\n/**\n * Event map for FieldNode model events.\n * Most events don't have detail - they just notify that something changed.\n */\nexport interface ModelEventMap {\n update: undefined;\n \"field-value-changed\": unknown;\n \"this-field-value-changed\": undefined;\n \"field-value-updated\": unknown;\n \"this-state-changed\": undefined;\n \"state-changed\": unknown;\n \"validity-changed\": undefined;\n \"array-changed\": unknown;\n \"this-array-changed\": undefined;\n \"map-changed\": unknown;\n \"this-map-changed\": undefined;\n \"parent-readonly-set\": undefined;\n \"parent-readonly-unset\": undefined;\n \"model-injected\": undefined;\n}\n\n/**\n * Interface for FieldNode-like objects that support event listening and path navigation.\n */\ninterface FieldNodeLike {\n __addEventListener(type: string, listener: (e: CustomEvent) => void): void;\n __removeEventListener(type: string, listener: (e: CustomEvent) => void): void;\n __getFieldNodeByPath?(path: string): FieldNodeLike | undefined;\n value?: unknown;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Metadata Storage\n// ─────────────────────────────────────────────────────────────────\n\ninterface BindingMeta {\n model: FieldNodeLike;\n path: string;\n eventType: ModelEventType;\n}\nconst bindingsMetadata = new WeakMap<object, Map<string, BindingMeta>>();\n\ninterface EventBindingMeta {\n propertyKey: string;\n model: FieldNodeLike;\n path: string | null;\n eventType: ModelEventType;\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n method: Function;\n}\n\n// Symbol keys for instance storage\nconst MODEL_BIND_LISTENERS = Symbol.for(\"__modelBindListeners__\");\nconst MODEL_EVENT_LISTENERS = Symbol.for(\"__modelEventListeners__\");\nconst MODEL_BIND_PATCHED = Symbol.for(\"__modelBindPatched__\");\nconst MODEL_EVENT_PATCHED = Symbol.for(\"__modelEventPatched__\");\nconst MODEL_EVENT_METHODS = Symbol.for(\"__modelEventMethods__\");\n\n// ─────────────────────────────────────────────────────────────────\n// modelBindings Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * ### modelBindings Factory\n *\n * Creates type-safe decorators bound to a specific FieldNode model.\n * Use this to bind component properties and methods to model events.\n *\n * Usage:\n * ```typescript\n * import { modelBindings } from \"@x/furo/open-models/ModelDecorators\";\n * import { CubeEntityModel } from \"./CubeEntityModel\";\n *\n * const cubeModel = modelBindings(CubeEntityModel.model);\n *\n * class MyComponent extends LitElement {\n * // Bind to a nested field value - updates when cube.length changes\n * @cubeModel.bind(\"cube.length\")\n * @state()\n * private cubeLength: number = 0;\n *\n * // Bind to model validity\n * @cubeModel.bind(\"__isValid\", \"validity-changed\")\n * @state()\n * private isValid: boolean = true;\n *\n * // React to any field value change on the model\n * @cubeModel.onEvent(\"field-value-changed\")\n * private onAnyFieldChanged() {\n * console.log(\"Something changed!\");\n * }\n *\n * // React to a specific field's changes\n * @cubeModel.onFieldEvent(\"cube.length\", \"this-field-value-changed\")\n * private onLengthChanged() {\n * console.log(\"Length changed!\");\n * }\n * }\n * ```\n *\n * @param model - The FieldNode model to bind to\n * @returns Object with `bind`, `onEvent`, and `onFieldEvent` decorator factories\n */\nexport function ModelBindings<TEventMap extends ModelEventMap = ModelEventMap>(model: FieldNodeLike) {\n return {\n /**\n * Binds a component property to a model field value.\n * When the field changes, the property is automatically updated.\n *\n * @param path - Path to the field (e.g., \"cube.length\", \"__isValid\")\n * @param eventType - Event to listen for (defaults to \"this-field-value-changed\")\n */\n bind(path: string, eventType: ModelEventType = \"this-field-value-changed\") {\n return function bindDecorator(target: object, propertyKey: string) {\n let metadata = bindingsMetadata.get(target);\n if (!metadata) {\n metadata = new Map();\n bindingsMetadata.set(target, metadata);\n }\n metadata.set(propertyKey, { model, path, eventType });\n\n patchBindLifecycle(target.constructor as typeof ReactiveElement);\n };\n },\n\n /**\n * Binds a method to an event on the root model.\n * When the event fires, the method is called.\n *\n * @param eventType - The event type to listen for\n */\n onEvent<K extends keyof TEventMap & ModelEventType>(eventType: K) {\n return function onEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n if (!methods) {\n methods = [];\n (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS] = methods;\n }\n methods.push({ propertyKey, model, path: null, eventType, method: originalMethod });\n\n patchEventLifecycle(ctor);\n };\n },\n\n /**\n * Binds a method to an event on a specific field.\n * When the event fires on that field, the method is called.\n *\n * @param path - Path to the field (e.g., \"cube.length\")\n * @param eventType - The event type to listen for\n */\n onFieldEvent<K extends keyof TEventMap & ModelEventType>(path: string, eventType: K) {\n return function onFieldEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n const originalMethod = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n if (!methods) {\n methods = [];\n (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS] = methods;\n }\n methods.push({ propertyKey, model, path, eventType, method: originalMethod });\n\n patchEventLifecycle(ctor);\n };\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Lifecycle Patching\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Get the field node for a path, handling both direct properties and nested paths.\n */\nfunction getFieldForPath(model: FieldNodeLike, path: string): FieldNodeLike {\n // Check if it's a direct property (starts with __ or no dots)\n if (path.startsWith(\"__\") || !path.includes(\".\")) {\n return model;\n }\n // Navigate to nested field (we trust the field exists per user guarantee)\n if (model.__getFieldNodeByPath) {\n return model.__getFieldNodeByPath(path) as FieldNodeLike;\n }\n return model;\n}\n\n/**\n * Get the value for a path from the model.\n */\nfunction getValueForPath(model: FieldNodeLike, path: string): unknown {\n if (path.startsWith(\"__\")) {\n // Direct property access for internal properties\n return (model as unknown as Record<string, unknown>)[path];\n }\n if (!path.includes(\".\")) {\n // Direct child field\n const field = (model as unknown as Record<string, unknown>)[path] as FieldNodeLike | undefined;\n return field?.value ?? field;\n }\n // Nested path - get the field and return its value\n if (model.__getFieldNodeByPath) {\n const field = model.__getFieldNodeByPath(path);\n return field?.value ?? field;\n }\n return undefined;\n}\n\n/**\n * Patch connectedCallback/disconnectedCallback for bind decorators.\n */\nfunction patchBindLifecycle(ctor: typeof ReactiveElement): void {\n if ((ctor as unknown as Record<symbol, boolean>)[MODEL_BIND_PATCHED]) {\n return;\n }\n (ctor as unknown as Record<symbol, boolean>)[MODEL_BIND_PATCHED] = true;\n\n const originalConnected = ctor.prototype.connectedCallback;\n const originalDisconnected = ctor.prototype.disconnectedCallback;\n\n ctor.prototype.connectedCallback = function connectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n originalConnected?.call(this);\n\n const metadata = bindingsMetadata.get(Object.getPrototypeOf(this));\n if (!metadata) return;\n\n const listeners = new Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>();\n this[MODEL_BIND_LISTENERS] = listeners;\n\n metadata.forEach(({ model, path, eventType }, propKey) => {\n const field = getFieldForPath(model, path);\n\n // Set initial value\n (this as unknown as Record<string, unknown>)[propKey] = getValueForPath(model, path);\n\n const listener = () => {\n (this as unknown as Record<string, unknown>)[propKey] = getValueForPath(model, path);\n };\n\n listeners.set(propKey, { listener, field, eventType });\n field.__addEventListener(eventType, listener);\n });\n };\n\n ctor.prototype.disconnectedCallback = function disconnectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n const listeners = this[MODEL_BIND_LISTENERS];\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n originalDisconnected?.call(this);\n };\n}\n\n/**\n * Patch connectedCallback/disconnectedCallback for event decorators.\n */\nfunction patchEventLifecycle(ctor: typeof ReactiveElement): void {\n if ((ctor as unknown as Record<symbol, boolean>)[MODEL_EVENT_PATCHED]) {\n return;\n }\n (ctor as unknown as Record<symbol, boolean>)[MODEL_EVENT_PATCHED] = true;\n\n const originalConnected = ctor.prototype.connectedCallback;\n const originalDisconnected = ctor.prototype.disconnectedCallback;\n\n ctor.prototype.connectedCallback = function connectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n originalConnected?.call(this);\n\n const methods = (this.constructor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n if (!methods) return;\n\n const listeners = new Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>();\n this[MODEL_EVENT_LISTENERS] = listeners;\n\n methods.forEach(({ propertyKey, model, path, eventType, method }) => {\n // If path is specified, listen on that field; otherwise listen on root model\n const field = path ? getFieldForPath(model, path) : model;\n\n const listener = (e: CustomEvent) => {\n method.call(this, e.detail);\n };\n\n listeners.set(propertyKey, { listener, field, eventType });\n field.__addEventListener(eventType, listener);\n });\n };\n\n ctor.prototype.disconnectedCallback = function disconnectedCallback(\n this: LitElement & Record<symbol, Map<string, { listener: (e: CustomEvent) => void; field: FieldNodeLike; eventType: string }>>\n ) {\n const listeners = this[MODEL_EVENT_LISTENERS];\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n originalDisconnected?.call(this);\n };\n}\n"]}
@@ -0,0 +1,15 @@
1
+ import { FieldNode } from "../FieldNode.js";
2
+ import type { JSONSchema7 } from "json-schema";
3
+ interface FieldNodeSchema {
4
+ $schema: string;
5
+ $id: string;
6
+ [key: string]: unknown;
7
+ }
8
+ export declare class SchemaBuilder {
9
+ static generate(model: FieldNode): JSONSchema7;
10
+ private static getProps;
11
+ private static getRequiredFields;
12
+ private static getConstraints;
13
+ static createFieldNodeFromSchema(schema: FieldNodeSchema): FieldNode;
14
+ }
15
+ export {};