@furo/open-models 1.13.0 → 1.15.0

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.
@@ -6,6 +6,13 @@ import { FieldNode } from './FieldNode.js';
6
6
  export declare class Registry {
7
7
  static register(type: string, clazz: any): void;
8
8
  private static get;
9
+ /**
10
+ * Checks whether a given type is registered.
11
+ *
12
+ * @param {string} type - The name of the type to check for registration.
13
+ * @returns {boolean} `true` if the specified type is registered; otherwise, `false`.
14
+ */
15
+ static isRegistered(type: string): boolean;
9
16
  /**
10
17
  * Internal method to create instances for Any
11
18
  * @param typename
package/dist/Registry.js CHANGED
@@ -12,6 +12,15 @@ export class Registry {
12
12
  }
13
13
  throw new Error(`Cannot find type ${typename}, ${typename} is not in the registry`);
14
14
  }
15
+ /**
16
+ * Checks whether a given type is registered.
17
+ *
18
+ * @param {string} type - The name of the type to check for registration.
19
+ * @returns {boolean} `true` if the specified type is registered; otherwise, `false`.
20
+ */
21
+ static isRegistered(type) {
22
+ return registry.has(type);
23
+ }
15
24
  /**
16
25
  * Internal method to create instances for Any
17
26
  * @param typename
@@ -1 +1 @@
1
- {"version":3,"file":"Registry.js","sourceRoot":"","sources":["../src/Registry.ts"],"names":[],"mappings":"AAMA,MAAM,QAAQ,GAA2B,IAAI,GAAG,EAAqB,CAAC;AAEtE,MAAM,OAAO,QAAQ;IACnB,8DAA8D;IAC9D,MAAM,CAAC,QAAQ,CAAC,IAAY,EAAE,KAAU;QACtC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC5B,CAAC;IAEO,MAAM,CAAC,GAAG,CAChB,QAAgB;QAMhB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,8DAA8D;YAC9D,OAAO,KAAY,CAAC;QACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,oBAAoB,QAAQ,KAAK,QAAQ,yBAAyB,CACnE,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,wBAAwB,CAC7B,QAAgB;IAChB,8DAA8D;IAC9D,QAAc,EACd,MAAkB,EAClB,aAAsB;QAEtB,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/C,OAAO,IAAI,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9D,CAAC;CACF","sourcesContent":["/**\n * notes:\n * primitives are also registered\n */\nimport { FieldNode } from './FieldNode';\n\nconst registry: Map<string, FieldNode> = new Map<string, FieldNode>();\n\nexport class Registry {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static register(type: string, clazz: any) {\n registry.set(type, clazz);\n }\n\n private static get(\n typename: string,\n ): new (\n initData?: object,\n parent?: FieldNode | undefined,\n attributeName?: string | undefined,\n ) => FieldNode {\n const clazz = registry.get(typename);\n if (clazz !== undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return clazz as any;\n }\n throw new Error(\n `Cannot find type ${typename}, ${typename} is not in the registry`,\n );\n }\n\n /**\n * Internal method to create instances for Any\n * @param typename\n * @param initData\n * @param parent\n * @param attributeName\n */\n static createInstanceByTypeName(\n typename: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n initData?: any,\n parent?: FieldNode,\n attributeName?: string,\n ): FieldNode {\n const ConstructorName = Registry.get(typename);\n return new ConstructorName(initData, parent, attributeName);\n }\n}\n"]}
1
+ {"version":3,"file":"Registry.js","sourceRoot":"","sources":["../src/Registry.ts"],"names":[],"mappings":"AAMA,MAAM,QAAQ,GAA2B,IAAI,GAAG,EAAqB,CAAC;AAEtE,MAAM,OAAO,QAAQ;IACnB,8DAA8D;IAC9D,MAAM,CAAC,QAAQ,CAAC,IAAY,EAAE,KAAU;QACtC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC5B,CAAC;IAEO,MAAM,CAAC,GAAG,CAChB,QAAgB;QAMhB,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,8DAA8D;YAC9D,OAAO,KAAY,CAAC;QACtB,CAAC;QACD,MAAM,IAAI,KAAK,CACb,oBAAoB,QAAQ,KAAK,QAAQ,yBAAyB,CACnE,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACI,MAAM,CAAC,YAAY,CAAC,IAAW;QACpC,OAAO,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IAED;;;;;;OAMG;IACH,MAAM,CAAC,wBAAwB,CAC7B,QAAgB;IAChB,8DAA8D;IAC9D,QAAc,EACd,MAAkB,EAClB,aAAsB;QAEtB,MAAM,eAAe,GAAG,QAAQ,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC/C,OAAO,IAAI,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;IAC9D,CAAC;CACF","sourcesContent":["/**\n * notes:\n * primitives are also registered\n */\nimport { FieldNode } from './FieldNode';\n\nconst registry: Map<string, FieldNode> = new Map<string, FieldNode>();\n\nexport class Registry {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n static register(type: string, clazz: any) {\n registry.set(type, clazz);\n }\n\n private static get(\n typename: string,\n ): new (\n initData?: object,\n parent?: FieldNode | undefined,\n attributeName?: string | undefined,\n ) => FieldNode {\n const clazz = registry.get(typename);\n if (clazz !== undefined) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return clazz as any;\n }\n throw new Error(\n `Cannot find type ${typename}, ${typename} is not in the registry`,\n );\n }\n\n /**\n * Checks whether a given type is registered.\n *\n * @param {string} type - The name of the type to check for registration.\n * @returns {boolean} `true` if the specified type is registered; otherwise, `false`.\n */\n public static isRegistered(type:string): boolean {\n return registry.has(type);\n }\n\n /**\n * Internal method to create instances for Any\n * @param typename\n * @param initData\n * @param parent\n * @param attributeName\n */\n static createInstanceByTypeName(\n typename: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n initData?: any,\n parent?: FieldNode,\n attributeName?: string,\n ): FieldNode {\n const ConstructorName = Registry.get(typename);\n return new ConstructorName(initData, parent, attributeName);\n }\n}\n"]}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * ### EntityServiceEventType
3
+ *
4
+ * Standard event types dispatched by entity services.
5
+ * These events cover the full lifecycle of REST API operations.
6
+ *
7
+ * #### Request Lifecycle Events
8
+ * | Event | Description |
9
+ * |--------------------|-------------------------------------------------------|
10
+ * | `busy-changed` | Loading/saving state changed |
11
+ * | `request-started` | Request has been initiated |
12
+ * | `request-finished` | Request completed (success, error, or abort) |
13
+ * | `request-aborted` | Request was aborted (navigation, timeout, new request)|
14
+ *
15
+ * #### Success Events
16
+ * | Event | Description |
17
+ * |--------------------|-------------------------------------------------------|
18
+ * | `response-received`| Successful response with parsed data |
19
+ * | `raw-response` | Successful response (raw, before parsing) |
20
+ *
21
+ * #### Error Events
22
+ * | Event | Description |
23
+ * |--------------------|-------------------------------------------------------|
24
+ * | `response-error` | Any error response (4xx/5xx) with parsed body |
25
+ * | `raw-error` | Any error response (raw, before parsing) |
26
+ * | `error-404` | Resource not found (404) |
27
+ * | `error-5xx` | Server error (500+) |
28
+ * | `fatal-error` | Unhandled/unexpected error |
29
+ *
30
+ * Usage:
31
+ * ```typescript
32
+ * export type MyServiceEventType = EntityServiceEventType;
33
+ * // Or extend with custom events:
34
+ * export type MyServiceEventType = EntityServiceEventType | "custom-event";
35
+ * ```
36
+ */
37
+ export type EntityServiceEventType = "busy-changed" | "request-started" | "request-finished" | "request-aborted" | "response-received" | "raw-response" | "response-error" | "raw-error" | "error-404" | "error-5xx" | "fatal-error";
38
+ /**
39
+ * ### EntityServiceEventMap
40
+ *
41
+ * Maps each event type to its detail payload type.
42
+ * This enables type-safe event handling with `storeBindings`.
43
+ *
44
+ * #### Request Lifecycle Events
45
+ * | Event | Detail Type |
46
+ * |--------------------|--------------------------------------------------|
47
+ * | `busy-changed` | `{ busy: boolean }` |
48
+ * | `request-started` | `{ request: unknown }` |
49
+ * | `request-finished` | `{ request: unknown }` |
50
+ * | `request-aborted` | `{ reason: string }` |
51
+ *
52
+ * #### Success Events
53
+ * | Event | Detail Type |
54
+ * |--------------------|--------------------------------------------------|
55
+ * | `response-received`| `{ response: unknown, serverResponse: Response }`|
56
+ * | `raw-response` | `{ serverResponse: Response }` |
57
+ *
58
+ * #### Error Events
59
+ * | Event | Detail Type |
60
+ * |--------------------|--------------------------------------------------|
61
+ * | `response-error` | `{ parsedResponse: unknown, serverResponse: Response }` |
62
+ * | `raw-error` | `{ serverResponse: Response }` |
63
+ * | `error-404` | `{ serverResponse: Response }` |
64
+ * | `error-5xx` | `{ serverResponse: Response }` |
65
+ * | `fatal-error` | `{ error: unknown }` |
66
+ *
67
+ * Usage:
68
+ * ```typescript
69
+ * // Extend with custom events:
70
+ * interface MyServiceEventMap extends EntityServiceEventMap {
71
+ * "custom-event": { data: string };
72
+ * }
73
+ * ```
74
+ */
75
+ export interface EntityServiceEventMap {
76
+ "busy-changed": {
77
+ busy: boolean;
78
+ };
79
+ "request-started": {
80
+ request: unknown;
81
+ };
82
+ "request-finished": {
83
+ request: unknown;
84
+ };
85
+ "request-aborted": {
86
+ reason: string;
87
+ };
88
+ "response-received": {
89
+ response: unknown;
90
+ serverResponse: Response;
91
+ };
92
+ "raw-response": {
93
+ serverResponse: Response;
94
+ };
95
+ "response-error": {
96
+ parsedResponse: unknown;
97
+ serverResponse: Response;
98
+ };
99
+ "raw-error": {
100
+ serverResponse: Response;
101
+ };
102
+ "error-404": {
103
+ serverResponse: Response;
104
+ };
105
+ "error-5xx": {
106
+ serverResponse: Response;
107
+ };
108
+ "fatal-error": {
109
+ error: unknown;
110
+ };
111
+ }
112
+ /**
113
+ * ### TypedEntityService
114
+ *
115
+ * Type alias for entity services with type-safe event handling.
116
+ * Use this when you need to type a variable or parameter that holds a service.
117
+ *
118
+ * Note: Services extend `EventTarget` at runtime, but this type provides
119
+ * compile-time type safety for event names when used with `storeBindings`.
120
+ *
121
+ * Usage:
122
+ * ```typescript
123
+ * // Type a variable
124
+ * const service: TypedEntityService = cubeEntityService;
125
+ *
126
+ * // With custom events
127
+ * interface MyEventMap extends EntityServiceEventMap {
128
+ * "custom-event": { data: string };
129
+ * }
130
+ * const myService: TypedEntityService<MyEventMap> = myEntityService;
131
+ * ```
132
+ */
133
+ export type TypedEntityService<_TEventMap extends EntityServiceEventMap = EntityServiceEventMap> = EventTarget;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=EntityServiceTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EntityServiceTypes.js","sourceRoot":"","sources":["../../src/decorators/EntityServiceTypes.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * ### EntityServiceEventType\n *\n * Standard event types dispatched by entity services.\n * These events cover the full lifecycle of REST API operations.\n *\n * #### Request Lifecycle Events\n * | Event | Description |\n * |--------------------|-------------------------------------------------------|\n * | `busy-changed` | Loading/saving state changed |\n * | `request-started` | Request has been initiated |\n * | `request-finished` | Request completed (success, error, or abort) |\n * | `request-aborted` | Request was aborted (navigation, timeout, new request)|\n *\n * #### Success Events\n * | Event | Description |\n * |--------------------|-------------------------------------------------------|\n * | `response-received`| Successful response with parsed data |\n * | `raw-response` | Successful response (raw, before parsing) |\n *\n * #### Error Events\n * | Event | Description |\n * |--------------------|-------------------------------------------------------|\n * | `response-error` | Any error response (4xx/5xx) with parsed body |\n * | `raw-error` | Any error response (raw, before parsing) |\n * | `error-404` | Resource not found (404) |\n * | `error-5xx` | Server error (500+) |\n * | `fatal-error` | Unhandled/unexpected error |\n *\n * Usage:\n * ```typescript\n * export type MyServiceEventType = EntityServiceEventType;\n * // Or extend with custom events:\n * export type MyServiceEventType = EntityServiceEventType | \"custom-event\";\n * ```\n */\nexport type EntityServiceEventType =\n // Request lifecycle\n | \"busy-changed\"\n | \"request-started\"\n | \"request-finished\"\n | \"request-aborted\"\n // Success\n | \"response-received\"\n | \"raw-response\"\n // Errors\n | \"response-error\"\n | \"raw-error\"\n | \"error-404\"\n | \"error-5xx\"\n | \"fatal-error\";\n\n/**\n * ### EntityServiceEventMap\n *\n * Maps each event type to its detail payload type.\n * This enables type-safe event handling with `storeBindings`.\n *\n * #### Request Lifecycle Events\n * | Event | Detail Type |\n * |--------------------|--------------------------------------------------|\n * | `busy-changed` | `{ busy: boolean }` |\n * | `request-started` | `{ request: unknown }` |\n * | `request-finished` | `{ request: unknown }` |\n * | `request-aborted` | `{ reason: string }` |\n *\n * #### Success Events\n * | Event | Detail Type |\n * |--------------------|--------------------------------------------------|\n * | `response-received`| `{ response: unknown, serverResponse: Response }`|\n * | `raw-response` | `{ serverResponse: Response }` |\n *\n * #### Error Events\n * | Event | Detail Type |\n * |--------------------|--------------------------------------------------|\n * | `response-error` | `{ parsedResponse: unknown, serverResponse: Response }` |\n * | `raw-error` | `{ serverResponse: Response }` |\n * | `error-404` | `{ serverResponse: Response }` |\n * | `error-5xx` | `{ serverResponse: Response }` |\n * | `fatal-error` | `{ error: unknown }` |\n *\n * Usage:\n * ```typescript\n * // Extend with custom events:\n * interface MyServiceEventMap extends EntityServiceEventMap {\n * \"custom-event\": { data: string };\n * }\n * ```\n */\nexport interface EntityServiceEventMap {\n // Request lifecycle\n \"busy-changed\": { busy: boolean };\n \"request-started\": { request: unknown };\n \"request-finished\": { request: unknown };\n \"request-aborted\": { reason: string };\n // Success\n \"response-received\": { response: unknown; serverResponse: Response };\n \"raw-response\": { serverResponse: Response };\n // Errors\n \"response-error\": { parsedResponse: unknown; serverResponse: Response };\n \"raw-error\": { serverResponse: Response };\n \"error-404\": { serverResponse: Response };\n \"error-5xx\": { serverResponse: Response };\n \"fatal-error\": { error: unknown };\n}\n\n/**\n * ### TypedEntityService\n *\n * Type alias for entity services with type-safe event handling.\n * Use this when you need to type a variable or parameter that holds a service.\n *\n * Note: Services extend `EventTarget` at runtime, but this type provides\n * compile-time type safety for event names when used with `storeBindings`.\n *\n * Usage:\n * ```typescript\n * // Type a variable\n * const service: TypedEntityService = cubeEntityService;\n *\n * // With custom events\n * interface MyEventMap extends EntityServiceEventMap {\n * \"custom-event\": { data: string };\n * }\n * const myService: TypedEntityService<MyEventMap> = myEntityService;\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nexport type TypedEntityService<_TEventMap extends EntityServiceEventMap = EntityServiceEventMap> = EventTarget;\n"]}
@@ -0,0 +1,104 @@
1
+ import { LitElement } from "lit";
2
+ import type { ModelEventType } from "./ModelDecorators.js";
3
+ /**
4
+ * Interface for FieldNode-like objects that support event listening.
5
+ */
6
+ export interface FieldNodeLike {
7
+ __addEventListener(type: string, listener: (e: CustomEvent) => void): void;
8
+ __removeEventListener(type: string, listener: (e: CustomEvent) => void): void;
9
+ __meta?: {
10
+ typeName?: string;
11
+ };
12
+ value?: unknown;
13
+ }
14
+ /**
15
+ * Interface for components that bind to FieldNode models.
16
+ *
17
+ * Components implement `modelReaders` and `modelWriters` maps
18
+ * keyed by `__meta.typeName` (e.g., "primitives.STRING", "furo.fat.String").
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * export class MyInput extends LitElement implements BindableComponent {
23
+ * @fieldBindings.model()
24
+ * model: STRING | XString | undefined;
25
+ *
26
+ * // Provided by decorator
27
+ * declare writeToModel: () => void;
28
+ *
29
+ * modelReaders = new Map<string, () => void>([
30
+ * ["primitives.STRING", () => { this.value = (this.model as STRING).value ?? ""; }],
31
+ * ["furo.fat.String", () => { this.value = (this.model as XString).value.value ?? ""; }],
32
+ * ]);
33
+ *
34
+ * modelWriters = new Map<string, () => void>([
35
+ * ["primitives.STRING", () => { (this.model as STRING).value = this.value; }],
36
+ * ["furo.fat.String", () => { (this.model as XString).value.value = this.value; }],
37
+ * ]);
38
+ * }
39
+ * ```
40
+ */
41
+ export interface BindableComponent extends LitElement {
42
+ model: FieldNodeLike | undefined;
43
+ /** Map of typeName → reader function (model → component) */
44
+ modelReaders: Map<string, () => void>;
45
+ /** Map of typeName → writer function (component → model) */
46
+ modelWriters: Map<string, () => void>;
47
+ /** Helper to write current value to model (provided by decorator) */
48
+ writeToModel: () => void;
49
+ }
50
+ /**
51
+ * ### fieldBindings
52
+ *
53
+ * Decorators for creating reusable components that bind to FieldNode models.
54
+ *
55
+ * The component provides `modelReaders` and `modelWriters` maps keyed by
56
+ * `__meta.typeName`. The decorator handles:
57
+ * - Binding/unbinding on model change
58
+ * - Calling the correct reader when model value changes
59
+ * - Providing `writeToModel()` method that calls the correct writer
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * export class MyInput extends LitElement implements BindableComponent {
64
+ * @fieldBindings.model()
65
+ * model: STRING | XString | undefined;
66
+ *
67
+ * declare writeToModel: () => void;
68
+ *
69
+ * modelReaders = new Map([
70
+ * ["primitives.STRING", () => { this.value = (this.model as STRING).value ?? ""; }],
71
+ * ["furo.fat.String", () => { this.value = (this.model as XString).value.value ?? ""; }],
72
+ * ]);
73
+ *
74
+ * modelWriters = new Map([
75
+ * ["primitives.STRING", () => { (this.model as STRING).value = this.value; }],
76
+ * ["furo.fat.String", () => { (this.model as XString).value.value = this.value; }],
77
+ * ]);
78
+ *
79
+ * private _onInput(e: Event) {
80
+ * this.value = (e.target as HTMLInputElement).value;
81
+ * this.writeToModel();
82
+ * }
83
+ * }
84
+ * ```
85
+ */
86
+ export declare const fieldBindings: {
87
+ /**
88
+ * Decorator for the `model` property.
89
+ *
90
+ * Handles:
91
+ * - Binding/unbinding when model changes
92
+ * - Resolving reader/writer functions based on model type
93
+ * - Calling reader on model value changes
94
+ * - Providing `writeToModel()` method
95
+ */
96
+ model(): <T extends FieldNodeLike>(target: object, propertyKey: string) => void;
97
+ /**
98
+ * Binds a method to an event on the model.
99
+ * When the event fires, the method is called with the event detail.
100
+ *
101
+ * @param eventType - The event type to listen for
102
+ */
103
+ onEvent(eventType: ModelEventType): (target: object, propertyKey: string, descriptor: PropertyDescriptor) => void;
104
+ };
@@ -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":"AAsEA,iCAAiC;AACjC,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAEnD,mCAAmC;AACnC,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;AACzD,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;AACrD,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;AACrD,MAAM,cAAc,GAAG,MAAM,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;AACtD,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;AAEpD,oEAAoE;AACpE,8DAA8D;AAC9D,oEAAoE;AAEpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B;;;;;;;;OAQG;IACH,KAAK;QACH,OAAO,SAAS,cAAc,CAA0B,MAAc,EAAE,WAAmB;YACzF,MAAM,IAAI,GAAG,MAAM,CAAC,WAAqC,CAAC;YAE1D,kBAAkB;YAClB,cAAc,CAAC,IAAI,CAAC,CAAC;YAErB,iCAAiC;YACjC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,EAAE,cAAc,CAAC,EAAE,CAAC;gBAClE,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,cAAc,EAAE;oBAC5C,KAAK,EAAE,SAAS,YAAY;wBAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;wBACrC,IAAI,OAAO,EAAE,CAAC;4BACZ,IAAI,CAAC;gCACH,OAAO,EAAE,CAAC;4BACZ,CAAC;4BAAC,OAAO,CAAC,EAAE,CAAC;gCACX,sCAAsC;gCACtC,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,CAAC,CAAC,CAAC;4BAChD,CAAC;wBACH,CAAC;oBACH,CAAC;oBACD,QAAQ,EAAE,KAAK;oBACf,UAAU,EAAE,KAAK;oBACjB,YAAY,EAAE,IAAI;iBACnB,CAAC,CAAC;YACL,CAAC;YAED,8CAA8C;YAC9C,MAAM,CAAC,cAAc,CAAC,MAAM,EAAE,WAAW,EAAE;gBACzC,GAAG;oBACD,OAAO,IAAI,CAAC,aAAa,CAAC,CAAC;gBAC7B,CAAC;gBACD,GAAG,CAAsF,KAAoB;oBAC3G,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAkB,CAAC;oBACtD,IAAI,KAAK,KAAK,QAAQ;wBAAE,OAAO;oBAE/B,wBAAwB;oBACxB,IAAI,QAAQ,EAAE,CAAC;wBACb,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;oBAClC,CAAC;oBAED,kBAAkB;oBAClB,IAAI,CAAC,aAAa,CAAC,GAAG,KAAK,CAAC;oBAE5B,gDAAgD;oBAChD,IAAI,KAAK,EAAE,CAAC;wBACV,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,EAAE,QAAQ,IAAI,mBAAmB,CAAC;wBAE/D,iBAAiB;wBACjB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;wBAChD,IAAI,MAAM,EAAE,CAAC;4BACX,IAAI,CAAC,aAAa,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;wBAC1C,CAAC;6BAAM,CAAC;4BACN,sCAAsC;4BACtC,OAAO,CAAC,IAAI,CAAC,4BAA4B,QAAQ,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;4BACvH,IAAI,CAAC,aAAa,CAAC,GAAG,SAAS,CAAC;wBAClC,CAAC;wBAED,iBAAiB;wBACjB,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,QAAQ,CAAC,CAAC;wBAChD,IAAI,MAAM,EAAE,CAAC;4BACX,IAAI,CAAC,cAAc,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;wBAC3C,CAAC;6BAAM,CAAC;4BACN,sCAAsC;4BACtC,OAAO,CAAC,IAAI,CAAC,4BAA4B,QAAQ,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;4BACvH,IAAI,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;wBACnC,CAAC;oBACH,CAAC;yBAAM,CAAC;wBACN,IAAI,CAAC,aAAa,CAAC,GAAG,SAAS,CAAC;wBAChC,IAAI,CAAC,cAAc,CAAC,GAAG,SAAS,CAAC;oBACnC,CAAC;oBAED,mCAAmC;oBACnC,IAAI,KAAK,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;wBAC9B,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;oBAC3B,CAAC;oBAED,qBAAqB;oBACrB,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,CAAC;gBACD,UAAU,EAAE,IAAI;gBAChB,YAAY,EAAE,IAAI;aACnB,CAAC,CAAC;QACL,CAAC,CAAC;IACJ,CAAC;IAED;;;;;OAKG;IACH,OAAO,CAAC,SAAyB;QAC/B,OAAO,SAAS,gBAAgB,CAAC,MAAc,EAAE,WAAmB,EAAE,UAA8B;YAClG,MAAM,cAAc,GAAG,UAAU,CAAC,KAAK,CAAC;YACxC,MAAM,IAAI,GAAG,MAAM,CAAC,WAAqC,CAAC;YAE1D,IAAI,MAAM,GAAI,IAAoD,CAAC,YAAY,CAAC,CAAC;YACjF,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,GAAG,EAAE,CAAC;gBACX,IAAoD,CAAC,YAAY,CAAC,GAAG,MAAM,CAAC;YAC/E,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,CAAC;YAEhE,cAAc,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC,CAAC;IACJ,CAAC;CACF,CAAC;AAaF;;GAEG;AACH,SAAS,WAAW,CAAC,SAAiC,EAAE,KAAoB;IAC1E,MAAM,IAAI,GAAG,SAAS,CAAC,WAAqC,CAAC;IAC7D,MAAM,MAAM,GAAI,IAAoD,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC;IAEzF,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAC;IACnD,SAAS,CAAC,eAAe,CAAC,GAAG,SAAS,CAAC;IAEvC,qCAAqC;IACrC,MAAM,MAAM,GAAG,SAAS,CAAC,aAAa,CAA6B,CAAC;IAEpE,kDAAkD;IAClD,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,aAAa,GAAG,GAAG,EAAE;YACzB,MAAM,EAAE,CAAC;QACX,CAAC,CAAC;QAEF,SAAS,CAAC,GAAG,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,0BAA0B,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,CAAC;QAC3F,KAAK,CAAC,kBAAkB,CAAC,0BAA0B,EAAE,aAAa,CAAC,CAAC;QAEpE,eAAe;QACf,MAAM,EAAE,CAAC;IACX,CAAC;IAED,+DAA+D;IAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE;QACpD,MAAM,QAAQ,GAAG,CAAC,CAAc,EAAE,EAAE;YAClC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC;QACnC,CAAC,CAAC;QAEF,SAAS,CAAC,GAAG,CAAC,SAAS,WAAW,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC/D,KAAK,CAAC,kBAAkB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;GAEG;AACH,SAAS,eAAe,CAAC,SAAiC,EAAE,KAAoB;IAC9E,MAAM,SAAS,GAAG,SAAS,CAAC,eAAe,CAA2C,CAAC;IACvF,IAAI,CAAC,SAAS;QAAE,OAAO;IAEvB,SAAS,CAAC,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,EAAE;QAC5C,KAAK,CAAC,qBAAqB,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;IACH,SAAS,CAAC,KAAK,EAAE,CAAC;AACpB,CAAC;AAED,oEAAoE;AACpE,qBAAqB;AACrB,oEAAoE;AAEpE;;GAEG;AACH,SAAS,cAAc,CAAC,IAA4B;IAClD,IAAK,IAA2C,CAAC,aAAa,CAAC,EAAE,CAAC;QAChE,OAAO;IACT,CAAC;IACA,IAA2C,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC;IAEnE,MAAM,iBAAiB,GAAG,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC;IAC3D,MAAM,oBAAoB,GAAG,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;IAEjE,IAAI,CAAC,SAAS,CAAC,iBAAiB,GAAG,SAAS,iBAAiB;QAC3D,iBAAiB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAE9B,+BAA+B;QAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAA8B,CAAC;QAC/D,IAAI,KAAK,EAAE,CAAC;YACV,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,CAAC,SAAS,CAAC,oBAAoB,GAAG,SAAS,oBAAoB;QACjE,oBAAoB;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAA8B,CAAC;QAC/D,IAAI,KAAK,EAAE,CAAC;YACV,eAAe,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC/B,CAAC;QAED,oBAAoB,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC,CAAC;AACJ,CAAC","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"]}