-
Notifications
You must be signed in to change notification settings - Fork 599
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add defer hydration support to fast-element (#6441)
* adding mutation observer and starting to refactor fast-element * break observable re-binding into it's own method * re-apply observable values in constructor so that they don't need to be manually cached and re-applied during connection * revert unnecessary binding observer changes * refactor and reogranize draft implementation, adding tests * added features to exports and updated api report * Change files * align StyleStrategy and ElementControllerStrategy implementations * fix api report * revert observable property re-assignment * re-organize hydration support into export paths * leverage strategy API internally * make mutation observer internal for now * revert removal of context export path * fix unit test failure * perform default strategy assignment as file side effect Co-authored-by: nicholasrice <[email protected]>
- Loading branch information
Showing
9 changed files
with
295 additions
and
11 deletions.
There are no files selected for viewing
7 changes: 7 additions & 0 deletions
7
change/@microsoft-fast-element-720b7480-23f3-4944-9238-cde64e7ca3c4.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"type": "prerelease", | ||
"comment": "Added support to fast-element to support element hydration and the `defer-hydration` attribute", | ||
"packageName": "@microsoft/fast-element", | ||
"email": "[email protected]", | ||
"dependentChangeType": "prerelease" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
155 changes: 155 additions & 0 deletions
155
packages/web-components/fast-element/src/components/hydration.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import chai, { expect } from "chai"; | ||
import { css, HostBehavior, Updates } from "../index.js"; | ||
import { html } from "../templating/template.js"; | ||
import { uniqueElementName } from "../testing/exports.js"; | ||
import { ElementController } from "./element-controller.js"; | ||
import { FASTElementDefinition, PartialFASTElementDefinition } from "./fast-definitions.js"; | ||
import { FASTElement } from "./fast-element.js"; | ||
import { HydratableElementController } from "./hydration.js"; | ||
import spies from "chai-spies"; | ||
|
||
chai.use(spies) | ||
|
||
describe("The HydratableElementController", () => { | ||
beforeEach(() => { | ||
HydratableElementController.install(); | ||
}) | ||
afterEach(() => { | ||
ElementController.setStrategy(ElementController); | ||
}) | ||
function createController( | ||
config: Omit<PartialFASTElementDefinition, "name"> = {}, | ||
BaseClass = FASTElement | ||
) { | ||
const name = uniqueElementName(); | ||
const definition = FASTElementDefinition.compose( | ||
class ControllerTest extends BaseClass { | ||
static definition = { ...config, name }; | ||
} | ||
).define(); | ||
|
||
const element = document.createElement(name) as FASTElement; | ||
const controller = ElementController.forCustomElement(element); | ||
|
||
return { | ||
name, | ||
element, | ||
controller, | ||
definition, | ||
shadowRoot: element.shadowRoot! as ShadowRoot & { | ||
adoptedStyleSheets: CSSStyleSheet[]; | ||
}, | ||
}; | ||
} | ||
|
||
it("A FASTElement's controller should be an instance of HydratableElementController after invoking 'addHydrationSupport'", () => { | ||
const { element } = createController() | ||
|
||
expect(element.$fastController).to.be.instanceOf(HydratableElementController); | ||
}); | ||
|
||
describe("without the `defer-hydration` attribute on connection", () => { | ||
it("should render the element's template", async () => { | ||
const { element } = createController({template: html`<p>Hello world</p>`}) | ||
|
||
document.body.appendChild(element); | ||
await Updates.next(); | ||
expect(element.shadowRoot?.innerHTML).to.be.equal("<p>Hello world</p>"); | ||
document.body.removeChild(element) | ||
}); | ||
it("should apply the element's main stylesheet", async () => { | ||
const { element } = createController({styles: css`:host{ color :red}`}) | ||
|
||
document.body.appendChild(element); | ||
expect(element.$fastController.mainStyles?.isAttachedTo(element)).to.be.true; | ||
document.body.removeChild(element) | ||
}); | ||
it("should invoke a HostBehavior's connectedCallback", async () => { | ||
const behavior: HostBehavior = { | ||
connectedCallback: chai.spy(() => {}) | ||
} | ||
|
||
const { element, controller } = createController() | ||
controller.addBehavior(behavior); | ||
|
||
document.body.appendChild(element); | ||
expect(behavior.connectedCallback).to.have.been.called() | ||
document.body.removeChild(element) | ||
}); | ||
}) | ||
|
||
describe("with the `defer-hydration` is set before connection", () => { | ||
it("should not render the element's template", async () => { | ||
const { element } = createController({template: html`<p>Hello world</p>`}) | ||
|
||
element.setAttribute('defer-hydration', ''); | ||
document.body.appendChild(element); | ||
await Updates.next(); | ||
expect(element.shadowRoot?.innerHTML).be.equal(""); | ||
document.body.removeChild(element) | ||
}); | ||
it("should not attach the element's main stylesheet", async () => { | ||
const { element } = createController({styles: css`:host{ color :red}`}) | ||
|
||
element.setAttribute('defer-hydration', ''); | ||
document.body.appendChild(element); | ||
expect(element.$fastController.mainStyles?.isAttachedTo(element)).to.be.false; | ||
document.body.removeChild(element) | ||
}); | ||
it("should not invoke a HostBehavior's connectedCallback", async () => { | ||
const behavior: HostBehavior = { | ||
connectedCallback: chai.spy(() => {}) | ||
} | ||
|
||
const { element, controller } = createController() | ||
element.setAttribute('defer-hydration', '') | ||
controller.addBehavior(behavior); | ||
|
||
document.body.appendChild(element); | ||
expect(behavior.connectedCallback).not.to.have.been.called() | ||
document.body.removeChild(element) | ||
}); | ||
}) | ||
|
||
describe("when the `defer-hydration` attribute removed after connection", () => { | ||
it("should render the element's template", async () => { | ||
const { element } = createController({template: html`<p>Hello world</p>`}) | ||
|
||
element.setAttribute('defer-hydration', ''); | ||
document.body.appendChild(element); | ||
await Updates.next(); | ||
expect(element.shadowRoot?.innerHTML).be.equal(""); | ||
element.removeAttribute('defer-hydration') | ||
await Updates.next(); | ||
expect(element.shadowRoot?.innerHTML).to.be.equal("<p>Hello world</p>"); | ||
document.body.removeChild(element) | ||
}); | ||
it("should attach the element's main stylesheet", async () => { | ||
const { element } = createController({styles: css`:host{ color :red}`}) | ||
|
||
element.setAttribute('defer-hydration', ''); | ||
document.body.appendChild(element); | ||
expect(element.$fastController.mainStyles?.isAttachedTo(element)).to.be.false; | ||
element.removeAttribute('defer-hydration'); | ||
await Updates.next(); | ||
expect(element.$fastController.mainStyles?.isAttachedTo(element)).to.be.true; | ||
document.body.removeChild(element); | ||
}); | ||
it("should invoke a HostBehavior's connectedCallback", async () => { | ||
const behavior: HostBehavior = { | ||
connectedCallback: chai.spy(() => {}) | ||
} | ||
|
||
const { element, controller } = createController() | ||
element.setAttribute('defer-hydration', '') | ||
controller.addBehavior(behavior); | ||
|
||
document.body.appendChild(element); | ||
expect(behavior.connectedCallback).not.to.have.been.called(); | ||
element.removeAttribute('defer-hydration'); | ||
await Updates.next(); | ||
expect(behavior.connectedCallback).to.have.been.called(); | ||
document.body.removeChild(element) | ||
}); | ||
}) | ||
}); |
44 changes: 44 additions & 0 deletions
44
packages/web-components/fast-element/src/components/hydration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { UnobservableMutationObserver } from "../utilities.js"; | ||
import { ElementController } from "./element-controller.js"; | ||
|
||
const deferHydrationAttribute = "defer-hydration"; | ||
|
||
/** | ||
* An ElementController capable of hydrating FAST elements from | ||
* Declarative Shadow DOM. | ||
* | ||
* @beta | ||
*/ | ||
export class HydratableElementController< | ||
TElement extends HTMLElement = HTMLElement | ||
> extends ElementController<TElement> { | ||
private static hydrationObserver = new UnobservableMutationObserver( | ||
HydratableElementController.hydrationObserverHandler | ||
); | ||
|
||
private static hydrationObserverHandler(records: MutationRecord[]) { | ||
for (const record of records) { | ||
HydratableElementController.hydrationObserver.unobserve(record.target); | ||
(record.target as any).$fastController.connect(); | ||
} | ||
} | ||
|
||
public connect() { | ||
if (this.source.hasAttribute(deferHydrationAttribute)) { | ||
HydratableElementController.hydrationObserver.observe(this.source, { | ||
attributeFilter: [deferHydrationAttribute], | ||
}); | ||
} else { | ||
super.connect(); | ||
} | ||
} | ||
|
||
public disconnect() { | ||
super.disconnect(); | ||
HydratableElementController.hydrationObserver.unobserve(this.source); | ||
} | ||
|
||
public static install() { | ||
ElementController.setStrategy(HydratableElementController); | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/web-components/fast-element/src/components/install-hydration.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { HydratableElementController } from "./hydration.js"; | ||
|
||
HydratableElementController.install(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters