@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.
- package/custom-elements.json +941 -608
- package/dist/Registry.d.ts +7 -0
- package/dist/Registry.js +9 -0
- package/dist/Registry.js.map +1 -1
- package/dist/decorators/EntityServiceTypes.d.ts +133 -0
- package/dist/decorators/EntityServiceTypes.js +2 -0
- package/dist/decorators/EntityServiceTypes.js.map +1 -0
- package/dist/decorators/FieldBindings.d.ts +104 -0
- package/dist/decorators/FieldBindings.js +229 -0
- package/dist/decorators/FieldBindings.js.map +1 -0
- package/dist/decorators/ModelDecorators.d.ts +100 -0
- package/dist/decorators/ModelDecorators.js +227 -0
- package/dist/decorators/ModelDecorators.js.map +1 -0
- package/dist/decorators/SchemaBuilder.d.ts +15 -0
- package/dist/decorators/SchemaBuilder.js +89 -0
- package/dist/decorators/SchemaBuilder.js.map +1 -0
- package/dist/decorators/ServiceDecorators.d.ts +79 -0
- package/dist/decorators/ServiceDecorators.js +203 -0
- package/dist/decorators/ServiceDecorators.js.map +1 -0
- package/dist/decorators/defaultServiceEventHandlers.d.ts +89 -0
- package/dist/decorators/defaultServiceEventHandlers.js +100 -0
- package/dist/decorators/defaultServiceEventHandlers.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/Registry.d.ts
CHANGED
|
@@ -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
|
package/dist/Registry.js.map
CHANGED
|
@@ -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 @@
|
|
|
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":"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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"]}
|