Skip to content

Commit b78786a

Browse files
authored
feat: modularize FASTElement with opt-in hydration and subpath exports (#7497)
# Pull Request ## 📖 Description Modularizes `@microsoft/fast-element` to reduce core bundle size and make hydration, styles, and array observation opt-in through subpath exports. **Hydration is now opt-in** — `enableHydration()` from `@microsoft/fast-element/hydration.js` activates hydration via a pluggable hook. Without it, `ElementController` has zero hydration imports or logic. **New subpath exports** split heavy features out of the main barrel: - `./styles.js` — `css`, `ElementStyles`, `CSSDirective`, style strategies - `./arrays.js` — `ArrayObserver`, `Splice`, `SpliceStrategy`, array utilities - `./hydration.js` — `enableHydration`, `deferHydrationAttribute` **`globalThis.FAST` removed** — `FAST` is now a module-scoped export with no side-effecting global mutation. `FAST.getById()`, `FASTGlobal`, `KernelServiceId`, and the `requestIdleCallback` polyfill are all removed. **Per-element lifecycle callbacks** are passed directly to `declarativeTemplate()`, and `isPrerendered` / `isHydrated` are now separate promises with clear semantics. ## 👩‍💻 Reviewer Notes - `TemplateElement.config()` is preserved as a deprecated shim for backward compatibility. - `ElementController` now uses composition for `PropertyChangeNotifier` instead of inheritance. - The hydration rendering path (`renderPrerendered`) moved entirely into `enable-hydration.ts` via `ElementController.installHydrationHook()`. - `isPrerendered` resolves `true` when a DSD shadow root existed (independent of hydration). `isHydrated` resolves `true` only when hydration actually ran. - Declarative template `{{ }}` bindings with spaces inside braces do not work — the templates.html files must use `{{binding}}` without spaces. ## 📑 Test Plan - 1396 Playwright tests pass (chromium) - 176 declarative fixture tests pass (chromium) - New `declarative-no-hydration` fixture (5 tests) verifies client-side rendering, lifecycle callbacks without hydration, and `isPrerendered`/`isHydrated` semantics - Updated `platform.pw.spec.ts` and `debug.pw.spec.ts` for module-scoped FAST - Updated `lifecycle-callbacks` and `performance-metrics` fixtures for new API ## ✅ Checklist ### General - [x] I have included a change request file using `$ npm run change` - [x] I have added tests for my changes. - [x] I have tested my changes. - [x] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/microsoft/fast/blob/main/CONTRIBUTING.md) documentation and followed the [standards](https://github.com/microsoft/fast/blob/main/CODE_OF_CONDUCT.md#our-standards) for this project. ## ⏭ Next Steps - Run full CI across all browsers (Firefox, WebKit) - Remove `TemplateElement.config()` deprecated shim in a future release - Consider lazy-loading the `Compiler` on first render for further size reduction
1 parent 803c757 commit b78786a

197 files changed

Lines changed: 3827 additions & 4866 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "feat: modularize hydration and expose lifecycle callbacks via enableHydration() and declarativeTemplate()",
4+
"packageName": "@microsoft/fast-element",
5+
"email": "7559015+janechu@users.noreply.github.com",
6+
"dependentChangeType": "none"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "Update imports for styles.js subpath export",
4+
"packageName": "@microsoft/fast-router",
5+
"email": "7559015+janechu@users.noreply.github.com",
6+
"dependentChangeType": "none"
7+
}

examples/todo-app/src/todo-app.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { css } from "@microsoft/fast-element";
1+
import { css } from "@microsoft/fast-element/styles.js";
22

33
export const styles = css`
44
:host {

examples/todo-app/src/todo-form.styles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { css } from "@microsoft/fast-element";
1+
import { css } from "@microsoft/fast-element/styles.js";
22

33
export const styles = css`
44
form {

packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,11 @@ Once the template is attached to the partial definition, the element completes i
9696
When custom elements are instantiated in the DOM, the following occurs:
9797

9898
1. **Element Creation**: The platform creates instances of the custom element
99-
2. **Prerendered Content Detection**: `ElementController` detects the existing shadow root from SSR and sets `isPrerendered = true`
100-
3. **Concrete Template Ready**: Because `declarativeTemplate()` resolved during
99+
2. **Prerendered Content Detection**: `ElementController` detects the existing shadow root from SSR — `isPrerendered` resolves `true`
100+
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.
101+
4. **Concrete Template Ready**: Because `declarativeTemplate()` resolved during
101102
definition, `connect()` starts with the final template already attached.
102-
4. **Hydration**: `ElementController` uses `template.hydrate()` to create a
103+
5. **Hydration**: `ElementController` uses `template.hydrate()` to create a
103104
`HydrationView` that maps existing DOM nodes to binding targets using `fe:b`
104105
/ `fe:/b` markers
105106

@@ -154,23 +155,21 @@ FAST HTML provides a set of lifecycle callbacks that allow you to hook into vari
154155

155156
### Available Callbacks
156157

157-
The lifecycle callbacks are organized into three categories:
158+
The lifecycle callbacks are split between two APIs:
158159

159-
**Template Registration Callbacks:**
160+
**Per-element callbacks** — passed to `declarativeTemplate()`:
160161
- `elementDidRegister(name: string)` - Called after the JavaScript class definition has been registered as a partial definition
161162
- `templateWillUpdate(name: string)` - Called before the template has been evaluated and assigned to the definition
162-
163-
**Template Processing Callbacks:**
164163
- `templateDidUpdate(name: string)` - Called after the template has been assigned to the definition
165164
- `elementDidDefine(name: string)` - Called after the custom element has been fully defined with the platform
166-
167-
**Hydration Callbacks:**
168-
- `hydrationStarted()` - Called once when the first prerendered element begins hydrating
169165
- `elementWillHydrate(source: HTMLElement)` - Called before an element begins hydration
170166
- `elementDidHydrate(source: HTMLElement)` - Called after an element completes hydration
167+
168+
**Global hydration callbacks** — passed to `enableHydration()`:
169+
- `hydrationStarted()` - Called once when the first prerendered element begins hydrating
171170
- `hydrationComplete()` - Called once after all prerendered elements have completed hydration
172171

173-
Hydration callbacks are tracked at the element level by `ElementController`. The `hydrationComplete` callback fires only after every prerendered element has finished binding.
172+
The `hydrationComplete` callback fires only after every prerendered element has finished binding.
174173

175174
### Callback Execution Order
176175

@@ -186,7 +185,7 @@ Template Processing Phase (asynchronous):
186185
4. templateDidUpdate(name)
187186
5. elementDidDefine(name)
188187
189-
Hydration Phase (per element):
188+
Hydration Phase (per element, only when enableHydration() has been called):
190189
6. hydrationStarted() [once, on first element]
191190
7. elementWillHydrate(source)
192191
8. [Hydration occurs]
@@ -200,64 +199,90 @@ Completion (called once for all elements):
200199

201200
### Configuring Callbacks
202201

203-
Configure callbacks using `TemplateElement.config()` before defining the template element:
202+
Hydration must be explicitly opted into by calling `enableHydration()`. Per-element
203+
callbacks are passed directly to `declarativeTemplate()`:
204204

205205
```typescript
206-
import { TemplateElement, type HydrationLifecycleCallbacks } from "@microsoft/fast-element/declarative.js";
206+
import { enableHydration } from "@microsoft/fast-element/hydration.js";
207+
import { declarativeTemplate } from "@microsoft/fast-element/declarative.js";
207208

208-
TemplateElement.config({
209-
elementDidRegister(name) {
210-
console.log(`${name} registered`);
211-
},
212-
templateWillUpdate(name) {
213-
console.log(`${name} template updating`);
214-
},
215-
templateDidUpdate(name) {
216-
console.log(`${name} template updated`);
217-
},
218-
elementDidDefine(name) {
219-
console.log(`${name} fully defined`);
220-
},
221-
elementWillHydrate(source) {
222-
console.log(`${source.localName} starting hydration`);
223-
},
224-
elementDidHydrate(source) {
225-
console.log(`${source.localName} hydrated`);
209+
// Global hydration events
210+
enableHydration({
211+
hydrationStarted() {
212+
console.log("Hydration started");
226213
},
227214
hydrationComplete() {
228-
console.log('All elements hydrated');
229-
}
215+
console.log("All elements hydrated");
216+
},
217+
});
218+
219+
// Per-element lifecycle callbacks
220+
MyComponent.define({
221+
name: "my-component",
222+
template: declarativeTemplate({
223+
elementDidRegister(name) {
224+
console.log(`${name} registered`);
225+
},
226+
templateWillUpdate(name) {
227+
console.log(`${name} template updating`);
228+
},
229+
templateDidUpdate(name) {
230+
console.log(`${name} template updated`);
231+
},
232+
elementDidDefine(name) {
233+
console.log(`${name} fully defined`);
234+
},
235+
elementWillHydrate(source) {
236+
console.log(`${source.localName} starting hydration`);
237+
},
238+
elementDidHydrate(source) {
239+
console.log(`${source.localName} hydrated`);
240+
},
241+
}),
230242
});
231243
```
232244

233245
### Use Cases
234246

235247
**Performance Monitoring:**
236248
```typescript
237-
TemplateElement.config({
238-
elementWillHydrate(source) {
239-
performance.mark(`${source.localName}-hydration-start`);
240-
},
241-
elementDidHydrate(source) {
242-
performance.mark(`${source.localName}-hydration-end`);
243-
performance.measure(`${source.localName}-hydration`, `${source.localName}-hydration-start`, `${source.localName}-hydration-end`);
244-
},
249+
import { enableHydration } from "@microsoft/fast-element/hydration.js";
250+
import { declarativeTemplate } from "@microsoft/fast-element/declarative.js";
251+
252+
enableHydration({
245253
hydrationComplete() {
246-
const measures = performance.getEntriesByType('measure');
254+
const measures = performance.getEntriesByType("measure");
247255
// Send metrics to analytics
248-
}
256+
},
257+
});
258+
259+
MyComponent.define({
260+
name: "my-component",
261+
template: declarativeTemplate({
262+
elementWillHydrate(source) {
263+
performance.mark(`${source.localName}-hydration-start`);
264+
},
265+
elementDidHydrate(source) {
266+
performance.mark(`${source.localName}-hydration-end`);
267+
performance.measure(
268+
`${source.localName}-hydration`,
269+
`${source.localName}-hydration-start`,
270+
`${source.localName}-hydration-end`,
271+
);
272+
},
273+
}),
249274
});
250275
```
251276

252277
**Loading State Management:**
253278
```typescript
254-
TemplateElement.config({
279+
enableHydration({
255280
hydrationStarted() {
256-
document.body.classList.add('hydrating');
281+
document.body.classList.add("hydrating");
257282
},
258283
hydrationComplete() {
259-
document.body.classList.remove('hydrating');
260-
document.body.classList.add('interactive');
261-
}
284+
document.body.classList.remove("hydrating");
285+
document.body.classList.add("interactive");
286+
},
262287
});
263288
```

0 commit comments

Comments
 (0)