@microsoft/fast-element 2.7.0 → 2.8.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/CHANGELOG.json CHANGED
@@ -2,7 +2,22 @@
2
2
  "name": "@microsoft/fast-element",
3
3
  "entries": [
4
4
  {
5
- "date": "Wed, 20 Aug 2025 20:56:28 GMT",
5
+ "date": "Mon, 13 Oct 2025 00:36:35 GMT",
6
+ "version": "2.8.0",
7
+ "tag": "@microsoft/fast-element_v2.8.0",
8
+ "comments": {
9
+ "minor": [
10
+ {
11
+ "author": "863023+radium-v@users.noreply.github.com",
12
+ "package": "@microsoft/fast-element",
13
+ "commit": "28a1d5dcae41f04e2ff8cb9114c312efb88fb1e1",
14
+ "comment": "[feat]: implement lifecycle callbacks for hydration and template events"
15
+ }
16
+ ]
17
+ }
18
+ },
19
+ {
20
+ "date": "Wed, 20 Aug 2025 20:57:02 GMT",
6
21
  "version": "2.7.0",
7
22
  "tag": "@microsoft/fast-element_v2.7.0",
8
23
  "comments": {
package/CHANGELOG.md CHANGED
@@ -1,12 +1,20 @@
1
1
  # Change Log - @microsoft/fast-element
2
2
 
3
- <!-- This log was last generated on Wed, 20 Aug 2025 20:56:28 GMT and should not be manually modified. -->
3
+ <!-- This log was last generated on Mon, 13 Oct 2025 00:36:35 GMT and should not be manually modified. -->
4
4
 
5
5
  <!-- Start content -->
6
6
 
7
+ ## 2.8.0
8
+
9
+ Mon, 13 Oct 2025 00:36:35 GMT
10
+
11
+ ### Minor changes
12
+
13
+ - [feat]: implement lifecycle callbacks for hydration and template events (863023+radium-v@users.noreply.github.com)
14
+
7
15
  ## 2.7.0
8
16
 
9
- Wed, 20 Aug 2025 20:56:28 GMT
17
+ Wed, 20 Aug 2025 20:57:02 GMT
10
18
 
11
19
  ### Minor changes
12
20
 
@@ -206,6 +206,24 @@ export declare class StyleElementStrategy implements StyleStrategy {
206
206
  }
207
207
  export declare const deferHydrationAttribute = "defer-hydration";
208
208
  export declare const needsHydrationAttribute = "needs-hydration";
209
+ /**
210
+ * Lifecycle callbacks for element hydration events
211
+ * @public
212
+ */
213
+ export interface HydrationControllerCallbacks {
214
+ /**
215
+ * Called before hydration has started
216
+ */
217
+ elementWillHydrate?(name: string): void;
218
+ /**
219
+ * Called after hydration has finished
220
+ */
221
+ elementDidHydrate?(name: string): void;
222
+ /**
223
+ * Called after all elements have completed hydration
224
+ */
225
+ hydrationComplete?(): void;
226
+ }
209
227
  /**
210
228
  * An ElementController capable of hydrating FAST elements from
211
229
  * Declarative Shadow DOM.
@@ -220,7 +238,19 @@ export declare class HydratableElementController<TElement extends HTMLElement =
220
238
  */
221
239
  protected needsHydration?: boolean;
222
240
  private static hydrationObserver;
241
+ /**
242
+ * Lifecycle callbacks for hydration events
243
+ */
244
+ private static lifecycleCallbacks?;
245
+ /**
246
+ * Configure lifecycle callbacks for hydration events
247
+ */
248
+ static config(callbacks: HydrationControllerCallbacks): typeof HydratableElementController;
223
249
  private static hydrationObserverHandler;
250
+ /**
251
+ * Checks if all elements have completed hydration and dispatches event if complete
252
+ */
253
+ private static checkHydrationComplete;
224
254
  static forCustomElement(element: HTMLElement, override?: boolean): ElementController<HTMLElement>;
225
255
  connect(): void;
226
256
  disconnect(): void;
@@ -22,10 +22,31 @@ export interface ShadowRootOptions extends ShadowRootInit {
22
22
  registry?: CustomElementRegistry;
23
23
  }
24
24
  /**
25
- * Template options.
25
+ * Values for the `templateOptions` property.
26
26
  * @alpha
27
27
  */
28
- export declare type TemplateOptions = "defer-and-hydrate";
28
+ export declare const TemplateOptions: {
29
+ readonly deferAndHydrate: "defer-and-hydrate";
30
+ };
31
+ /**
32
+ * Type for the `TemplateOptions` const enum.
33
+ * @alpha
34
+ */
35
+ export declare type TemplateOptions = (typeof TemplateOptions)[keyof typeof TemplateOptions];
36
+ /**
37
+ * Lifecycle callbacks for template events.
38
+ * @public
39
+ */
40
+ export interface TemplateLifecycleCallbacks {
41
+ /**
42
+ * Called after the template has been assigned to the definition
43
+ */
44
+ templateDidUpdate?(name: string): void;
45
+ /**
46
+ * Called after the custom element has been defined
47
+ */
48
+ elementDidDefine?(name: string): void;
49
+ }
29
50
  /**
30
51
  * Represents metadata configuration for a custom element.
31
52
  * @public
@@ -69,6 +90,10 @@ export interface PartialFASTElementDefinition {
69
90
  * If not provided, defaults to the global registry.
70
91
  */
71
92
  readonly registry?: CustomElementRegistry;
93
+ /**
94
+ * Lifecycle callbacks for template events.
95
+ */
96
+ readonly lifecycleCallbacks?: TemplateLifecycleCallbacks;
72
97
  }
73
98
  /**
74
99
  * Defines metadata for a FASTElement.
@@ -125,6 +150,10 @@ export declare class FASTElementDefinition<TType extends Constructable<HTMLEleme
125
150
  * The registry to register this component in by default.
126
151
  */
127
152
  readonly registry: CustomElementRegistry;
153
+ /**
154
+ * Lifecycle callbacks for template events.
155
+ */
156
+ readonly lifecycleCallbacks?: TemplateLifecycleCallbacks;
128
157
  /**
129
158
  * The definition has been registered to the FAST element registry.
130
159
  */
@@ -29,6 +29,6 @@ export { ElementView, HTMLView, SyntheticView, View, HydratableView, HydrationBi
29
29
  export { elements, ElementsFilter, NodeBehaviorOptions, NodeObservationDirective, } from "./templating/node-observation.js";
30
30
  export { render, RenderBehavior, RenderDirective } from "./templating/render.js";
31
31
  export { customElement, FASTElement } from "./components/fast-element.js";
32
- export { FASTElementDefinition, PartialFASTElementDefinition, ShadowRootOptions, fastElementRegistry, TemplateOptions, TypeRegistry, } from "./components/fast-definitions.js";
32
+ export { FASTElementDefinition, PartialFASTElementDefinition, ShadowRootOptions, fastElementRegistry, TemplateOptions, TypeRegistry, type TemplateLifecycleCallbacks, } from "./components/fast-definitions.js";
33
33
  export { attr, AttributeConfiguration, AttributeDefinition, AttributeMode, booleanConverter, DecoratorAttributeConfiguration, nullableBooleanConverter, nullableNumberConverter, ValueConverter, } from "./components/attributes.js";
34
- export { ElementController, ElementControllerStrategy, HydratableElementController, } from "./components/element-controller.js";
34
+ export { ElementController, ElementControllerStrategy, HydratableElementController, type HydrationControllerCallbacks, } from "./components/element-controller.js";
@@ -4,7 +4,7 @@ import { ExecutionContext, Observable, SourceLifetime, } from "../observation/ob
4
4
  import { FAST, makeSerializationNoop } from "../platform.js";
5
5
  import { ElementStyles } from "../styles/element-styles.js";
6
6
  import { UnobservableMutationObserver } from "../utilities.js";
7
- import { FASTElementDefinition } from "./fast-definitions.js";
7
+ import { FASTElementDefinition, TemplateOptions, } from "./fast-definitions.js";
8
8
  import { HydrationMarkup, isHydratable } from "./hydration.js";
9
9
  const defaultEventOptions = {
10
10
  bubbles: true,
@@ -591,24 +591,39 @@ export const needsHydrationAttribute = "needs-hydration";
591
591
  * @beta
592
592
  */
593
593
  export class HydratableElementController extends ElementController {
594
+ /**
595
+ * Configure lifecycle callbacks for hydration events
596
+ */
597
+ static config(callbacks) {
598
+ HydratableElementController.lifecycleCallbacks = callbacks;
599
+ return this;
600
+ }
594
601
  static hydrationObserverHandler(records) {
595
602
  for (const record of records) {
596
603
  HydratableElementController.hydrationObserver.unobserve(record.target);
597
604
  record.target.$fastController.connect();
598
605
  }
599
606
  }
607
+ /**
608
+ * Checks if all elements have completed hydration and dispatches event if complete
609
+ */
610
+ static checkHydrationComplete() {
611
+ var _a, _b;
612
+ if (!document.querySelector(`[${needsHydrationAttribute}]`)) {
613
+ (_b = (_a = HydratableElementController.lifecycleCallbacks) === null || _a === void 0 ? void 0 : _a.hydrationComplete) === null || _b === void 0 ? void 0 : _b.call(_a);
614
+ }
615
+ }
600
616
  static forCustomElement(element, override) {
601
617
  const definition = FASTElementDefinition.getForInstance(element);
602
- if (definition !== undefined &&
603
- definition.templateOptions === "defer-and-hydrate" &&
618
+ if ((definition === null || definition === void 0 ? void 0 : definition.templateOptions) === TemplateOptions.deferAndHydrate &&
604
619
  !definition.template) {
605
- element.setAttribute(deferHydrationAttribute, "");
606
- element.setAttribute(needsHydrationAttribute, "");
620
+ element.toggleAttribute(deferHydrationAttribute, true);
621
+ element.toggleAttribute(needsHydrationAttribute, true);
607
622
  }
608
623
  return super.forCustomElement(element, override);
609
624
  }
610
625
  connect() {
611
- var _a, _b;
626
+ var _a, _b, _c, _d, _e, _f;
612
627
  // Initialize needsHydration on first connect
613
628
  if (this.needsHydration === undefined) {
614
629
  this.needsHydration =
@@ -633,11 +648,13 @@ export class HydratableElementController extends ElementController {
633
648
  if (this.stage !== 3 /* Stages.disconnected */) {
634
649
  return;
635
650
  }
651
+ // Callback: Before hydration has started
652
+ (_b = (_a = HydratableElementController.lifecycleCallbacks) === null || _a === void 0 ? void 0 : _a.elementWillHydrate) === null || _b === void 0 ? void 0 : _b.call(_a, this.definition.name);
636
653
  this.stage = 0 /* Stages.connecting */;
637
654
  this.bindObservables();
638
655
  this.connectBehaviors();
639
656
  const element = this.source;
640
- const host = (_a = getShadowRoot(element)) !== null && _a !== void 0 ? _a : element;
657
+ const host = (_c = getShadowRoot(element)) !== null && _c !== void 0 ? _c : element;
641
658
  if (this.template) {
642
659
  if (isHydratable(this.template)) {
643
660
  let firstChild = host.firstChild;
@@ -654,7 +671,7 @@ export class HydratableElementController extends ElementController {
654
671
  }
655
672
  }
656
673
  this.view = this.template.hydrate(firstChild, lastChild, element);
657
- (_b = this.view) === null || _b === void 0 ? void 0 : _b.bind(this.source);
674
+ (_d = this.view) === null || _d === void 0 ? void 0 : _d.bind(this.source);
658
675
  }
659
676
  else {
660
677
  this.renderTemplate(this.template);
@@ -665,6 +682,10 @@ export class HydratableElementController extends ElementController {
665
682
  this.source.removeAttribute(needsHydrationAttribute);
666
683
  this.needsInitialization = this.needsHydration = false;
667
684
  Observable.notify(this, isConnectedPropertyName);
685
+ // Callback: After hydration has finished
686
+ (_f = (_e = HydratableElementController.lifecycleCallbacks) === null || _e === void 0 ? void 0 : _e.elementDidHydrate) === null || _f === void 0 ? void 0 : _f.call(_e, this.definition.name);
687
+ // Check if hydration is complete after this element is hydrated
688
+ HydratableElementController.checkHydrationComplete();
668
689
  }
669
690
  disconnect() {
670
691
  super.disconnect();
@@ -21,6 +21,13 @@ const fastElementBaseTypes = new Set();
21
21
  * @internal
22
22
  */
23
23
  export const fastElementRegistry = FAST.getById(KernelServiceId.elementRegistry, () => createTypeRegistry());
24
+ /**
25
+ * Values for the `templateOptions` property.
26
+ * @alpha
27
+ */
28
+ export const TemplateOptions = {
29
+ deferAndHydrate: "defer-and-hydrate",
30
+ };
24
31
  /**
25
32
  * Defines metadata for a FASTElement.
26
33
  * @public
@@ -84,10 +91,12 @@ export class FASTElementDefinition {
84
91
  * This operation is idempotent per registry.
85
92
  */
86
93
  define(registry = this.registry) {
94
+ var _b, _c;
87
95
  const type = this.type;
88
96
  if (!registry.get(this.name)) {
89
97
  this.platformDefined = true;
90
98
  registry.define(this.name, type, this.elementOptions);
99
+ (_c = (_b = this.lifecycleCallbacks) === null || _b === void 0 ? void 0 : _b.elementDidDefine) === null || _c === void 0 ? void 0 : _c.call(_b, this.name);
91
100
  }
92
101
  return this;
93
102
  }
@@ -128,15 +137,13 @@ export class FASTElementDefinition {
128
137
  }, nameOrDef));
129
138
  }
130
139
  const definition = new FASTElementDefinition(type, nameOrDef);
131
- Promise.all([
132
- new Promise(resolve => {
133
- Observable.getNotifier(definition).subscribe({
134
- handleChange: () => resolve(),
135
- }, "template");
136
- }),
137
- ]).then(() => {
138
- resolve(definition);
139
- });
140
+ Observable.getNotifier(definition).subscribe({
141
+ handleChange: () => {
142
+ var _b, _c;
143
+ (_c = (_b = definition.lifecycleCallbacks) === null || _b === void 0 ? void 0 : _b.templateDidUpdate) === null || _c === void 0 ? void 0 : _c.call(_b, definition.name);
144
+ resolve(definition);
145
+ },
146
+ }, "template");
140
147
  });
141
148
  }
142
149
  }
@@ -165,9 +172,7 @@ FASTElementDefinition.registerAsync = (name) => __awaiter(void 0, void 0, void 0
165
172
  if (FASTElementDefinition.isRegistered[name]) {
166
173
  resolve(FASTElementDefinition.isRegistered[name]);
167
174
  }
168
- Observable.getNotifier(FASTElementDefinition.isRegistered).subscribe({
169
- handleChange: () => resolve(FASTElementDefinition.isRegistered[name]),
170
- }, name);
175
+ Observable.getNotifier(FASTElementDefinition.isRegistered).subscribe({ handleChange: () => resolve(FASTElementDefinition.isRegistered[name]) }, name);
171
176
  });
172
177
  });
173
178
  Observable.defineProperty(FASTElementDefinition.prototype, "template");
@@ -1,3 +1,12 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { isFunction } from "../interfaces.js";
2
11
  import { ElementController } from "./element-controller.js";
3
12
  import { FASTElementDefinition, } from "./fast-definitions.js";
@@ -32,21 +41,11 @@ function compose(type, nameOrDef) {
32
41
  return FASTElementDefinition.compose(this, type);
33
42
  }
34
43
  function defineAsync(type, nameOrDef) {
35
- if (isFunction(type)) {
36
- return new Promise(resolve => {
37
- FASTElementDefinition.composeAsync(type, nameOrDef).then(value => {
38
- resolve(value);
39
- });
40
- }).then(value => {
41
- return value.define().type;
42
- });
43
- }
44
- return new Promise(resolve => {
45
- FASTElementDefinition.composeAsync(this, type).then(value => {
46
- resolve(value);
47
- });
48
- }).then(value => {
49
- return value.define().type;
44
+ return __awaiter(this, void 0, void 0, function* () {
45
+ if (isFunction(type)) {
46
+ return (yield FASTElementDefinition.composeAsync(type, nameOrDef)).define().type;
47
+ }
48
+ return (yield FASTElementDefinition.composeAsync(this, type)).define().type;
50
49
  });
51
50
  }
52
51
  function define(type, nameOrDef) {
package/dist/esm/index.js CHANGED
@@ -34,6 +34,6 @@ export { elements, NodeObservationDirective, } from "./templating/node-observati
34
34
  export { render, RenderBehavior, RenderDirective } from "./templating/render.js";
35
35
  // Components
36
36
  export { customElement, FASTElement } from "./components/fast-element.js";
37
- export { FASTElementDefinition, fastElementRegistry, } from "./components/fast-definitions.js";
37
+ export { FASTElementDefinition, fastElementRegistry, TemplateOptions, } from "./components/fast-definitions.js";
38
38
  export { attr, AttributeConfiguration, AttributeDefinition, booleanConverter, nullableBooleanConverter, nullableNumberConverter, } from "./components/attributes.js";
39
39
  export { ElementController, HydratableElementController, } from "./components/element-controller.js";
@@ -360,7 +360,7 @@ export class HydrationView extends DefaultExecutionContext {
360
360
  fragment.appendChild(end);
361
361
  }
362
362
  bind(source, context = this) {
363
- var _b, _c;
363
+ var _b;
364
364
  if (this.hydrationStage !== HydrationStage.hydrated) {
365
365
  this._hydrationStage = HydrationStage.hydrating;
366
366
  }
@@ -404,7 +404,28 @@ export class HydrationView extends DefaultExecutionContext {
404
404
  if (typeof templateString !== "string") {
405
405
  templateString = templateString.innerHTML;
406
406
  }
407
- throw new HydrationBindingError(`HydrationView was unable to successfully target bindings inside "${(_c = ((_b = this.firstChild) === null || _b === void 0 ? void 0 : _b.getRootNode()).host) === null || _c === void 0 ? void 0 : _c.nodeName}".`, factory, createRangeForNodes(this.firstChild, this.lastChild).cloneContents(), templateString);
407
+ const hostElement = ((_b = this.firstChild) === null || _b === void 0 ? void 0 : _b.getRootNode())
408
+ .host;
409
+ const hostName = (hostElement === null || hostElement === void 0 ? void 0 : hostElement.nodeName) || "unknown";
410
+ const factoryInfo = factory;
411
+ // Build detailed error message
412
+ const details = [
413
+ `HydrationView was unable to successfully target bindings inside "<${hostName.toLowerCase()}>".`,
414
+ `\nMismatch Details:`,
415
+ ` - Expected target node ID: "${factory.targetNodeId}"`,
416
+ ` - Available target IDs: [${Object.keys(this.targets).join(", ") || "none"}]`,
417
+ ];
418
+ if (factory.targetTagName) {
419
+ details.push(` - Expected tag name: "${factory.targetTagName}"`);
420
+ }
421
+ if (factoryInfo.sourceAspect) {
422
+ details.push(` - Source aspect: "${factoryInfo.sourceAspect}"`);
423
+ }
424
+ if (factoryInfo.aspectType !== undefined) {
425
+ details.push(` - Aspect type: ${factoryInfo.aspectType}`);
426
+ }
427
+ details.push(`\nThis usually means:`, ` 1. The server-rendered HTML doesn't match the client template`, ` 2. The hydration markers are missing or corrupted`, ` 3. The DOM structure was modified before hydration`, `\nTemplate: ${templateString.slice(0, 200)}${templateString.length > 200 ? "..." : ""}`);
428
+ throw new HydrationBindingError(details.join("\n"), factory, createRangeForNodes(this.firstChild, this.lastChild).cloneContents(), templateString);
408
429
  }
409
430
  }
410
431
  }