diff --git a/change/@microsoft-fast-element-bfa58c92-2d69-4eea-b132-64dbe65d1663.json b/change/@microsoft-fast-element-bfa58c92-2d69-4eea-b132-64dbe65d1663.json new file mode 100644 index 00000000000..e068df1b4c9 --- /dev/null +++ b/change/@microsoft-fast-element-bfa58c92-2d69-4eea-b132-64dbe65d1663.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: modularize hydration and expose lifecycle callbacks via enableHydration() and declarativeTemplate()", + "packageName": "@microsoft/fast-element", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/change/@microsoft-fast-router-de58ce36-92f3-4c7f-86a2-43a66a522f81.json b/change/@microsoft-fast-router-de58ce36-92f3-4c7f-86a2-43a66a522f81.json new file mode 100644 index 00000000000..3af1bd0477b --- /dev/null +++ b/change/@microsoft-fast-router-de58ce36-92f3-4c7f-86a2-43a66a522f81.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "Update imports for styles.js subpath export", + "packageName": "@microsoft/fast-router", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/examples/todo-app/src/todo-app.styles.ts b/examples/todo-app/src/todo-app.styles.ts index 9902de9f1e1..4d6c0366e4b 100644 --- a/examples/todo-app/src/todo-app.styles.ts +++ b/examples/todo-app/src/todo-app.styles.ts @@ -1,4 +1,4 @@ -import { css } from "@microsoft/fast-element"; +import { css } from "@microsoft/fast-element/styles.js"; export const styles = css` :host { diff --git a/examples/todo-app/src/todo-form.styles.ts b/examples/todo-app/src/todo-form.styles.ts index 94d2d95b360..56c210c60c7 100644 --- a/examples/todo-app/src/todo-form.styles.ts +++ b/examples/todo-app/src/todo-form.styles.ts @@ -1,4 +1,4 @@ -import { css } from "@microsoft/fast-element"; +import { css } from "@microsoft/fast-element/styles.js"; export const styles = css` form { diff --git a/packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md b/packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md index 20877036fd1..58c41ea6e37 100644 --- a/packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md +++ b/packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md @@ -96,10 +96,11 @@ Once the template is attached to the partial definition, the element completes i When custom elements are instantiated in the DOM, the following occurs: 1. **Element Creation**: The platform creates instances of the custom element -2. **Prerendered Content Detection**: `ElementController` detects the existing shadow root from SSR and sets `isPrerendered = true` -3. **Concrete Template Ready**: Because `declarativeTemplate()` resolved during +2. **Prerendered Content Detection**: `ElementController` detects the existing shadow root from SSR — `isPrerendered` resolves `true` +3. **Hydration Check**: If `enableHydration()` was called and the template is hydratable, the element hydrates — `isHydrated` resolves `true`. Otherwise it falls back to client-side rendering. +4. **Concrete Template Ready**: Because `declarativeTemplate()` resolved during definition, `connect()` starts with the final template already attached. -4. **Hydration**: `ElementController` uses `template.hydrate()` to create a +5. **Hydration**: `ElementController` uses `template.hydrate()` to create a `HydrationView` that maps existing DOM nodes to binding targets using `fe:b` / `fe:/b` markers @@ -154,23 +155,21 @@ FAST HTML provides a set of lifecycle callbacks that allow you to hook into vari ### Available Callbacks -The lifecycle callbacks are organized into three categories: +The lifecycle callbacks are split between two APIs: -**Template Registration Callbacks:** +**Per-element callbacks** — passed to `declarativeTemplate()`: - `elementDidRegister(name: string)` - Called after the JavaScript class definition has been registered as a partial definition - `templateWillUpdate(name: string)` - Called before the template has been evaluated and assigned to the definition - -**Template Processing Callbacks:** - `templateDidUpdate(name: string)` - Called after the template has been assigned to the definition - `elementDidDefine(name: string)` - Called after the custom element has been fully defined with the platform - -**Hydration Callbacks:** -- `hydrationStarted()` - Called once when the first prerendered element begins hydrating - `elementWillHydrate(source: HTMLElement)` - Called before an element begins hydration - `elementDidHydrate(source: HTMLElement)` - Called after an element completes hydration + +**Global hydration callbacks** — passed to `enableHydration()`: +- `hydrationStarted()` - Called once when the first prerendered element begins hydrating - `hydrationComplete()` - Called once after all prerendered elements have completed hydration -Hydration callbacks are tracked at the element level by `ElementController`. The `hydrationComplete` callback fires only after every prerendered element has finished binding. +The `hydrationComplete` callback fires only after every prerendered element has finished binding. ### Callback Execution Order @@ -186,7 +185,7 @@ Template Processing Phase (asynchronous): 4. templateDidUpdate(name) 5. elementDidDefine(name) -Hydration Phase (per element): +Hydration Phase (per element, only when enableHydration() has been called): 6. hydrationStarted() [once, on first element] 7. elementWillHydrate(source) 8. [Hydration occurs] @@ -200,33 +199,46 @@ Completion (called once for all elements): ### Configuring Callbacks -Configure callbacks using `TemplateElement.config()` before defining the template element: +Hydration must be explicitly opted into by calling `enableHydration()`. Per-element +callbacks are passed directly to `declarativeTemplate()`: ```typescript -import { TemplateElement, type HydrationLifecycleCallbacks } from "@microsoft/fast-element/declarative.js"; +import { enableHydration } from "@microsoft/fast-element/hydration.js"; +import { declarativeTemplate } from "@microsoft/fast-element/declarative.js"; -TemplateElement.config({ - elementDidRegister(name) { - console.log(`${name} registered`); - }, - templateWillUpdate(name) { - console.log(`${name} template updating`); - }, - templateDidUpdate(name) { - console.log(`${name} template updated`); - }, - elementDidDefine(name) { - console.log(`${name} fully defined`); - }, - elementWillHydrate(source) { - console.log(`${source.localName} starting hydration`); - }, - elementDidHydrate(source) { - console.log(`${source.localName} hydrated`); +// Global hydration events +enableHydration({ + hydrationStarted() { + console.log("Hydration started"); }, hydrationComplete() { - console.log('All elements hydrated'); - } + console.log("All elements hydrated"); + }, +}); + +// Per-element lifecycle callbacks +MyComponent.define({ + name: "my-component", + template: declarativeTemplate({ + elementDidRegister(name) { + console.log(`${name} registered`); + }, + templateWillUpdate(name) { + console.log(`${name} template updating`); + }, + templateDidUpdate(name) { + console.log(`${name} template updated`); + }, + elementDidDefine(name) { + console.log(`${name} fully defined`); + }, + elementWillHydrate(source) { + console.log(`${source.localName} starting hydration`); + }, + elementDidHydrate(source) { + console.log(`${source.localName} hydrated`); + }, + }), }); ``` @@ -234,30 +246,43 @@ TemplateElement.config({ **Performance Monitoring:** ```typescript -TemplateElement.config({ - elementWillHydrate(source) { - performance.mark(`${source.localName}-hydration-start`); - }, - elementDidHydrate(source) { - performance.mark(`${source.localName}-hydration-end`); - performance.measure(`${source.localName}-hydration`, `${source.localName}-hydration-start`, `${source.localName}-hydration-end`); - }, +import { enableHydration } from "@microsoft/fast-element/hydration.js"; +import { declarativeTemplate } from "@microsoft/fast-element/declarative.js"; + +enableHydration({ hydrationComplete() { - const measures = performance.getEntriesByType('measure'); + const measures = performance.getEntriesByType("measure"); // Send metrics to analytics - } + }, +}); + +MyComponent.define({ + name: "my-component", + template: declarativeTemplate({ + elementWillHydrate(source) { + performance.mark(`${source.localName}-hydration-start`); + }, + elementDidHydrate(source) { + performance.mark(`${source.localName}-hydration-end`); + performance.measure( + `${source.localName}-hydration`, + `${source.localName}-hydration-start`, + `${source.localName}-hydration-end`, + ); + }, + }), }); ``` **Loading State Management:** ```typescript -TemplateElement.config({ +enableHydration({ hydrationStarted() { - document.body.classList.add('hydrating'); + document.body.classList.add("hydrating"); }, hydrationComplete() { - document.body.classList.remove('hydrating'); - document.body.classList.add('interactive'); - } + document.body.classList.remove("hydrating"); + document.body.classList.add("interactive"); + }, }); ``` diff --git a/packages/fast-element/DESIGN.md b/packages/fast-element/DESIGN.md index e59a9212537..a0b225c8085 100644 --- a/packages/fast-element/DESIGN.md +++ b/packages/fast-element/DESIGN.md @@ -10,7 +10,7 @@ For deep dives into specific areas, see the linked detailed documents. 1. [High-Level Overview](#high-level-overview) 2. [Core Concepts](#core-concepts) - - [FAST Global](#fast-global) + - [FAST Object](#fast-object) - [FASTElement & ElementController](#fastelement--elementcontroller) - [Observables & Notifiers](#observables--notifiers) - [Bindings](#bindings) @@ -49,30 +49,22 @@ For deep dives into specific areas, see the linked detailed documents. | Context protocol | W3C community Context protocol (`Context.create`, `Context.for`) | | Reactive state helpers | `state()`, `watch()` (beta) | -The library's kernel (the `FAST` global, the `Updates` queue, and the `Observable` system) is stored on `globalThis.FAST`. - -The v3 runtime assumes native `globalThis`. `src/polyfills.ts` only backfills -`requestIdleCallback` / `cancelIdleCallback`, so applications targeting older -engines must install their own `globalThis` polyfill before FAST loads. +The library's kernel (the `FAST` object, the `Updates` queue, and the `Observable` system) is module-scoped — imported from `@microsoft/fast-element` rather than stored on `globalThis`. --- ## Core Concepts -### FAST Global +### FAST Object **File**: `src/platform.ts`, `src/interfaces.ts` -`FAST` is a singleton object attached to `globalThis`. It provides: +`FAST` is a module-scoped singleton exported from `@microsoft/fast-element`. It is **not** attached to `globalThis`. It provides: -- `FAST.getById(id, initializer)` – shared kernel slot registry (used to share the update queue and observable system across FAST instances) - `FAST.warn(code, values)` / `FAST.error(code, values)` – structured diagnostic messages - `FAST.addMessages(dict)` – registers human-readable debug messages used by `enableDebug()` and declarative runtime diagnostics -The `KernelServiceId` enum provides the fixed numeric keys used for shared -services on the `FAST` global. These stable IDs let FAST instances on the same -page reuse the update queue, observable system, context event, and element -registry. +The previous `FAST.getById()` slot registry, `FASTGlobal` type, and `KernelServiceId` enum have been removed. Kernel services (update queue, observable system, etc.) are resolved through standard ES module imports rather than a shared global registry. --- @@ -90,15 +82,15 @@ registry. `ElementController` is the real workhorse. It: -- Extends `PropertyChangeNotifier` so the element itself participates in the observable system. +- Uses composition with an internal `_notifier` field (rather than extending `PropertyChangeNotifier`) and implements the `Notifier` interface directly, so the element still participates in the observable system. - Holds the element's `FASTElementDefinition` (name, template, styles, observed attributes). - Manages a `Stages` state machine: `disconnected → connecting → connected → disconnecting → disconnected`. -- Exposes `isPrerendered: Promise` which resolves to `true` after prerendered content has been hydrated, or `false` when the component is client-side rendered. The `ViewController` interface also exposes `isPrerendered` as `Promise` for custom directives. Attribute-skip logic during the hydration bind uses an internal `_skipAttrUpdates` flag that is never exposed as a public boolean. +- Exposes `isPrerendered: Promise` which resolves to `true` when the element had a declarative shadow root (DSD) at connect time, regardless of whether hydration ran. Exposes `isHydrated: Promise` which resolves to `true` only when hydration actually ran successfully. The `ViewController` interface also exposes both `isPrerendered` and `isHydrated` as `Promise` for custom directives. Attribute-skip logic during the hydration bind uses an internal `_skipAttrUpdates` flag that is never exposed as a public boolean. - On `connect()`: restores pre-upgrade observable values, calls `connectedCallback` on all `HostBehavior`s, renders the current template into the shadow root when one is available, and applies styles. -- Rendering is split into two modular paths via `renderPrerendered()` and `renderClientSide()`: - - **Prerendered**: `renderPrerendered()` registers the element in the static hydration tracker, swaps `onAttributeChangedCallback` to a no-op so the upgrade-time burst of callbacks is discarded, hydrates the existing DOM via `template.hydrate()`, then restores the standard handler and removes the element from the tracker. After this point, all future attribute changes flow through the real handler with zero overhead. +- Rendering is split into two modular paths. Hydration is pluggable: `enableHydration()` from `@microsoft/fast-element/hydration.js` installs a hook via `ElementController.installHydrationHook()`, keeping zero hydration imports in the core controller: + - **Prerendered**: The hydration hook (installed by `enableHydration()`) registers the element in the static hydration tracker, fires the definition's `elementWillHydrate` callback, swaps `onAttributeChangedCallback` to a no-op so the upgrade-time burst of callbacks is discarded, hydrates the existing DOM via `template.hydrate()`, fires `elementDidHydrate`, then restores the standard handler and removes the element from the tracker. The entire method is wrapped in `try/finally` to guarantee cleanup even if an error occurs during hydration. After this point, all future attribute changes flow through the real handler with zero overhead. - **Client-side**: `renderClientSide()` clones the compiled fragment, binds, and appends to the host — the standard path with no prerender logic. -- **Static hydration tracking**: `ElementController` delegates element-level hydration tracking to a `HydrationTracker` instance (in `hydration-tracker.ts`). `configHydration()` creates the tracker with the provided callbacks. `HydrationTracker` manages a `Set` of pending elements, fires per-element callbacks (`hydrationStarted`, `elementWillHydrate`, `elementDidHydrate`), and fires `hydrationComplete` via a debounced `setTimeout(0)` after the last element finishes binding — ensuring all async template batches settle first. +- **Static hydration tracking**: Hydration is opt-in via `enableHydration()` from `@microsoft/fast-element/hydration.js`, which creates a `HydrationTracker` and installs a pluggable hydration hook on `ElementController` via `ElementController.installHydrationHook()`. Until this is called, `renderTemplate()` always uses the client-side path — even if the element has a pre-existing shadow root. `HydrationTracker` manages a `Set` of pending elements, fires global callbacks (`hydrationStarted`, `hydrationComplete`), and fires `hydrationComplete` via a debounced `setTimeout(0)` after the last element finishes binding — ensuring all async template batches settle first. Per-element hydration callbacks (`elementWillHydrate`, `elementDidHydrate`) are stored on the `FASTElementDefinition.lifecycleCallbacks` and fired directly by the hydration hook. - On `disconnect()`: calls `disconnectedCallback` on behaviors, unbinds the view. - `onAttributeChangedCallback()` is the standard handler that processes attribute changes. During the prerendered bind, it is temporarily swapped to a no-op (see above) to avoid redundant processing of server-rendered attribute values. - Exposes `addBehavior` / `removeBehavior` for dynamic `HostBehavior` management (used by `ElementStyles`). @@ -255,7 +247,7 @@ See [src/templating/TEMPLATE-BINDINGS.md](./src/templating/TEMPLATE-BINDINGS.md) **Exported as**: `Updates` -`Updates` is a shared, batched task queue used to synchronise writes to the DOM. It is stored on the `FAST` global (under `KernelServiceId.updateQueue`) so multiple FAST instances on the same page share a single flush cycle. +`Updates` is a shared, batched task queue used to synchronise writes to the DOM. It is resolved through standard ES module imports so multiple parts of the application share a single flush cycle. - `Updates.enqueue(callable)` – schedules a task for the next batch. - `Updates.process()` – forces immediate synchronous flush (useful in tests). @@ -274,7 +266,9 @@ See [ARCHITECTURE_UPDATES.md](./ARCHITECTURE_UPDATES.md) for more detail. **Files**: `src/styles/css.ts`, `src/styles/element-styles.ts`, `src/styles/css-directive.ts` -The `css` tag (analogous to `html`) builds `ElementStyles` objects. During `ElementController.connect()`, styles are applied to the element's shadow root either via `adoptedStylesheets` (preferred) or an appended `