@microsoft/fast-html 1.0.0-alpha.29 → 1.0.0-alpha.30
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/README.md +144 -0
- package/dist/dts/components/index.d.ts +1 -1
- package/dist/dts/components/request-idle-callback.d.ts +41 -0
- package/dist/dts/components/template.d.ts +46 -3
- package/dist/dts/fixtures/lifecycle-callbacks/lifecycle-callbacks.spec.d.ts +1 -0
- package/dist/dts/fixtures/lifecycle-callbacks/main.d.ts +5 -0
- package/dist/esm/components/element.js +3 -7
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/components/request-idle-callback.js +72 -0
- package/dist/esm/components/template.js +68 -29
- package/dist/esm/fixtures/lifecycle-callbacks/lifecycle-callbacks.spec.js +166 -0
- package/dist/esm/fixtures/lifecycle-callbacks/main.js +126 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/fast-html.api.json +55 -1
- package/dist/fast-html.d.ts +49 -2
- package/dist/fast-html.untrimmed.d.ts +49 -2
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -73,6 +73,150 @@ RenderableFASTElement(MyCustomElement).defineAsync({
|
|
|
73
73
|
});
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
+
#### Lifecycle Callbacks
|
|
77
|
+
|
|
78
|
+
FAST HTML provides lifecycle callbacks that allow you to hook into various stages of template processing and element hydration. These callbacks are useful for tracking the rendering lifecycle, gathering analytics, or coordinating complex initialization sequences.
|
|
79
|
+
|
|
80
|
+
##### Available Callbacks
|
|
81
|
+
|
|
82
|
+
**Template Lifecycle Callbacks:**
|
|
83
|
+
- `elementDidRegister(name: string)` - Called after the JavaScript class definition has been registered
|
|
84
|
+
- `templateWillUpdate(name: string)` - Called before the template has been evaluated and assigned
|
|
85
|
+
- `templateDidUpdate(name: string)` - Called after the template has been assigned to the definition
|
|
86
|
+
- `elementDidDefine(name: string)` - Called after the custom element has been defined
|
|
87
|
+
|
|
88
|
+
**Hydration Lifecycle Callbacks:**
|
|
89
|
+
- `elementWillHydrate(name: string)` - Called before an element begins hydration
|
|
90
|
+
- `elementDidHydrate(name: string)` - Called after an element completes hydration
|
|
91
|
+
- `hydrationComplete()` - Called after all elements have completed hydration
|
|
92
|
+
|
|
93
|
+
##### Configuring Callbacks
|
|
94
|
+
|
|
95
|
+
Configure lifecycle callbacks using `TemplateElement.config()`:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { TemplateElement, type HydrationLifecycleCallbacks } from "@microsoft/fast-html";
|
|
99
|
+
|
|
100
|
+
// You can configure all callbacks at once
|
|
101
|
+
const callbacks: HydrationLifecycleCallbacks = {
|
|
102
|
+
elementDidRegister(name: string) {
|
|
103
|
+
console.log(`Element registered: ${name}`);
|
|
104
|
+
},
|
|
105
|
+
templateWillUpdate(name: string) {
|
|
106
|
+
console.log(`Template updating: ${name}`);
|
|
107
|
+
},
|
|
108
|
+
templateDidUpdate(name: string) {
|
|
109
|
+
console.log(`Template updated: ${name}`);
|
|
110
|
+
},
|
|
111
|
+
elementDidDefine(name: string) {
|
|
112
|
+
console.log(`Element defined: ${name}`);
|
|
113
|
+
},
|
|
114
|
+
elementWillHydrate(name: string) {
|
|
115
|
+
console.log(`Element will hydrate: ${name}`);
|
|
116
|
+
},
|
|
117
|
+
elementDidHydrate(name: string) {
|
|
118
|
+
console.log(`Element hydrated: ${name}`);
|
|
119
|
+
},
|
|
120
|
+
hydrationComplete() {
|
|
121
|
+
console.log('All elements hydrated');
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
TemplateElement.config(callbacks);
|
|
126
|
+
|
|
127
|
+
// Or configure only the callbacks you need
|
|
128
|
+
TemplateElement.config({
|
|
129
|
+
elementDidHydrate(name: string) {
|
|
130
|
+
console.log(`${name} is ready`);
|
|
131
|
+
},
|
|
132
|
+
hydrationComplete() {
|
|
133
|
+
console.log('Page is interactive');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
##### Lifecycle Order
|
|
139
|
+
|
|
140
|
+
The lifecycle callbacks occur in the following general sequence:
|
|
141
|
+
|
|
142
|
+
1. **Registration Phase**: `elementDidRegister` is called when the element class is registered
|
|
143
|
+
2. **Template Phase**: `templateWillUpdate` → (template processing) → `templateDidUpdate` → `elementDidDefine`
|
|
144
|
+
3. **Hydration Phase**: `elementWillHydrate` → (hydration) → `elementDidHydrate`
|
|
145
|
+
4. **Completion**: `hydrationComplete` is called after all elements finish hydrating
|
|
146
|
+
|
|
147
|
+
**Note:** Template processing is asynchronous and happens independently for each element. The template and hydration phases can be interleaved when multiple elements are being processed simultaneously.
|
|
148
|
+
|
|
149
|
+
##### Use Cases
|
|
150
|
+
|
|
151
|
+
**Performance Monitoring:**
|
|
152
|
+
```typescript
|
|
153
|
+
TemplateElement.config({
|
|
154
|
+
elementWillHydrate(name: string) {
|
|
155
|
+
performance.mark(`${name}-hydration-start`);
|
|
156
|
+
},
|
|
157
|
+
elementDidHydrate(name: string) {
|
|
158
|
+
performance.mark(`${name}-hydration-end`);
|
|
159
|
+
performance.measure(
|
|
160
|
+
`${name}-hydration`,
|
|
161
|
+
`${name}-hydration-start`,
|
|
162
|
+
`${name}-hydration-end`
|
|
163
|
+
);
|
|
164
|
+
},
|
|
165
|
+
hydrationComplete() {
|
|
166
|
+
// Report to analytics
|
|
167
|
+
const entries = performance.getEntriesByType('measure');
|
|
168
|
+
console.log('Hydration metrics:', entries);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Loading State Management:**
|
|
174
|
+
```typescript
|
|
175
|
+
TemplateElement.config({
|
|
176
|
+
elementWillHydrate(name: string) {
|
|
177
|
+
// Show loading indicator
|
|
178
|
+
document.body.classList.add('hydrating');
|
|
179
|
+
},
|
|
180
|
+
hydrationComplete() {
|
|
181
|
+
// Hide loading indicator once all elements are ready
|
|
182
|
+
document.body.classList.remove('hydrating');
|
|
183
|
+
document.body.classList.add('hydrated');
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Debugging and Development:**
|
|
189
|
+
```typescript
|
|
190
|
+
if (process.env.NODE_ENV === 'development') {
|
|
191
|
+
const events: Array<{callback: string; name?: string; timestamp: number}> = [];
|
|
192
|
+
|
|
193
|
+
TemplateElement.config({
|
|
194
|
+
elementDidRegister(name) {
|
|
195
|
+
events.push({ callback: 'elementDidRegister', name, timestamp: Date.now() });
|
|
196
|
+
},
|
|
197
|
+
templateWillUpdate(name) {
|
|
198
|
+
events.push({ callback: 'templateWillUpdate', name, timestamp: Date.now() });
|
|
199
|
+
},
|
|
200
|
+
templateDidUpdate(name) {
|
|
201
|
+
events.push({ callback: 'templateDidUpdate', name, timestamp: Date.now() });
|
|
202
|
+
},
|
|
203
|
+
elementDidDefine(name) {
|
|
204
|
+
events.push({ callback: 'elementDidDefine', name, timestamp: Date.now() });
|
|
205
|
+
},
|
|
206
|
+
elementWillHydrate(name) {
|
|
207
|
+
events.push({ callback: 'elementWillHydrate', name, timestamp: Date.now() });
|
|
208
|
+
},
|
|
209
|
+
elementDidHydrate(name) {
|
|
210
|
+
events.push({ callback: 'elementDidHydrate', name, timestamp: Date.now() });
|
|
211
|
+
},
|
|
212
|
+
hydrationComplete() {
|
|
213
|
+
events.push({ callback: 'hydrationComplete', timestamp: Date.now() });
|
|
214
|
+
console.table(events);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
76
220
|
### Syntax
|
|
77
221
|
|
|
78
222
|
All bindings use a handlebars-like syntax.
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { TemplateElement } from "./template.js";
|
|
2
1
|
export { RenderableFASTElement } from "./element.js";
|
|
3
2
|
export { ObserverMap } from "./observer-map.js";
|
|
3
|
+
export { ObserverMapOption, TemplateElement, type ElementOptions, type ElementOptionsDictionary, type HydrationLifecycleCallbacks, } from "./template.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ponyfill for requestIdleCallback that falls back to setTimeout.
|
|
3
|
+
* Uses the native requestIdleCallback when available.
|
|
4
|
+
*
|
|
5
|
+
* @param callback - The function to call when the browser is idle.
|
|
6
|
+
* @param options - Options object that may contain a timeout property.
|
|
7
|
+
* @returns An ID that can be used to cancel the callback.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export declare function requestIdleCallback(callback: (deadline: IdleDeadline) => void, options?: {
|
|
11
|
+
timeout: number;
|
|
12
|
+
}): ReturnType<typeof globalThis.requestIdleCallback | typeof setTimeout>;
|
|
13
|
+
/**
|
|
14
|
+
* A ponyfill for cancelIdleCallback that falls back to clearTimeout.
|
|
15
|
+
* Uses the native cancelIdleCallback when available.
|
|
16
|
+
*
|
|
17
|
+
* @param id - The ID of the callback to cancel.
|
|
18
|
+
* @public
|
|
19
|
+
*/
|
|
20
|
+
export declare function cancelIdleCallback(id: ReturnType<typeof globalThis.requestIdleCallback | typeof setTimeout>): void;
|
|
21
|
+
/**
|
|
22
|
+
* Waits for all custom element descendants of a target element to be connected.
|
|
23
|
+
*
|
|
24
|
+
* @param target - The target element to observe.
|
|
25
|
+
* @param callback - The function to call when all custom element descendants are connected.
|
|
26
|
+
* @param options - Options object that may contain a timeout property for the idle callback.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* This function uses requestIdleCallback to periodically check if all custom element
|
|
30
|
+
* descendants (elements with a hyphen in their tag name) are connected to the DOM.
|
|
31
|
+
* Once all such elements are connected, the provided callback is invoked.
|
|
32
|
+
*
|
|
33
|
+
* The `timeout` option specifies the maximum time to wait for each idle callback before
|
|
34
|
+
* rechecking, defaulting to 50 milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
export declare function waitForConnectedDescendants(target: HTMLElement, callback: () => void, options?: {
|
|
39
|
+
shallow?: boolean;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}): void;
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import { FASTElement } from "@microsoft/fast-element";
|
|
1
|
+
import { FASTElement, HydrationControllerCallbacks, TemplateLifecycleCallbacks } from "@microsoft/fast-element";
|
|
2
2
|
import "@microsoft/fast-element/install-hydratable-view-templates.js";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Values for the observerMap element option.
|
|
5
|
+
*/
|
|
6
|
+
export declare const ObserverMapOption: {
|
|
7
|
+
readonly all: "all";
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Type for the observerMap element option.
|
|
11
|
+
*/
|
|
12
|
+
export type ObserverMapOption = (typeof ObserverMapOption)[keyof typeof ObserverMapOption];
|
|
13
|
+
/**
|
|
14
|
+
* Element options the TemplateElement will use to update the registered element
|
|
15
|
+
*/
|
|
4
16
|
export interface ElementOptions {
|
|
5
|
-
observerMap?: ObserverMapOption
|
|
17
|
+
observerMap?: ObserverMapOption;
|
|
6
18
|
}
|
|
7
19
|
/**
|
|
8
20
|
* A dictionary of element options the TemplateElement will use to update the registered element
|
|
@@ -10,6 +22,20 @@ export interface ElementOptions {
|
|
|
10
22
|
export interface ElementOptionsDictionary<ElementOptionsType = ElementOptions> {
|
|
11
23
|
[key: string]: ElementOptionsType;
|
|
12
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Lifecycle callbacks for template and hydration events.
|
|
27
|
+
* Combines template lifecycle callbacks with hydration callbacks and adds template-processing events.
|
|
28
|
+
*/
|
|
29
|
+
export interface HydrationLifecycleCallbacks extends HydrationControllerCallbacks, TemplateLifecycleCallbacks {
|
|
30
|
+
/**
|
|
31
|
+
* Called after the JS class definition has been registered
|
|
32
|
+
*/
|
|
33
|
+
elementDidRegister?(name: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* Called before the template has been evaluated and assigned
|
|
36
|
+
*/
|
|
37
|
+
templateWillUpdate?(name: string): void;
|
|
38
|
+
}
|
|
13
39
|
/**
|
|
14
40
|
* The <f-template> custom element that will provide view logic to the element
|
|
15
41
|
*/
|
|
@@ -34,6 +60,23 @@ declare class TemplateElement extends FASTElement {
|
|
|
34
60
|
* Metadata containing JSON schema for properties on a custom eleemnt
|
|
35
61
|
*/
|
|
36
62
|
private schema?;
|
|
63
|
+
/**
|
|
64
|
+
* Lifecycle callbacks for hydration events
|
|
65
|
+
*/
|
|
66
|
+
private static lifecycleCallbacks;
|
|
67
|
+
/**
|
|
68
|
+
* Configure lifecycle callbacks for hydration events.
|
|
69
|
+
*
|
|
70
|
+
* @param callbacks - Lifecycle callbacks to configure.
|
|
71
|
+
* @returns The {@link TemplateElement} class.
|
|
72
|
+
*/
|
|
73
|
+
static config(callbacks: HydrationLifecycleCallbacks): typeof TemplateElement;
|
|
74
|
+
/**
|
|
75
|
+
* Set options for custom elements.
|
|
76
|
+
*
|
|
77
|
+
* @param elementOptions - A dictionary of custom element options
|
|
78
|
+
* @returns The TemplateElement class.
|
|
79
|
+
*/
|
|
37
80
|
static options(elementOptions?: ElementOptionsDictionary): typeof TemplateElement;
|
|
38
81
|
constructor();
|
|
39
82
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -10,16 +10,12 @@ import { attr } from "@microsoft/fast-element";
|
|
|
10
10
|
export function RenderableFASTElement(BaseCtor) {
|
|
11
11
|
const C = class extends BaseCtor {
|
|
12
12
|
constructor(...args) {
|
|
13
|
+
var _a, _b;
|
|
13
14
|
super(...args);
|
|
14
15
|
this.deferHydration = true;
|
|
15
|
-
|
|
16
|
-
this.prepare().then(() => {
|
|
17
|
-
this.deferHydration = false;
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
16
|
+
((_b = (_a = this.prepare) === null || _a === void 0 ? void 0 : _a.call(this)) !== null && _b !== void 0 ? _b : Promise.resolve()).then(() => {
|
|
21
17
|
this.deferHydration = false;
|
|
22
|
-
}
|
|
18
|
+
});
|
|
23
19
|
}
|
|
24
20
|
};
|
|
25
21
|
attr({ mode: "boolean", attribute: "defer-hydration" })(C.prototype, "deferHydration");
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ponyfill for requestIdleCallback that falls back to setTimeout.
|
|
3
|
+
* Uses the native requestIdleCallback when available.
|
|
4
|
+
*
|
|
5
|
+
* @param callback - The function to call when the browser is idle.
|
|
6
|
+
* @param options - Options object that may contain a timeout property.
|
|
7
|
+
* @returns An ID that can be used to cancel the callback.
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export function requestIdleCallback(callback, options) {
|
|
11
|
+
if ("requestIdleCallback" in globalThis) {
|
|
12
|
+
return globalThis.requestIdleCallback(callback, options);
|
|
13
|
+
}
|
|
14
|
+
const start = Date.now();
|
|
15
|
+
return setTimeout(() => {
|
|
16
|
+
callback({
|
|
17
|
+
didTimeout: (options === null || options === void 0 ? void 0 : options.timeout) ? Date.now() - start >= options.timeout : false,
|
|
18
|
+
timeRemaining: () => 0,
|
|
19
|
+
});
|
|
20
|
+
}, 1);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A ponyfill for cancelIdleCallback that falls back to clearTimeout.
|
|
24
|
+
* Uses the native cancelIdleCallback when available.
|
|
25
|
+
*
|
|
26
|
+
* @param id - The ID of the callback to cancel.
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export function cancelIdleCallback(id) {
|
|
30
|
+
if ("cancelIdleCallback" in globalThis) {
|
|
31
|
+
globalThis.cancelIdleCallback(id);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
clearTimeout(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Waits for all custom element descendants of a target element to be connected.
|
|
39
|
+
*
|
|
40
|
+
* @param target - The target element to observe.
|
|
41
|
+
* @param callback - The function to call when all custom element descendants are connected.
|
|
42
|
+
* @param options - Options object that may contain a timeout property for the idle callback.
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* This function uses requestIdleCallback to periodically check if all custom element
|
|
46
|
+
* descendants (elements with a hyphen in their tag name) are connected to the DOM.
|
|
47
|
+
* Once all such elements are connected, the provided callback is invoked.
|
|
48
|
+
*
|
|
49
|
+
* The `timeout` option specifies the maximum time to wait for each idle callback before
|
|
50
|
+
* rechecking, defaulting to 50 milliseconds.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export function waitForConnectedDescendants(target, callback, options) {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
let idleCallbackId;
|
|
57
|
+
const shallow = (_a = options === null || options === void 0 ? void 0 : options.shallow) !== null && _a !== void 0 ? _a : false;
|
|
58
|
+
const query = `${shallow ? ":scope > " : ""}:not(:defined)`;
|
|
59
|
+
const timeout = (_b = options === null || options === void 0 ? void 0 : options.timeout) !== null && _b !== void 0 ? _b : 50;
|
|
60
|
+
const scheduleCheck = (deadline) => {
|
|
61
|
+
if (idleCallbackId) {
|
|
62
|
+
cancelIdleCallback(idleCallbackId);
|
|
63
|
+
idleCallbackId = undefined;
|
|
64
|
+
}
|
|
65
|
+
if (!target.querySelector(query) || (deadline && deadline.timeRemaining() <= 0)) {
|
|
66
|
+
callback();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
idleCallbackId = requestIdleCallback(scheduleCheck, { timeout });
|
|
70
|
+
};
|
|
71
|
+
scheduleCheck();
|
|
72
|
+
}
|
|
@@ -1,13 +1,37 @@
|
|
|
1
1
|
import { __awaiter, __decorate, __metadata } from "tslib";
|
|
2
2
|
import { attr, children, elements, FAST, FASTElement, FASTElementDefinition, fastElementRegistry, HydratableElementController, ref, repeat, slotted, ViewTemplate, when, } from "@microsoft/fast-element";
|
|
3
3
|
import "@microsoft/fast-element/install-hydratable-view-templates.js";
|
|
4
|
-
import { bindingResolver, getExpressionChain, getNextBehavior, getRootPropertyName, resolveWhen, transformInnerHTML, } from "./utilities.js";
|
|
5
4
|
import { ObserverMap } from "./observer-map.js";
|
|
6
5
|
import { Schema } from "./schema.js";
|
|
6
|
+
import { bindingResolver, getExpressionChain, getNextBehavior, getRootPropertyName, resolveWhen, transformInnerHTML, } from "./utilities.js";
|
|
7
|
+
/**
|
|
8
|
+
* Values for the observerMap element option.
|
|
9
|
+
*/
|
|
10
|
+
export const ObserverMapOption = {
|
|
11
|
+
all: "all",
|
|
12
|
+
};
|
|
7
13
|
/**
|
|
8
14
|
* The <f-template> custom element that will provide view logic to the element
|
|
9
15
|
*/
|
|
10
16
|
class TemplateElement extends FASTElement {
|
|
17
|
+
/**
|
|
18
|
+
* Configure lifecycle callbacks for hydration events.
|
|
19
|
+
*
|
|
20
|
+
* @param callbacks - Lifecycle callbacks to configure.
|
|
21
|
+
* @returns The {@link TemplateElement} class.
|
|
22
|
+
*/
|
|
23
|
+
static config(callbacks) {
|
|
24
|
+
TemplateElement.lifecycleCallbacks = callbacks;
|
|
25
|
+
// Pass the hydration-specific callbacks to HydratableElementController
|
|
26
|
+
HydratableElementController.config(Object.assign({}, callbacks));
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Set options for custom elements.
|
|
31
|
+
*
|
|
32
|
+
* @param elementOptions - A dictionary of custom element options
|
|
33
|
+
* @returns The TemplateElement class.
|
|
34
|
+
*/
|
|
11
35
|
static options(elementOptions = {}) {
|
|
12
36
|
const result = {};
|
|
13
37
|
for (const key in elementOptions) {
|
|
@@ -39,35 +63,46 @@ class TemplateElement extends FASTElement {
|
|
|
39
63
|
}
|
|
40
64
|
connectedCallback() {
|
|
41
65
|
super.connectedCallback();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
var _a, _b, _c;
|
|
46
|
-
if (!((_a = TemplateElement.elementOptions) === null || _a === void 0 ? void 0 : _a[this.name])) {
|
|
47
|
-
TemplateElement.setOptions(this.name);
|
|
48
|
-
}
|
|
49
|
-
if (((_b = TemplateElement.elementOptions[this.name]) === null || _b === void 0 ? void 0 : _b.observerMap) ===
|
|
50
|
-
"all") {
|
|
51
|
-
this.observerMap = new ObserverMap(value.prototype, this.schema);
|
|
52
|
-
}
|
|
53
|
-
const registeredFastElement = fastElementRegistry.getByType(value);
|
|
54
|
-
const template = this.getElementsByTagName("template").item(0);
|
|
55
|
-
if (template) {
|
|
56
|
-
const innerHTML = yield transformInnerHTML(this.innerHTML);
|
|
57
|
-
// Cache paths during template processing (pass undefined if observerMap is not available)
|
|
58
|
-
const { strings, values } = yield this.resolveStringsAndValues(null, innerHTML, false, null, 0, this.schema, this.observerMap);
|
|
59
|
-
// Define the root properties cached in the observer map as observable (only if observerMap exists)
|
|
60
|
-
(_c = this.observerMap) === null || _c === void 0 ? void 0 : _c.defineProperties();
|
|
61
|
-
if (registeredFastElement) {
|
|
62
|
-
// all new elements will get the updated template
|
|
63
|
-
registeredFastElement.template = this.resolveTemplateOrBehavior(strings, values);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
throw FAST.error(2000 /* Message.noTemplateProvided */, { name: this.name });
|
|
68
|
-
}
|
|
69
|
-
}));
|
|
66
|
+
const name = this.name;
|
|
67
|
+
if (typeof name !== "string") {
|
|
68
|
+
return;
|
|
70
69
|
}
|
|
70
|
+
this.schema = new Schema(name);
|
|
71
|
+
FASTElementDefinition.registerAsync(name).then((value) => __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
73
|
+
(_b = (_a = TemplateElement.lifecycleCallbacks).elementDidRegister) === null || _b === void 0 ? void 0 : _b.call(_a, name);
|
|
74
|
+
if (!((_c = TemplateElement.elementOptions) === null || _c === void 0 ? void 0 : _c[name])) {
|
|
75
|
+
TemplateElement.setOptions(name);
|
|
76
|
+
}
|
|
77
|
+
if (((_d = TemplateElement.elementOptions[name]) === null || _d === void 0 ? void 0 : _d.observerMap) === "all") {
|
|
78
|
+
this.observerMap = new ObserverMap(value.prototype, this.schema);
|
|
79
|
+
}
|
|
80
|
+
const registeredFastElement = fastElementRegistry.getByType(value);
|
|
81
|
+
const template = this.getElementsByTagName("template").item(0);
|
|
82
|
+
if (template) {
|
|
83
|
+
// Callback: Before template has been evaluated and assigned
|
|
84
|
+
(_f = (_e = TemplateElement.lifecycleCallbacks).templateWillUpdate) === null || _f === void 0 ? void 0 : _f.call(_e, name);
|
|
85
|
+
const innerHTML = transformInnerHTML(this.innerHTML);
|
|
86
|
+
// Cache paths during template processing (pass undefined if observerMap is not available)
|
|
87
|
+
const { strings, values } = yield this.resolveStringsAndValues(null, innerHTML, false, null, 0, this.schema, this.observerMap);
|
|
88
|
+
// Define the root properties cached in the observer map as observable (only if observerMap exists)
|
|
89
|
+
(_g = this.observerMap) === null || _g === void 0 ? void 0 : _g.defineProperties();
|
|
90
|
+
if (registeredFastElement) {
|
|
91
|
+
// Attach lifecycle callbacks to the definition before assigning template
|
|
92
|
+
// This allows the Observable notification to trigger the callbacks
|
|
93
|
+
registeredFastElement.lifecycleCallbacks = {
|
|
94
|
+
templateDidUpdate: TemplateElement.lifecycleCallbacks.templateDidUpdate,
|
|
95
|
+
elementDidDefine: TemplateElement.lifecycleCallbacks.elementDidDefine,
|
|
96
|
+
};
|
|
97
|
+
// All new elements will get the updated template
|
|
98
|
+
// This assignment triggers the Observable notification → callbacks fire
|
|
99
|
+
registeredFastElement.template = this.resolveTemplateOrBehavior(strings, values);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
throw FAST.error(2000 /* Message.noTemplateProvided */, { name: this.name });
|
|
104
|
+
}
|
|
105
|
+
}));
|
|
71
106
|
}
|
|
72
107
|
/**
|
|
73
108
|
* Resolve strings and values from an innerHTML string
|
|
@@ -265,6 +300,10 @@ TemplateElement.elementOptions = {};
|
|
|
265
300
|
* Default element options
|
|
266
301
|
*/
|
|
267
302
|
TemplateElement.defaultElementOptions = {};
|
|
303
|
+
/**
|
|
304
|
+
* Lifecycle callbacks for hydration events
|
|
305
|
+
*/
|
|
306
|
+
TemplateElement.lifecycleCallbacks = {};
|
|
268
307
|
__decorate([
|
|
269
308
|
attr,
|
|
270
309
|
__metadata("design:type", String)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { expect, test } from "@playwright/test";
|
|
3
|
+
test.describe("Lifecycle Callbacks", () => __awaiter(void 0, void 0, void 0, function* () {
|
|
4
|
+
test("should invoke all lifecycle callbacks in correct order for simple element", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
5
|
+
yield page.goto("/lifecycle-callbacks");
|
|
6
|
+
// Wait for hydration to complete
|
|
7
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
8
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
9
|
+
// Verify callbacks for simple-element were called
|
|
10
|
+
const simpleElementEvents = events.filter((e) => e.name === "simple-element");
|
|
11
|
+
expect(simpleElementEvents.length).toBeGreaterThan(0);
|
|
12
|
+
// Check that callbacks were called in the correct order
|
|
13
|
+
const callbackOrder = simpleElementEvents.map((e) => e.callback);
|
|
14
|
+
expect(callbackOrder).toContain("elementDidRegister");
|
|
15
|
+
expect(callbackOrder).toContain("templateWillUpdate");
|
|
16
|
+
expect(callbackOrder).toContain("templateDidUpdate");
|
|
17
|
+
expect(callbackOrder).toContain("elementDidDefine");
|
|
18
|
+
expect(callbackOrder).toContain("elementWillHydrate");
|
|
19
|
+
expect(callbackOrder).toContain("elementDidHydrate");
|
|
20
|
+
// Verify the order is correct
|
|
21
|
+
const registerIndex = callbackOrder.indexOf("elementDidRegister");
|
|
22
|
+
const willUpdateIndex = callbackOrder.indexOf("templateWillUpdate");
|
|
23
|
+
const didUpdateIndex = callbackOrder.indexOf("templateDidUpdate");
|
|
24
|
+
const defineIndex = callbackOrder.indexOf("elementDidDefine");
|
|
25
|
+
const willHydrateIndex = callbackOrder.indexOf("elementWillHydrate");
|
|
26
|
+
const didHydrateIndex = callbackOrder.indexOf("elementDidHydrate");
|
|
27
|
+
expect(registerIndex).toBeLessThan(willUpdateIndex);
|
|
28
|
+
expect(willUpdateIndex).toBeLessThan(didUpdateIndex);
|
|
29
|
+
expect(didUpdateIndex).toBeLessThan(defineIndex);
|
|
30
|
+
expect(willHydrateIndex).toBeGreaterThan(-1);
|
|
31
|
+
expect(didHydrateIndex).toBeGreaterThan(willHydrateIndex);
|
|
32
|
+
}));
|
|
33
|
+
test("should invoke callbacks for multiple elements", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
34
|
+
yield page.goto("/lifecycle-callbacks");
|
|
35
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
36
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
37
|
+
// Check that callbacks were invoked for all elements
|
|
38
|
+
const elementNames = new Set(events.map((e) => e.name));
|
|
39
|
+
expect(elementNames.has("simple-element")).toBe(true);
|
|
40
|
+
expect(elementNames.has("complex-element")).toBe(true);
|
|
41
|
+
expect(elementNames.has("nested-element")).toBe(true);
|
|
42
|
+
}));
|
|
43
|
+
test("should invoke elementWillHydrate before hydration", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
44
|
+
yield page.goto("/lifecycle-callbacks");
|
|
45
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
46
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
47
|
+
const willHydrateEvents = events.filter((e) => e.callback === "elementWillHydrate");
|
|
48
|
+
const didHydrateEvents = events.filter((e) => e.callback === "elementDidHydrate");
|
|
49
|
+
// Every elementWillHydrate should be followed by elementDidHydrate for the same element
|
|
50
|
+
willHydrateEvents.forEach((willEvent) => {
|
|
51
|
+
const correspondingDidEvent = didHydrateEvents.find((e) => e.name === willEvent.name);
|
|
52
|
+
expect(correspondingDidEvent).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
}));
|
|
55
|
+
test("should invoke hydrationComplete callback after all elements hydrate", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
56
|
+
yield page.goto("/lifecycle-callbacks");
|
|
57
|
+
const hydrationComplete = yield page.waitForFunction(() => window.getHydrationCompleteStatus(), { timeout: 5000 });
|
|
58
|
+
expect(hydrationComplete).toBeTruthy();
|
|
59
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
60
|
+
// Verify hydrationComplete callback was invoked
|
|
61
|
+
const hydrationCompleteEvents = events.filter((e) => e.callback === "hydrationComplete");
|
|
62
|
+
expect(hydrationCompleteEvents.length).toBe(1);
|
|
63
|
+
// Verify all elements are hydrated
|
|
64
|
+
const simpleElement = yield page.locator("simple-element");
|
|
65
|
+
const complexElement = yield page.locator("complex-element");
|
|
66
|
+
const nestedElement = yield page.locator("nested-element");
|
|
67
|
+
yield expect(simpleElement).not.toHaveAttribute("needs-hydration");
|
|
68
|
+
yield expect(complexElement).not.toHaveAttribute("needs-hydration");
|
|
69
|
+
yield expect(nestedElement).not.toHaveAttribute("needs-hydration");
|
|
70
|
+
// Verify hydrationComplete was called AFTER all individual element hydrations
|
|
71
|
+
const lastElementDidHydrateIndex = events.reduce((maxIndex, e, index) => {
|
|
72
|
+
return e.callback === "elementDidHydrate" ? index : maxIndex;
|
|
73
|
+
}, -1);
|
|
74
|
+
const hydrationCompleteIndex = events.findIndex((e) => e.callback === "hydrationComplete");
|
|
75
|
+
expect(hydrationCompleteIndex).toBeGreaterThan(lastElementDidHydrateIndex);
|
|
76
|
+
}));
|
|
77
|
+
test("should handle complex element with observer map", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
78
|
+
yield page.goto("/lifecycle-callbacks");
|
|
79
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
80
|
+
const complexElement = yield page.locator("complex-element");
|
|
81
|
+
// Verify the element has hydrated correctly
|
|
82
|
+
yield expect(complexElement).toHaveText(/Complex/);
|
|
83
|
+
yield expect(complexElement).not.toHaveAttribute("needs-hydration");
|
|
84
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
85
|
+
const complexEvents = events.filter((e) => e.name === "complex-element");
|
|
86
|
+
// Complex element should have all lifecycle callbacks
|
|
87
|
+
expect(complexEvents.length).toBeGreaterThan(0);
|
|
88
|
+
expect(complexEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
|
|
89
|
+
}));
|
|
90
|
+
test("should handle nested elements correctly", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
91
|
+
yield page.goto("/lifecycle-callbacks");
|
|
92
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
93
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
94
|
+
// Both parent complex-element and nested nested-element should have callbacks
|
|
95
|
+
const complexEvents = events.filter((e) => e.name === "complex-element");
|
|
96
|
+
const nestedEvents = events.filter((e) => e.name === "nested-element");
|
|
97
|
+
expect(complexEvents.length).toBeGreaterThan(0);
|
|
98
|
+
expect(nestedEvents.length).toBeGreaterThan(0);
|
|
99
|
+
// Both should have hydrated
|
|
100
|
+
expect(complexEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
|
|
101
|
+
expect(nestedEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
|
|
102
|
+
}));
|
|
103
|
+
test("should handle deferred hydration element", ({ page }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
104
|
+
yield page.goto("/lifecycle-callbacks");
|
|
105
|
+
// The deferred element has a prepare() method that delays hydration
|
|
106
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus(), {
|
|
107
|
+
timeout: 5000,
|
|
108
|
+
});
|
|
109
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
110
|
+
const deferredEvents = events.filter((e) => e.name === "deferred-element");
|
|
111
|
+
// Should have hydration callbacks
|
|
112
|
+
expect(deferredEvents.some((e) => e.callback === "elementWillHydrate")).toBe(true);
|
|
113
|
+
expect(deferredEvents.some((e) => e.callback === "elementDidHydrate")).toBe(true);
|
|
114
|
+
const deferredElement = yield page.locator("deferred-element");
|
|
115
|
+
yield expect(deferredElement).not.toHaveAttribute("needs-hydration");
|
|
116
|
+
yield expect(deferredElement).not.toHaveAttribute("defer-hydration");
|
|
117
|
+
}));
|
|
118
|
+
test("should verify template and hydration callbacks are invoked", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
119
|
+
yield page.goto("/lifecycle-callbacks");
|
|
120
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
121
|
+
const events = yield page.evaluate(() => window.lifecycleEvents);
|
|
122
|
+
// For each element, verify both template and hydration callbacks are invoked
|
|
123
|
+
// Note: These callbacks can be interleaved as template processing is async
|
|
124
|
+
const elementNames = ["simple-element", "complex-element", "nested-element"];
|
|
125
|
+
elementNames.forEach(name => {
|
|
126
|
+
const elementEvents = events.filter((e) => e.name === name);
|
|
127
|
+
const callbacks = elementEvents.map((e) => e.callback);
|
|
128
|
+
// Verify all expected callbacks were invoked
|
|
129
|
+
expect(callbacks).toContain("elementDidRegister");
|
|
130
|
+
expect(callbacks).toContain("templateWillUpdate");
|
|
131
|
+
expect(callbacks).toContain("templateDidUpdate");
|
|
132
|
+
expect(callbacks).toContain("elementDidDefine");
|
|
133
|
+
expect(callbacks).toContain("elementWillHydrate");
|
|
134
|
+
expect(callbacks).toContain("elementDidHydrate");
|
|
135
|
+
// Verify hydration callbacks are in order
|
|
136
|
+
const willHydrateIndex = callbacks.indexOf("elementWillHydrate");
|
|
137
|
+
const didHydrateIndex = callbacks.indexOf("elementDidHydrate");
|
|
138
|
+
expect(willHydrateIndex).toBeLessThan(didHydrateIndex);
|
|
139
|
+
// Verify template callbacks are in order
|
|
140
|
+
const registerIndex = callbacks.indexOf("elementDidRegister");
|
|
141
|
+
const willUpdateIndex = callbacks.indexOf("templateWillUpdate");
|
|
142
|
+
const didUpdateIndex = callbacks.indexOf("templateDidUpdate");
|
|
143
|
+
const defineIndex = callbacks.indexOf("elementDidDefine");
|
|
144
|
+
expect(registerIndex).toBeLessThan(willUpdateIndex);
|
|
145
|
+
expect(willUpdateIndex).toBeLessThan(didUpdateIndex);
|
|
146
|
+
expect(didUpdateIndex).toBeLessThan(defineIndex);
|
|
147
|
+
});
|
|
148
|
+
}));
|
|
149
|
+
test("should properly hydrate elements and maintain functionality", ({ page, }) => __awaiter(void 0, void 0, void 0, function* () {
|
|
150
|
+
yield page.goto("/lifecycle-callbacks");
|
|
151
|
+
yield page.waitForFunction(() => window.getHydrationCompleteStatus());
|
|
152
|
+
const simpleElement = yield page.locator("simple-element");
|
|
153
|
+
const complexElement = yield page.locator("complex-element");
|
|
154
|
+
// Verify simple element displays correctly
|
|
155
|
+
yield expect(simpleElement).toHaveText("Hello World");
|
|
156
|
+
// Verify complex element displays correctly
|
|
157
|
+
yield expect(complexElement).toHaveText(/Complex/);
|
|
158
|
+
// Verify elements are interactive after hydration
|
|
159
|
+
const currentCount = yield complexElement.evaluate((el) => el.count);
|
|
160
|
+
expect(currentCount).toBe(0);
|
|
161
|
+
// Interact with the element
|
|
162
|
+
yield complexElement.evaluate((el) => el.increment());
|
|
163
|
+
const newCount = yield complexElement.evaluate((el) => el.count);
|
|
164
|
+
expect(newCount).toBe(1);
|
|
165
|
+
}));
|
|
166
|
+
}));
|