Skip to content

Commit ab3f528

Browse files
janechuCopilot
andcommitted
feat: reduce declarative template size
Reduce declarative template entrypoint size by moving optional map and lifecycle support behind schema hooks, replacing the custom template element with a native publisher, and updating docs/fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent b78786a commit ab3f528

85 files changed

Lines changed: 1532 additions & 2124 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": "major",
3+
"comment": "Remove the public declarative TemplateElement configuration APIs and make declarative templates use an internal native f-template publisher with explicit hydration opt-in.",
4+
"packageName": "@microsoft/fast-element",
5+
"email": "7559015+janechu@users.noreply.github.com",
6+
"dependentChangeType": "none"
7+
}

packages/fast-element/DECLARATIVE_DESIGN.md

Lines changed: 182 additions & 111 deletions
Large diffs are not rendered by default.

packages/fast-element/DECLARATIVE_HTML.md

Lines changed: 64 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ This document focuses on declarative-runtime implementation details:
1010
template structure, prerendered markup requirements, lifecycle callbacks,
1111
binding configuration, syntax, and integration testing.
1212

13-
For package installation, importing `TemplateElement`, basic registration, and
13+
For package installation, using `declarativeTemplate()`, extension setup, and
1414
the package-level hydration overview, see the
1515
[FAST Element README](./README.md#declarative-html) and
1616
[Prerendered Content Optimization](./README.md#prerendered-content-optimization).
@@ -29,9 +29,10 @@ the relevant registry and waits for the matching declarative template when it is
2929
already present or inserted later.
3030

3131
The `@microsoft/fast-element/declarative.js` entrypoint itself remains
32-
side-effect free at import time. The hydratable `ViewTemplate` runtime is
33-
installed lazily when `TemplateParser`, `TemplateElement`, or
34-
`declarativeTemplate()` first create a declarative template.
32+
side-effect free at import time. Declarative APIs lazily install declarative
33+
debug messages when they create templates. Hydratable `ViewTemplate` support is
34+
installed only when `enableHydration()` is called from
35+
`@microsoft/fast-element/hydration.js`.
3536

3637
Example:
3738
```html
@@ -59,158 +60,77 @@ format and initial-state application details, see
5960

6061
## Lifecycle Callbacks
6162

62-
FAST Element's declarative entrypoint provides lifecycle callbacks that allow
63-
you to hook into various stages of template processing and element hydration.
64-
These callbacks are useful for tracking the rendering lifecycle, gathering
65-
analytics, or coordinating complex initialization sequences.
63+
FAST Element's declarative APIs provide lifecycle callbacks that allow you to
64+
hook into template processing and hydration. The callbacks are split by scope:
6665

67-
### Available Callbacks
68-
69-
**Template Lifecycle Callbacks:**
70-
- `elementDidRegister(name: string)` - Called after the JavaScript class definition has been registered
71-
- `templateWillUpdate(name: string)` - Called before the template has been evaluated and assigned
72-
- `templateDidUpdate(name: string)` - Called after the template has been assigned to the definition
73-
- `elementDidDefine(name: string)` - Called after the custom element has been defined
74-
75-
**Hydration Lifecycle Callbacks:**
76-
- `hydrationStarted()` - Called once when the first prerendered element begins hydrating
77-
- `elementWillHydrate(source: HTMLElement)` - Called before an element begins hydration
78-
- `elementDidHydrate(source: HTMLElement)` - Called after an element completes hydration
79-
- `hydrationComplete()` - Called after all prerendered elements have completed hydration
80-
81-
Hydration callbacks are tracked at the element level by `ElementController``hydrationComplete` fires only after every prerendered element has finished binding.
82-
83-
### Configuring Callbacks
66+
| Scope | API | Callbacks |
67+
|---|---|---|
68+
| Per element | `declarativeTemplate(callbacks)` | `elementDidRegister`, `templateWillUpdate`, `templateDidUpdate`, `elementDidDefine`, `elementWillHydrate`, `elementDidHydrate` |
69+
| Global hydration | `enableHydration(options)` | `hydrationStarted`, `hydrationComplete` |
8470

85-
Configure lifecycle callbacks using `TemplateElement.config()`:
71+
Hydration is opt-in. Call `enableHydration()` before FAST elements connect when
72+
you want prerendered Declarative Shadow DOM to be reused:
8673

8774
```typescript
88-
import { TemplateElement, type HydrationLifecycleCallbacks } from "@microsoft/fast-element/declarative.js";
75+
import { enableHydration } from "@microsoft/fast-element/hydration.js";
8976

90-
// You can configure all callbacks at once
91-
const callbacks: HydrationLifecycleCallbacks = {
92-
elementDidRegister(name: string) {
93-
console.log(`Element registered: ${name}`);
94-
},
95-
templateWillUpdate(name: string) {
96-
console.log(`Template updating: ${name}`);
97-
},
98-
templateDidUpdate(name: string) {
99-
console.log(`Template updated: ${name}`);
100-
},
101-
elementDidDefine(name: string) {
102-
console.log(`Element defined: ${name}`);
103-
},
104-
elementWillHydrate(source: HTMLElement) {
105-
console.log(`Element will hydrate: ${source.localName}`);
106-
},
107-
elementDidHydrate(source: HTMLElement) {
108-
console.log(`Element hydrated: ${source.localName}`);
77+
enableHydration({
78+
hydrationStarted() {
79+
console.log("Hydration started");
10980
},
11081
hydrationComplete() {
111-
console.log('All elements hydrated');
112-
}
113-
};
114-
115-
TemplateElement.config(callbacks);
116-
117-
// Or configure only the callbacks you need
118-
TemplateElement.config({
119-
elementDidHydrate(source: HTMLElement) {
120-
console.log(`${source.localName} is ready`);
82+
console.log("All elements hydrated");
12183
},
122-
hydrationComplete() {
123-
console.log('Page is interactive');
124-
}
12584
});
12685
```
12786

128-
### Lifecycle Order
129-
130-
The lifecycle callbacks occur in the following general sequence:
131-
132-
1. **Registration Phase**: `elementDidRegister` is called when the element class is registered
133-
2. **Template Phase**: `templateWillUpdate` → (template processing) → `templateDidUpdate``elementDidDefine`
134-
3. **Hydration Phase**: `hydrationStarted``elementWillHydrate` → (hydration) → `elementDidHydrate`
135-
4. **Completion**: `hydrationComplete` is called after all prerendered elements finish hydrating
87+
Pass per-element lifecycle callbacks directly to `declarativeTemplate()`:
13688

137-
**Note:** Template processing is asynchronous and happens independently for each element. The template and hydration phases can be interleaved when multiple elements are being processed simultaneously.
138-
139-
### Use Cases
140-
141-
**Performance Monitoring:**
14289
```typescript
143-
TemplateElement.config({
144-
elementWillHydrate(source: HTMLElement) {
145-
performance.mark(`${source.localName}-hydration-start`);
146-
},
147-
elementDidHydrate(source: HTMLElement) {
148-
performance.mark(`${source.localName}-hydration-end`);
149-
performance.measure(
150-
`${source.localName}-hydration`,
151-
`${source.localName}-hydration-start`,
152-
`${source.localName}-hydration-end`
153-
);
154-
},
155-
hydrationComplete() {
156-
const entries = performance.getEntriesByType('measure');
157-
console.log('Hydration metrics:', entries);
158-
}
159-
});
160-
```
90+
import { declarativeTemplate } from "@microsoft/fast-element/declarative.js";
16191

162-
**Loading State Management:**
163-
```typescript
164-
TemplateElement.config({
165-
hydrationStarted() {
166-
document.body.classList.add('hydrating');
167-
},
168-
hydrationComplete() {
169-
document.body.classList.remove('hydrating');
170-
document.body.classList.add('hydrated');
171-
}
172-
});
173-
```
174-
175-
**Debugging and Development:**
176-
```typescript
177-
if (process.env.NODE_ENV === 'development') {
178-
const events: Array<{callback: string; name?: string; timestamp: number}> = [];
179-
180-
TemplateElement.config({
92+
MyComponent.define({
93+
name: "my-component",
94+
template: declarativeTemplate({
18195
elementDidRegister(name) {
182-
events.push({ callback: 'elementDidRegister', name, timestamp: Date.now() });
96+
console.log(`Element registered: ${name}`);
18397
},
18498
templateWillUpdate(name) {
185-
events.push({ callback: 'templateWillUpdate', name, timestamp: Date.now() });
99+
console.log(`Template updating: ${name}`);
186100
},
187101
templateDidUpdate(name) {
188-
events.push({ callback: 'templateDidUpdate', name, timestamp: Date.now() });
102+
console.log(`Template updated: ${name}`);
189103
},
190104
elementDidDefine(name) {
191-
events.push({ callback: 'elementDidDefine', name, timestamp: Date.now() });
105+
console.log(`Element defined: ${name}`);
192106
},
193107
elementWillHydrate(source) {
194-
events.push({ callback: 'elementWillHydrate', name: source.localName, timestamp: Date.now() });
108+
console.log(`Element will hydrate: ${source.localName}`);
195109
},
196110
elementDidHydrate(source) {
197-
events.push({ callback: 'elementDidHydrate', name: source.localName, timestamp: Date.now() });
111+
console.log(`Element hydrated: ${source.localName}`);
198112
},
199-
hydrationComplete() {
200-
events.push({ callback: 'hydrationComplete', timestamp: Date.now() });
201-
console.table(events);
202-
}
203-
});
204-
}
113+
}),
114+
});
205115
```
206116

117+
The lifecycle callbacks occur in this general sequence:
118+
119+
1. `elementDidRegister(name)`
120+
2. `templateWillUpdate(name)` → template processing → `templateDidUpdate(name)`
121+
3. `elementDidDefine(name)`
122+
4. If `enableHydration()` was called and the element has prerendered content:
123+
`hydrationStarted()``elementWillHydrate(source)` → hydration →
124+
`elementDidHydrate(source)``hydrationComplete()`
125+
126+
Template processing is asynchronous and happens independently for each element,
127+
so callbacks for different elements may interleave.
128+
207129
## `observerMap`
208130

209131
When the `observerMap()` extension is applied to an element definition,
210132
`@microsoft/fast-element/declarative.js` automatically sets up deep reactive
211-
observation for all root properties discovered in the template.
212-
`TemplateElement.options()` remains available as a compatibility fallback via
213-
`observerMap: {}`.
133+
observation for root properties discovered in the template.
214134

215135
For finer control, pass a configuration object with a `properties` key that maps root property names to a recursive path tree:
216136

@@ -245,7 +165,7 @@ Each path entry can be:
245165
Use `$observe: false` on a node to skip it by default, then re-include specific children:
246166

247167
```typescript
248-
observerMap: {
168+
observerMap({
249169
properties: {
250170
analytics: {
251171
charts: {
@@ -254,7 +174,7 @@ observerMap: {
254174
},
255175
},
256176
},
257-
}
177+
});
258178
```
259179

260180
When `properties` is omitted, all root properties are observed. When
@@ -265,15 +185,12 @@ are observed.
265185

266186
When the `attributeMap()` extension is applied to an element definition,
267187
`@microsoft/fast-element/declarative.js` automatically creates reactive `@attr`
268-
properties for every **leaf binding** in the template — simple expressions
269-
like `{{foo}}` or `id="{{foo-bar}}"` that have no nested properties. The
270-
default behavior uses the `"none"` attribute name strategy.
271-
`TemplateElement.options()` remains available as a compatibility fallback via
272-
`attributeMap: {}`.
273-
274-
By default, the **attribute name** and **property name** are both the binding key exactly as written in the template — no normalization is applied. Because HTML attributes are case-insensitive, binding keys should use lowercase names (optionally dash-separated). Properties with dashes must be accessed via bracket notation (e.g. `element["foo-bar"]`).
188+
properties for every **leaf binding** in the template — simple expressions like
189+
`{{foo}}` or `id="{{fooBar}}"` that have no nested properties.
275190

276-
Properties already decorated with `@attr` or `@observable` on the class are left untouched.
191+
By default, the binding key is treated as a camelCase property name and the HTML
192+
attribute name is derived by converting it to kebab-case. Properties already
193+
decorated with `@attr` or `@observable` on the class are left untouched.
277194

278195
```typescript
279196
MyElement.define(
@@ -291,21 +208,26 @@ With the template:
291208
<f-template name="my-element">
292209
<template>
293210
<p>{{greeting}}</p>
294-
<p>{{first-name}}</p>
211+
<p>{{firstName}}</p>
295212
</template>
296213
</f-template>
297214
```
298215

299-
This registers `greeting` (attribute `greeting`, property `greeting`) and `first-name` (attribute `first-name`, property `first-name`) as `@attr` properties on the element prototype, enabling `setAttribute("first-name", "Jane")` to trigger a template re-render automatically.
216+
This registers `greeting` (attribute `greeting`, property `greeting`) and
217+
`firstName` (attribute `first-name`, property `firstName`) as `@attr`
218+
properties on the element prototype, enabling `setAttribute("first-name",
219+
"Jane")` to trigger a template re-render automatically.
300220

301221
### `attribute-name-strategy`
302222

303-
The `attribute-name-strategy` configuration option controls how template binding keys map to HTML attribute names. This matches the build-time `--attribute-name-strategy` option in `@microsoft/fast-build`.
223+
The `attribute-name-strategy` configuration option controls how template binding
224+
keys map to HTML attribute names. This matches the build-time
225+
`--attribute-name-strategy` option in `@microsoft/fast-build`.
304226

305227
| Strategy | Behaviour | Example |
306228
|---|---|---|
307-
| `"none"` (default) | Binding key used as-is for both property and attribute | `{{foo-bar}}` → property `foo-bar`, attribute `foo-bar` |
308-
| `"camelCase"` | Binding key is the camelCase property; attribute name derived as kebab-case | `{{fooBar}}` → property `fooBar`, attribute `foo-bar` |
229+
| `"camelCase"` (default) | Binding key is the camelCase property; attribute name is derived as kebab-case | `{{fooBar}}` → property `fooBar`, attribute `foo-bar` |
230+
| `"none"` | Binding key used as-is for both property and attribute | `{{foo-bar}}` → property `foo-bar`, attribute `foo-bar` |
309231

310232
```typescript
311233
MyElement.define(
@@ -315,24 +237,14 @@ MyElement.define(
315237
},
316238
[
317239
attributeMap({
318-
"attribute-name-strategy": "camelCase",
240+
"attribute-name-strategy": "none",
319241
}),
320242
],
321243
);
322244
```
323245

324-
With the template:
325-
326-
```html
327-
<f-template name="my-element">
328-
<template>
329-
<p>{{greeting}}</p>
330-
<p>{{firstName}}</p>
331-
</template>
332-
</f-template>
333-
```
334-
335-
This registers `greeting` (attribute `greeting`, property `greeting`) and `firstName` (attribute `first-name`, property `firstName`) as `@attr` properties. `setAttribute("first-name", "Jane")` triggers a re-render, and the property is accessible as `element.firstName`.
246+
When using the `"none"` strategy, property names may contain dashes and must be
247+
accessed via bracket notation (e.g. `element["foo-bar"]`).
336248

337249
## Syntax
338250

packages/fast-element/DECLARATIVE_MIGRATION.md

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,26 +129,47 @@ Public imports now come from
129129
|---|---|
130130
| `Schema.jsonSchemaMap.get('my-element')` | `import { schemaRegistry } from "@microsoft/fast-element/declarative.js"; schemaRegistry.get('my-element')` |
131131

132-
### New public exports
132+
### Public exports
133133

134-
The following are now part of the public API:
134+
The public entrypoint exports the functional declarative API:
135135

136136
| Export | Purpose |
137137
|---|---|
138+
| `declarativeTemplate()` | Resolves `<f-template>` markup for a FAST element definition |
139+
| `attributeMap()` | Definition extension for automatic `@attr` property registration |
140+
| `observerMap()` | Definition extension for automatic deep observation |
138141
| `Schema` | JSON schema builder class |
139142
| `schemaRegistry` | Module-level registry for cross-element schema lookups |
140-
| `AttributeMap` | Automatic `@attr` property registration |
141-
| `AttributeMapOption` | Constant for the `"all"` option value |
142143
| `JSONSchema` | JSON Schema type interface |
143144
| `CachedPathMap` | Schema registry map type |
144145

145146
## Simplified ObserverMap and AttributeMap defaults
146147

147148
The explicit `ObserverMapOption.all` and `AttributeMapOption.all` constants have
148-
been removed. Calling `observerMap()` or `attributeMap()` with no arguments now
149-
applies the default "all properties" behavior.
149+
been removed. Calling `observerMap()` with no arguments observes every
150+
discovered root property, and calling `attributeMap()` with no arguments maps
151+
every discovered leaf binding.
150152

151153
| Before | After |
152154
|---|---|
153155
| `observerMap(ObserverMapOption.all)` | `observerMap()` |
154156
| `attributeMap(AttributeMapOption.all)` | `attributeMap()` |
157+
158+
159+
## Declarative TemplateElement API removal
160+
161+
The public declarative API is now the functional API. The `<f-template>`
162+
implementation is internal and is defined automatically by `declarativeTemplate()`.
163+
164+
| Removed | Replacement |
165+
|---|---|
166+
| `TemplateElement` public export | `declarativeTemplate()` on each FAST element definition |
167+
| `TemplateElement.define({ name: "f-template" })` | No manual definition; `declarativeTemplate()` defines the internal publisher in the target registry |
168+
| `TemplateElement.config(callbacks)` / `HydrationLifecycleCallbacks` | Per-element callbacks via `declarativeTemplate(callbacks)` and global hydration callbacks via `enableHydration(options)` |
169+
| `TemplateElement.options({ "my-el": { attributeMap, observerMap } })` | Define extensions: `MyElement.define(definition, [attributeMap(...), observerMap(...)])` |
170+
| `ElementOptions` / `ElementOptionsDictionary` | No replacement |
171+
| `AttributeMap` / `ObserverMap` public entrypoint exports | `attributeMap()` / `observerMap()` extension helpers and their config types |
172+
173+
Hydration is also no longer installed by `@microsoft/fast-element/declarative.js`.
174+
Call `enableHydration()` from `@microsoft/fast-element/hydration.js` before FAST
175+
elements connect when prerendered Declarative Shadow DOM should be reused.

0 commit comments

Comments
 (0)