Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "[feat]: implement lifecycle callbacks for hydration and template events",
"packageName": "@microsoft/fast-element",
"email": "[email protected]",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "prerelease",
"comment": "[feat]: implement lifecycle callbacks for hydration and template events",
"packageName": "@microsoft/fast-html",
"email": "[email protected]",
"dependentChangeType": "none"
}
23 changes: 22 additions & 1 deletion packages/web-components/fast-element/docs/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ export class FASTElementDefinition<TType extends Constructable<HTMLElement> = Co
static readonly getForInstance: (object: any) => FASTElementDefinition<Constructable<HTMLElement>> | undefined;
get isDefined(): boolean;
static isRegistered: Record<string, Function>;
readonly lifecycleCallbacks?: TemplateLifecycleCallbacks;
readonly name: string;
readonly propertyLookup: Record<string, AttributeDefinition>;
// @alpha
Expand Down Expand Up @@ -595,6 +596,7 @@ export class HTMLView<TSource = any, TParent = any> extends DefaultExecutionCont

// @beta
export class HydratableElementController<TElement extends HTMLElement = HTMLElement> extends ElementController<TElement> {
static config(callbacks: HydrationControllerCallbacks): typeof HydratableElementController;
// (undocumented)
connect(): void;
// (undocumented)
Expand Down Expand Up @@ -632,6 +634,13 @@ export class HydrationBindingError extends Error {
readonly templateString: string;
}

// @public
export interface HydrationControllerCallbacks {
elementDidHydrate?(name: string): void;
elementWillHydrate?(name: string): void;
hydrationComplete?(): void;
}

// @public
export class InlineTemplateDirective implements HTMLDirective {
constructor(html: string, factories?: Record<string, ViewBehaviorFactory>);
Expand Down Expand Up @@ -732,6 +741,7 @@ export const Parser: Readonly<{
export interface PartialFASTElementDefinition {
readonly attributes?: (AttributeConfiguration | string)[];
readonly elementOptions?: ElementDefinitionOptions;
readonly lifecycleCallbacks?: TemplateLifecycleCallbacks;
readonly name: string;
readonly registry?: CustomElementRegistry;
readonly shadowOptions?: Partial<ShadowRootOptions> | null;
Expand Down Expand Up @@ -970,8 +980,19 @@ export interface SyntheticViewTemplate<TSource = any, TParent = any> {
inline(): CaptureType<TSource, TParent>;
}

// @public
export interface TemplateLifecycleCallbacks {
elementDidDefine?(name: string): void;
templateDidUpdate?(name: string): void;
}

// @alpha
export const TemplateOptions: {
readonly deferAndHydrate: "defer-and-hydrate";
};

// @alpha
export type TemplateOptions = "defer-and-hydrate";
export type TemplateOptions = (typeof TemplateOptions)[keyof typeof TemplateOptions];

// @public
export type TemplateValue<TSource, TParent = any> = Expression<TSource, any, TParent> | Binding<TSource, any, TParent> | HTMLDirective | CaptureType<TSource, TParent>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import type { ViewController } from "../templating/html-directive.js";
import type { ElementViewTemplate } from "../templating/template.js";
import type { ElementView } from "../templating/view.js";
import { UnobservableMutationObserver } from "../utilities.js";
import { FASTElementDefinition, ShadowRootOptions } from "./fast-definitions.js";
import {
FASTElementDefinition,
ShadowRootOptions,
TemplateOptions,
} from "./fast-definitions.js";
import type { FASTElement } from "./fast-element.js";
import { HydrationMarkup, isHydratable } from "./hydration.js";

Expand Down Expand Up @@ -749,6 +753,27 @@ if (ElementStyles.supportsAdoptedStyleSheets) {
export const deferHydrationAttribute = "defer-hydration";
export const needsHydrationAttribute = "needs-hydration";

/**
* Lifecycle callbacks for element hydration events
* @public
*/
export interface HydrationControllerCallbacks {
/**
* Called before hydration has started
*/
elementWillHydrate?(name: string): void;

/**
* Called after hydration has finished
*/
elementDidHydrate?(name: string): void;

/**
* Called after all elements have completed hydration
*/
hydrationComplete?(): void;
}

/**
* An ElementController capable of hydrating FAST elements from
* Declarative Shadow DOM.
Expand All @@ -768,26 +793,47 @@ export class HydratableElementController<
HydratableElementController.hydrationObserverHandler
);

/**
* Lifecycle callbacks for hydration events
*/
private static lifecycleCallbacks?: HydrationControllerCallbacks;

/**
* Configure lifecycle callbacks for hydration events
*/
public static config(callbacks: HydrationControllerCallbacks) {
HydratableElementController.lifecycleCallbacks = callbacks;
return this;
}

private static hydrationObserverHandler(records: MutationRecord[]) {
for (const record of records) {
HydratableElementController.hydrationObserver.unobserve(record.target);
(record.target as any).$fastController.connect();
}
}

/**
* Checks if all elements have completed hydration and dispatches event if complete
*/
private static checkHydrationComplete(): void {
if (!document.querySelector(`[${needsHydrationAttribute}]`)) {
HydratableElementController.lifecycleCallbacks?.hydrationComplete?.();
}
}

public static forCustomElement(
element: HTMLElement,
override?: boolean
): ElementController<HTMLElement> {
const definition = FASTElementDefinition.getForInstance(element);

if (
definition !== undefined &&
definition.templateOptions === "defer-and-hydrate" &&
definition?.templateOptions === TemplateOptions.deferAndHydrate &&
!definition.template
) {
element.setAttribute(deferHydrationAttribute, "");
element.setAttribute(needsHydrationAttribute, "");
element.toggleAttribute(deferHydrationAttribute, true);
element.toggleAttribute(needsHydrationAttribute, true);
}

return super.forCustomElement(element, override);
Expand Down Expand Up @@ -823,6 +869,11 @@ export class HydratableElementController<
return;
}

// Callback: Before hydration has started
HydratableElementController.lifecycleCallbacks?.elementWillHydrate?.(
this.definition.name
);

this.stage = Stages.connecting;

this.bindObservables();
Expand Down Expand Up @@ -866,6 +917,14 @@ export class HydratableElementController<
this.source.removeAttribute(needsHydrationAttribute);
this.needsInitialization = this.needsHydration = false;
Observable.notify(this, isConnectedPropertyName);

// Callback: After hydration has finished
HydratableElementController.lifecycleCallbacks?.elementDidHydrate?.(
this.definition.name
);

// Check if hydration is complete after this element is hydrated
HydratableElementController.checkHydrationComplete();
}

public disconnect() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,34 @@ export interface ShadowRootOptions extends ShadowRootInit {
}

/**
* Template options.
* Values for the `templateOptions` property.
* @alpha
*/
export type TemplateOptions = "defer-and-hydrate";
export const TemplateOptions = {
deferAndHydrate: "defer-and-hydrate",
} as const;

/**
* Type for the `TemplateOptions` const enum.
* @alpha
*/
export type TemplateOptions = (typeof TemplateOptions)[keyof typeof TemplateOptions];

/**
* Lifecycle callbacks for template events.
* @public
*/
export interface TemplateLifecycleCallbacks {
/**
* Called after the template has been assigned to the definition
*/
templateDidUpdate?(name: string): void;

/**
* Called after the custom element has been defined
*/
elementDidDefine?(name: string): void;
}

/**
* Represents metadata configuration for a custom element.
Expand Down Expand Up @@ -89,6 +113,11 @@ export interface PartialFASTElementDefinition {
* If not provided, defaults to the global registry.
*/
readonly registry?: CustomElementRegistry;

/**
* Lifecycle callbacks for template events.
*/
readonly lifecycleCallbacks?: TemplateLifecycleCallbacks;
}

/**
Expand Down Expand Up @@ -163,6 +192,11 @@ export class FASTElementDefinition<
*/
readonly registry: CustomElementRegistry;

/**
* Lifecycle callbacks for template events.
*/
public readonly lifecycleCallbacks?: TemplateLifecycleCallbacks;

/**
* The definition has been registered to the FAST element registry.
*/
Expand Down Expand Up @@ -236,6 +270,7 @@ export class FASTElementDefinition<
if (!registry.get(this.name)) {
this.platformDefined = true;
registry.define(this.name, type as any, this.elementOptions);
this.lifecycleCallbacks?.elementDidDefine?.(this.name);
}

return this;
Expand Down Expand Up @@ -293,9 +328,7 @@ export class FASTElementDefinition<
}

Observable.getNotifier(FASTElementDefinition.isRegistered).subscribe(
{
handleChange: () => resolve(FASTElementDefinition.isRegistered[name]),
},
{ handleChange: () => resolve(FASTElementDefinition.isRegistered[name]) },
name
);
});
Expand Down Expand Up @@ -325,18 +358,17 @@ export class FASTElementDefinition<

const definition = new FASTElementDefinition<TType>(type, nameOrDef);

Promise.all([
new Promise<void>(resolve => {
Observable.getNotifier(definition).subscribe(
{
handleChange: () => resolve(),
},
"template"
);
}),
]).then(() => {
resolve(definition);
});
Observable.getNotifier(definition).subscribe(
{
handleChange: () => {
definition.lifecycleCallbacks?.templateDidUpdate?.(
definition.name
);
resolve(definition);
},
},
"template"
);
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,41 +124,29 @@ function compose<TType extends Constructable<HTMLElement> = Constructable<HTMLEl
return FASTElementDefinition.compose(this, type);
}

function defineAsync<
async function defineAsync<
TType extends Constructable<HTMLElement> = Constructable<HTMLElement>
>(
this: TType,
nameOrDef: string | PartialFASTElementDefinition
): Promise<FASTElementDefinition<TType>>;
function defineAsync<
async function defineAsync<
TType extends Constructable<HTMLElement> = Constructable<HTMLElement>
>(
type: TType,
nameOrDef?: string | PartialFASTElementDefinition
): Promise<FASTElementDefinition<TType>>;
function defineAsync<
async function defineAsync<
TType extends Constructable<HTMLElement> = Constructable<HTMLElement>
>(
type: TType | string | PartialFASTElementDefinition,
nameOrDef?: string | PartialFASTElementDefinition
): Promise<TType> {
if (isFunction(type)) {
return new Promise<FASTElementDefinition<TType>>(resolve => {
FASTElementDefinition.composeAsync(type, nameOrDef).then(value => {
resolve(value);
});
}).then(value => {
return value.define().type;
});
return (await FASTElementDefinition.composeAsync(type, nameOrDef)).define().type;
}

return new Promise<FASTElementDefinition<TType>>(resolve => {
FASTElementDefinition.composeAsync(this, type).then(value => {
resolve(value);
});
}).then(value => {
return value.define().type;
});
return (await FASTElementDefinition.composeAsync(this, type)).define().type;
}

function define<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>>(
Expand Down
2 changes: 2 additions & 0 deletions packages/web-components/fast-element/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ export {
fastElementRegistry,
TemplateOptions,
TypeRegistry,
type TemplateLifecycleCallbacks,
} from "./components/fast-definitions.js";
export {
attr,
Expand All @@ -162,4 +163,5 @@ export {
ElementController,
ElementControllerStrategy,
HydratableElementController,
type HydrationControllerCallbacks,
} from "./components/element-controller.js";
Loading