@furo/open-models 1.16.2 → 1.16.3

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.
@@ -44,15 +44,17 @@ interface FieldNodeLike {
44
44
  * const cubeModel = ModelBindings(CubeEntityModel.model);
45
45
  *
46
46
  * class MyComponent extends LitElement {
47
- * // Bind to a nested field value - updates when cube.length changes
47
+ * // Triggers re-render on any model update
48
+ * @cubeModel.bind()
49
+ * private _modelUpdated: unknown;
50
+ *
51
+ * // Triggers re-render when cube.length changes
48
52
  * @cubeModel.bind("cube.length")
49
- * @state()
50
- * private cubeLength: number = 0;
53
+ * private cubeLength: unknown;
51
54
  *
52
- * // Bind to model validity
55
+ * // Triggers re-render on model validity changes
53
56
  * @cubeModel.bind("__isValid", "validity-changed")
54
- * @state()
55
- * private isValid: boolean = true;
57
+ * private isValid: unknown;
56
58
  *
57
59
  * // React to any field value change on the model
58
60
  * @cubeModel.onEvent("field-value-changed")
@@ -73,13 +75,13 @@ interface FieldNodeLike {
73
75
  */
74
76
  export declare function ModelBindings<TEventMap extends ModelEventMap = ModelEventMap>(model: FieldNodeLike): {
75
77
  /**
76
- * Binds a component property to a model field value.
77
- * When the field changes, the property is automatically updated.
78
+ * Triggers a render update when a model event fires.
79
+ * When called without arguments, listens on the model root for the "update" event.
78
80
  *
79
- * @param path - Path to the field (e.g., "cube.length", "__isValid")
80
- * @param eventType - Event to listen for (defaults to "this-field-value-changed")
81
+ * @param path - Optional path to a field (e.g., "cube.length"). When omitted, listens on the model root.
82
+ * @param eventType - Event to listen for (defaults to "update")
81
83
  */
82
- bind(path: string, eventType?: ModelEventType): (target: object, propertyKey: string) => void;
84
+ bind(path?: string, eventType?: ModelEventType): (target: object, propertyKey: string) => void;
83
85
  /**
84
86
  * Binds a method to an event on the root model.
85
87
  * When the event fires, the method is called.
@@ -23,15 +23,17 @@ const MODEL_EVENT_METHODS = Symbol.for("__modelEventMethods__");
23
23
  * const cubeModel = ModelBindings(CubeEntityModel.model);
24
24
  *
25
25
  * class MyComponent extends LitElement {
26
- * // Bind to a nested field value - updates when cube.length changes
26
+ * // Triggers re-render on any model update
27
+ * @cubeModel.bind()
28
+ * private _modelUpdated: unknown;
29
+ *
30
+ * // Triggers re-render when cube.length changes
27
31
  * @cubeModel.bind("cube.length")
28
- * @state()
29
- * private cubeLength: number = 0;
32
+ * private cubeLength: unknown;
30
33
  *
31
- * // Bind to model validity
34
+ * // Triggers re-render on model validity changes
32
35
  * @cubeModel.bind("__isValid", "validity-changed")
33
- * @state()
34
- * private isValid: boolean = true;
36
+ * private isValid: unknown;
35
37
  *
36
38
  * // React to any field value change on the model
37
39
  * @cubeModel.onEvent("field-value-changed")
@@ -53,13 +55,13 @@ const MODEL_EVENT_METHODS = Symbol.for("__modelEventMethods__");
53
55
  export function ModelBindings(model) {
54
56
  return {
55
57
  /**
56
- * Binds a component property to a model field value.
57
- * When the field changes, the property is automatically updated.
58
+ * Triggers a render update when a model event fires.
59
+ * When called without arguments, listens on the model root for the "update" event.
58
60
  *
59
- * @param path - Path to the field (e.g., "cube.length", "__isValid")
60
- * @param eventType - Event to listen for (defaults to "this-field-value-changed")
61
+ * @param path - Optional path to a field (e.g., "cube.length"). When omitted, listens on the model root.
62
+ * @param eventType - Event to listen for (defaults to "update")
61
63
  */
62
- bind(path, eventType = "this-field-value-changed") {
64
+ bind(path, eventType = "update") {
63
65
  return function bindDecorator(target, propertyKey) {
64
66
  let metadata = bindingsMetadata.get(target);
65
67
  if (!metadata) {
@@ -132,26 +134,6 @@ function getFieldForPath(model, path) {
132
134
  }
133
135
  return model;
134
136
  }
135
- /**
136
- * Get the value for a path from the model.
137
- */
138
- function getValueForPath(model, path) {
139
- if (path.startsWith("__")) {
140
- // Direct property access for internal properties
141
- return model[path];
142
- }
143
- if (!path.includes(".")) {
144
- // Direct child field
145
- const field = model[path];
146
- return field?.value ?? field;
147
- }
148
- // Nested path - get the field and return its value
149
- if (model.__getFieldNodeByPath) {
150
- const field = model.__getFieldNodeByPath(path);
151
- return field?.value ?? field;
152
- }
153
- return undefined;
154
- }
155
137
  /**
156
138
  * Patch connectedCallback/disconnectedCallback for bind decorators.
157
139
  */
@@ -173,11 +155,9 @@ function patchBindLifecycle(ctor) {
173
155
  const listeners = new Map();
174
156
  this[MODEL_BIND_LISTENERS] = listeners;
175
157
  metadata.forEach(({ model, path, eventType }, propKey) => {
176
- const field = getFieldForPath(model, path);
177
- // Set initial value
178
- this[propKey] = getValueForPath(model, path);
158
+ const field = path ? getFieldForPath(model, path) : model;
179
159
  const listener = () => {
180
- this[propKey] = getValueForPath(model, path);
160
+ this.requestUpdate();
181
161
  };
182
162
  listeners.set(propKey, { listener, field, eventType });
183
163
  field.__addEventListener(eventType, listener);
@@ -1 +1 @@
1
- {"version":3,"file":"ModelDecorators.js","sourceRoot":"","sources":["../../src/decorators/ModelDecorators.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, ReactiveElement } from \"lit\";\n\nimport type { ModelEventType } from \"@/FieldNode\";\n\n/**\n * Event map for FieldNode model events.\n * Most events don't have detail - they just notify that something changed.\n *\n * This may look like a copy of the ModelEventType, but is needed for the implementation.\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(eventType: keyof TEventMap & ModelEventType) {\n return function onEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- descriptor.value is untyped by design\n const originalMethod: EventBindingMeta[\"method\"] = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods: EventBindingMeta[] = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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(path: string, eventType: keyof TEventMap & ModelEventType) {\n return function onFieldEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- descriptor.value is untyped by design\n const originalMethod: EventBindingMeta[\"method\"] = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods: EventBindingMeta[] = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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)!;\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 // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\n const originalConnected = ctor.prototype.connectedCallback;\n // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n originalConnected?.call(this);\n\n const metadata = bindingsMetadata.get(Object.getPrototypeOf(this) as object);\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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 // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\n const originalConnected = ctor.prototype.connectedCallback;\n // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n originalConnected?.call(this);\n\n const methods = (this.constructor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n originalDisconnected?.call(this);\n };\n}\n"]}
1
+ {"version":3,"file":"ModelDecorators.js","sourceRoot":"","sources":["../../src/decorators/ModelDecorators.ts"],"names":[],"mappings":"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","sourcesContent":["import { LitElement, ReactiveElement } from \"lit\";\n\nimport type { ModelEventType } from \"@/FieldNode\";\n\n/**\n * Event map for FieldNode model events.\n * Most events don't have detail - they just notify that something changed.\n *\n * This may look like a copy of the ModelEventType, but is needed for the implementation.\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 | undefined;\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 * // Triggers re-render on any model update\n * @cubeModel.bind()\n * private _modelUpdated: unknown;\n *\n * // Triggers re-render when cube.length changes\n * @cubeModel.bind(\"cube.length\")\n * private cubeLength: unknown;\n *\n * // Triggers re-render on model validity changes\n * @cubeModel.bind(\"__isValid\", \"validity-changed\")\n * private isValid: unknown;\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 * Triggers a render update when a model event fires.\n * When called without arguments, listens on the model root for the \"update\" event.\n *\n * @param path - Optional path to a field (e.g., \"cube.length\"). When omitted, listens on the model root.\n * @param eventType - Event to listen for (defaults to \"update\")\n */\n bind(path?: string, eventType: ModelEventType = \"update\") {\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(eventType: keyof TEventMap & ModelEventType) {\n return function onEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- descriptor.value is untyped by design\n const originalMethod: EventBindingMeta[\"method\"] = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods: EventBindingMeta[] = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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(path: string, eventType: keyof TEventMap & ModelEventType) {\n return function onFieldEventDecorator(target: object, propertyKey: string, descriptor: PropertyDescriptor) {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- descriptor.value is untyped by design\n const originalMethod: EventBindingMeta[\"method\"] = descriptor.value;\n const ctor = target.constructor as typeof ReactiveElement;\n\n let methods: EventBindingMeta[] = (ctor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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)!;\n }\n return model;\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 // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\n const originalConnected = ctor.prototype.connectedCallback;\n // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n originalConnected?.call(this);\n\n const metadata = bindingsMetadata.get(Object.getPrototypeOf(this) as object);\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 = path ? getFieldForPath(model, path) : model;\n\n const listener = () => {\n this.requestUpdate();\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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 // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\n const originalConnected = ctor.prototype.connectedCallback;\n // eslint-disable-next-line @typescript-eslint/unbound-method -- method is called with .call()\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n originalConnected?.call(this);\n\n const methods = (this.constructor as unknown as Record<symbol, EventBindingMeta[]>)[MODEL_EVENT_METHODS];\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n if (listeners) {\n listeners.forEach(({ listener, field, eventType }) => {\n field.__removeEventListener(eventType, listener);\n });\n listeners.clear();\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- runtime data may not match types (REST API input)\n originalDisconnected?.call(this);\n };\n}\n"]}
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "url": "git+ssh://git@github.com/eclipse/eclipsefuro-web.git"
9
9
  },
10
10
  "author": "eclipse furo",
11
- "version": "1.16.2",
11
+ "version": "1.16.3",
12
12
  "type": "module",
13
13
  "main": "dist/index.js",
14
14
  "types": "dist/index.d.ts",
package/web-types.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/JetBrains/web-types/master/schema/web-types.json",
3
3
  "name": "@furo/open-models",
4
- "version": "1.16.2",
4
+ "version": "1.16.3",
5
5
  "description-markup": "markdown",
6
6
  "default-icon": "icon.svg",
7
7
  "contributions": {