diff --git a/packages/web-components/fast-ssr/server/server.ts b/packages/web-components/fast-ssr/server/server.ts index e47e01fad16..866fa10b5e8 100644 --- a/packages/web-components/fast-ssr/server/server.ts +++ b/packages/web-components/fast-ssr/server/server.ts @@ -1,6 +1,9 @@ import { Readable } from "stream"; import express, { Request, Response } from "express"; +import fs from "fs"; +import path from "path"; +const __dirname = path.resolve(path.dirname("")); const PORT = 8080; function handleRequest(req: Request, res: Response) { res.set("Content-Type", "text/html"); @@ -20,6 +23,50 @@ function handleRequest(req: Request, res: Response) { }); } +function handleStyleRequest(req: Request, res: Response) { + res.set("Content-Type", "text/html"); + fs.readFile( + path.resolve(__dirname, "./src/fast-style/index.fixture.html"), + { encoding: "utf8" }, + (err, data) => { + const stream = (Readable as any).from(data); + stream.on("readable", function (this: any) { + while ((data = this.read())) { + res.write(data); + } + }); + stream.on("close", () => res.end()); + stream.on("error", (e: Error) => { + console.error(e); + process.exit(1); + }); + } + ); +} + +function handleStyleScriptRequest(req: Request, res: Response) { + res.set("Content-Type", "application/javascript"); + fs.readFile( + path.resolve(__dirname, "./dist/fast-style/index.js"), + { encoding: "utf8" }, + (err, data) => { + const stream = (Readable as any).from(data); + stream.on("readable", function (this: any) { + while ((data = this.read())) { + res.write(data); + } + }); + stream.on("close", () => res.end()); + stream.on("error", (e: Error) => { + console.error(e); + process.exit(1); + }); + } + ); +} + const app = express(); app.get("/", handleRequest); +app.get("/fast-style", handleStyleRequest); +app.get("/fast-style.js", handleStyleScriptRequest); app.listen(PORT); diff --git a/packages/web-components/fast-ssr/src/fast-style/index.fixture.html b/packages/web-components/fast-ssr/src/fast-style/index.fixture.html new file mode 100644 index 00000000000..92820fa5ab9 --- /dev/null +++ b/packages/web-components/fast-ssr/src/fast-style/index.fixture.html @@ -0,0 +1,436 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/web-components/fast-ssr/src/fast-style/index.ts b/packages/web-components/fast-ssr/src/fast-style/index.ts new file mode 100644 index 00000000000..cb480301651 --- /dev/null +++ b/packages/web-components/fast-ssr/src/fast-style/index.ts @@ -0,0 +1,79 @@ +interface StyleCache { + [key: string]: CSSStyleSheet | string; +} + +/** + * The FASTStyle web component that takes attributes: + * - css - a string version of the CSS to be applied to the parent web component, this can be omitted if the data-style-id has been used in the DOM as the cached value will be fetched + * - data-style-id - a dataset attribute used as an identifier for the CSS attr string + */ +export default class FASTStyle extends HTMLElement { + private static cache: StyleCache = {}; + private static hashIdDataSetName: string = "data-style-id"; + private static supportsAdoptedStyleSheets: boolean = + Array.isArray((document as any).adoptedStyleSheets) && + "replace" in CSSStyleSheet.prototype; + + /** + * @internal + */ + public connectedCallback(): void { + const hashId: string = this.getAttribute(FASTStyle.hashIdDataSetName) as string; + const css: string = this.getAttribute("css") as string; + this.registerStyles(hashId, css); + } + + /** + * Register styles if they are not part of the cache and attach them + */ + private registerStyles = (hashId: string, css: string): void => { + if (FASTStyle.supportsAdoptedStyleSheets) { + if (!(hashId in FASTStyle.cache)) { + this.memoizeAdoptedStylesheetStyles(hashId, css); + } + this.attachAdoptedStylesheetStyles(this.parentNode as ShadowRoot, hashId); + } else { + if (!(hashId in FASTStyle.cache)) { + this.memoizeStyleElementStyles(hashId, css); + } + this.attachStyleElementStyles(this.parentNode as ShadowRoot, hashId); + } + }; + + /** + * Memoize CSSStyleSheets + */ + private memoizeAdoptedStylesheetStyles(hashId: string, css: string) { + const sheet = new CSSStyleSheet(); + (sheet as any).replaceSync(css); + FASTStyle.cache[hashId] = sheet; + } + + /** + * Attach CSSStyleSheets + */ + private attachAdoptedStylesheetStyles(shadowRoot: ShadowRoot, hashId: string) { + (shadowRoot as any).adoptedStyleSheets = [ + ...(shadowRoot as any).adoptedStyleSheets!, + FASTStyle.cache[hashId] as CSSStyleSheet, + ]; + } + + /** + * Memoize css strings + */ + private memoizeStyleElementStyles(hashId: string, css: string) { + FASTStyle.cache[hashId] = css; + } + + /** + * Attach style elements + */ + private attachStyleElementStyles(shadowRoot: ShadowRoot, hashId: string) { + const element = document.createElement("style"); + element.innerHTML = FASTStyle.cache[hashId] as string; + shadowRoot.append(element); + } +} + +customElements.define("fast-style", FASTStyle); diff --git a/packages/web-components/fast-ssr/test/fast-style.spec.ts b/packages/web-components/fast-ssr/test/fast-style.spec.ts new file mode 100644 index 00000000000..08f74b18e39 --- /dev/null +++ b/packages/web-components/fast-ssr/test/fast-style.spec.ts @@ -0,0 +1,62 @@ +import { expect, test } from "@playwright/test" + +test("Check that the first element has styles assigned", async ({ page }) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle(card, null).getPropertyValue("background-color"); + }); + }); + + expect(styles[0]).toEqual("rgb(26, 26, 26)"); +}); +test("Check that the nested element in the first element has styles assigned", async ({ page }) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle( + (card.shadowRoot?.querySelector("fast-button") as Element), null + ).getPropertyValue("background-color"); + }); + }); + + expect(styles[0]).toEqual("rgb(43, 43, 43)"); +}); +test("Check that all elements have styles assigned", async ({ page }) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle(card, null).getPropertyValue("background-color"); + }); + }); + + expect(styles).toHaveLength(10); + + styles.forEach((style) => { + expect(style).toEqual("rgb(26, 26, 26)"); + }); +}); +test("Check that all nested elements have styles assigned", async ({ page}) => { + await page.goto("/fast-style"); + + const cards = page.locator("fast-card"); + const styles = await cards.evaluateAll((cardList) => { + return cardList.map((card) => { + return window.getComputedStyle( + (card.shadowRoot?.querySelector("fast-button") as Element), null + ).getPropertyValue("background-color"); + }); + }); + + expect(styles).toHaveLength(10); + + styles.forEach((style) => { + expect(style).toEqual("rgb(43, 43, 43)"); + }); +});