Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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": "patch",
"comment": "fix: use shadowOptions getter override to set hydration attributes",
"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": "fix: delegate attribute handling to HydratableElementController",
"packageName": "@microsoft/fast-html",
"email": "[email protected]",
"dependentChangeType": "none"
}
31 changes: 16 additions & 15 deletions packages/web-components/fast-element/docs/api-report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ export function customElement(nameOrDef: string | PartialFASTElementDefinition):
// @public
export type DecoratorAttributeConfiguration = Omit<AttributeConfiguration, "property">;

// @public
export const deferHydrationAttribute = "defer-hydration";

// @public
export interface Disposable {
dispose(): void;
Expand Down Expand Up @@ -282,43 +285,34 @@ export class ElementController<TElement extends HTMLElement = HTMLElement> exten
constructor(element: TElement, definition: FASTElementDefinition);
addBehavior(behavior: HostBehavior<TElement>): void;
addStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
// (undocumented)
protected behaviors: Map<HostBehavior<TElement>, number> | null;
// (undocumented)
protected bindObservables(): void;
connect(): void;
// (undocumented)
protected connectBehaviors(): void;
get context(): ExecutionContext;
readonly definition: FASTElementDefinition;
disconnect(): void;
// (undocumented)
protected disconnectBehaviors(): void;
emit(type: string, detail?: any, options?: Omit<CustomEventInit, "detail">): void | boolean;
static forCustomElement(element: HTMLElement, override?: boolean): ElementController;
protected hasExistingShadowRoot: boolean;
get isBound(): boolean;
get isConnected(): boolean;
get mainStyles(): ElementStyles | null;
set mainStyles(value: ElementStyles | null);
// (undocumented)
protected needsInitialization: boolean;
onAttributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
onUnbind(behavior: {
unbind(controller: ExpressionController<TElement>): any;
}): void;
removeBehavior(behavior: HostBehavior<TElement>, force?: boolean): void;
removeStyles(styles: ElementStyles | HTMLStyleElement | null | undefined): void;
// (undocumented)
protected renderTemplate(template: ElementViewTemplate | null | undefined): void;
static setStrategy(strategy: ElementControllerStrategy): void;
// (undocumented)
get shadowOptions(): ShadowRootOptions | undefined;
set shadowOptions(value: ShadowRootOptions | undefined);
readonly source: TElement;
get sourceLifetime(): SourceLifetime | undefined;
// Warning: (ae-forgotten-export) The symbol "Stages" needs to be exported by the entry point index.d.ts
//
// (undocumented)
protected stage: Stages;
get template(): ElementViewTemplate<TElement> | null;
set template(value: ElementViewTemplate<TElement> | null);
Expand Down Expand Up @@ -597,16 +591,12 @@ 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)
disconnect(): void;
// (undocumented)
static forCustomElement(element: HTMLElement, override?: boolean): ElementController<HTMLElement>;
// (undocumented)
static install(): void;
static lifecycleCallbacks?: HydrationControllerCallbacks;
protected needsHydration?: boolean;
set shadowOptions(value: ShadowRootOptions | undefined);
}

// @public (undocumented)
Expand Down Expand Up @@ -667,6 +657,9 @@ export const Markup: Readonly<{
comment: (id: string) => string;
}>;

// @public
export const needsHydrationAttribute = "needs-hydration";

// @public
export interface NodeBehaviorOptions<T = any> {
filter?: ElementsFilter;
Expand Down Expand Up @@ -922,6 +915,14 @@ export const SpliceStrategySupport: Readonly<{
// @public
export type SpliceStrategySupport = (typeof SpliceStrategySupport)[keyof typeof SpliceStrategySupport];

// @public
export const enum Stages {
connected = 1,
connecting = 0,
disconnected = 3,
disconnecting = 2
}

// @public
export abstract class StatelessAttachedAttributeDirective<TOptions> implements HTMLDirective, ViewBehaviorFactory, ViewBehavior {
constructor(options: TOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,18 @@ export interface ElementControllerStrategy {
new (element: HTMLElement, definition: FASTElementDefinition): ElementController;
}

const enum Stages {
/**
* The various lifecycle stages of an ElementController.
* @public
*/
export const enum Stages {
/** The element is in the process of connecting. */
connecting,
/** The element is connected. */
connected,
/** The element is in the process of disconnecting. */
disconnecting,
/** The element is disconnected. */
disconnected,
}

Expand All @@ -61,24 +69,58 @@ export class ElementController<TElement extends HTMLElement = HTMLElement>
extends PropertyChangeNotifier
implements HostController<TElement>
{
/**
* A map of observable properties that were set on the element before upgrade.
*/
private boundObservables: Record<string, any> | null = null;

/**
* Indicates whether the controller needs to perform initial rendering.
*/
protected needsInitialization: boolean = true;
private hasExistingShadowRoot = false;

/**
* Indicates whether the element has an existing shadow root (e.g. from declarative shadow DOM).
*/
protected hasExistingShadowRoot = false;

/**
* The template used to render the component.
*/
private _template: ElementViewTemplate<TElement> | null = null;

/**
* The shadow root options for the component.
*/
private _shadowRootOptions: ShadowRootOptions | undefined;

/**
* The current lifecycle stage of the controller.
*/
protected stage: Stages = Stages.disconnected;

/**
* A guard against connecting behaviors multiple times
* during connect in scenarios where a behavior adds
* another behavior during it's connectedCallback
*/
private guardBehaviorConnection = false;

/**
* The behaviors associated with the component.
*/
protected behaviors: Map<HostBehavior<TElement>, number> | null = null;

/**
* Tracks whether behaviors are connected so that
* behaviors cant be connected multiple times
*/
private behaviorsConnected: boolean = false;

/**
* The main set of styles used for the component, independent of any
* dynamically added styles.
*/
private _mainStyles: ElementStyles | null = null;

/**
Expand Down Expand Up @@ -173,10 +215,19 @@ export class ElementController<TElement extends HTMLElement = HTMLElement>
}
}

/**
* The shadow root options for the component.
*/
public get shadowOptions(): ShadowRootOptions | undefined {
return this._shadowRootOptions;
}

/**
* Sets the shadow root options for the component and attaches
* the shadow root if it does not already exist.
*
* @param value - The shadow root options to set.
*/
public set shadowOptions(value: ShadowRootOptions | undefined) {
// options on the shadowRoot can only be set once
if (this._shadowRootOptions === void 0 && value !== void 0) {
Expand Down Expand Up @@ -409,6 +460,9 @@ export class ElementController<TElement extends HTMLElement = HTMLElement>
Observable.notify(this, isConnectedPropertyName);
}

/**
* Binds any observables that were set before upgrade.
*/
protected bindObservables() {
if (this.boundObservables !== null) {
const element = this.source;
Expand All @@ -424,6 +478,9 @@ export class ElementController<TElement extends HTMLElement = HTMLElement>
}
}

/**
* Connects any existing behaviors on the associated element.
*/
protected connectBehaviors() {
if (this.behaviorsConnected === false) {
const behaviors = this.behaviors;
Expand All @@ -440,6 +497,9 @@ export class ElementController<TElement extends HTMLElement = HTMLElement>
}
}

/**
* Disconnects any behaviors on the associated element.
*/
protected disconnectBehaviors() {
if (this.behaviorsConnected === true) {
const behaviors = this.behaviors;
Expand Down Expand Up @@ -514,6 +574,13 @@ export class ElementController<TElement extends HTMLElement = HTMLElement>
return false;
}

/**
* Renders the provided template to the element.
*
* @param template - The template to render.
* @remarks
* If `null` is provided, any existing view will be removed.
*/
protected renderTemplate(template: ElementViewTemplate | null | undefined): void {
// When getting the host to render to, we start by looking
// up the shadow root. If there isn't one, then that means
Expand Down Expand Up @@ -750,7 +817,16 @@ if (ElementStyles.supportsAdoptedStyleSheets) {
ElementStyles.setDefaultStrategy(StyleElementStrategy);
}

/**
* The attribute used to defer hydration of an element.
* @public
*/
export const deferHydrationAttribute = "defer-hydration";

/**
* The attribute used to indicate that an element needs hydration.
* @public
*/
export const needsHydrationAttribute = "needs-hydration";

/**
Expand Down Expand Up @@ -793,6 +869,21 @@ export class HydratableElementController<
HydratableElementController.hydrationObserverHandler
);

/**
* Sets the defer-hydration and needs-hydration attributes when the TemplateOptions
* are set to deferAndHydrate and an existing shadow root is present.
*/
public set shadowOptions(value: ShadowRootOptions | undefined) {
super.shadowOptions = value;
if (
this.hasExistingShadowRoot &&
this.definition.templateOptions === TemplateOptions.deferAndHydrate
) {
this.source.toggleAttribute(deferHydrationAttribute, true);
this.source.toggleAttribute(needsHydrationAttribute, true);
}
}

/**
* Lifecycle callbacks for hydration events
*/
Expand Down Expand Up @@ -864,28 +955,13 @@ export class HydratableElementController<
}
}

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

if (
definition?.templateOptions === TemplateOptions.deferAndHydrate &&
!definition.template
) {
element.toggleAttribute(deferHydrationAttribute, true);
element.toggleAttribute(needsHydrationAttribute, true);
}

return super.forCustomElement(element, override);
}

/**
* Runs connected lifecycle behavior on the associated element.
*/
public connect() {
// Initialize needsHydration on first connect
this.needsHydration =
this.needsHydration ??
this.source.getAttribute(needsHydrationAttribute) !== null;
this.needsHydration ?? this.source.hasAttribute(needsHydrationAttribute);

if (this.needsHydration) {
HydratableElementController.lifecycleCallbacks?.elementWillHydrate?.(
Expand Down Expand Up @@ -1009,11 +1085,20 @@ export class HydratableElementController<
}
}

/**
* Unregisters the hydration observer when the element is disconnected.
*/
public disconnect() {
super.disconnect();
HydratableElementController.hydrationObserver.unobserve(this.source);
}

/**
* Sets the ElementController strategy to HydratableElementController.
* @remarks
* This method is typically called during application startup to enable
* hydration support for FAST elements.
*/
public static install() {
ElementController.setStrategy(HydratableElementController);
}
Expand Down
3 changes: 3 additions & 0 deletions packages/web-components/fast-element/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,11 @@ export {
ValueConverter,
} from "./components/attributes.js";
export {
deferHydrationAttribute,
ElementController,
ElementControllerStrategy,
HydratableElementController,
type HydrationControllerCallbacks,
needsHydrationAttribute,
Stages,
} from "./components/element-controller.js";
10 changes: 0 additions & 10 deletions packages/web-components/fast-html/src/components/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,10 @@ export function RenderableFASTElement<T extends Constructable<FASTElement>>(
): T {
const C = class extends BaseCtor {
deferHydration: boolean = true;
needsHydration: boolean = true;

constructor(...args: any[]) {
super(...args);

if ((this.$fastController as any).hasExistingShadowRoot) {
this.toggleAttribute("defer-hydration", true);
}

(this.prepare?.() ?? Promise.resolve()).then(() => {
this.deferHydration = false;
});
Expand All @@ -39,10 +34,5 @@ export function RenderableFASTElement<T extends Constructable<FASTElement>>(
"deferHydration"
);

attr({ mode: "boolean", attribute: "defer-hydration" })(
C.prototype,
"needsHydration"
);

return C;
}
Loading
Loading