Skip to content

Commit 9f05f56

Browse files
janechuCopilot
andcommitted
feat: add declarativeTemplate bridge
Introduce declarativeTemplate() and a registry-aware f-template publication bridge so FASTElement definitions can wait for DOM-authored templates without manual TemplateElement setup while preserving the legacy defer-and-hydrate fallback. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a4b11b8 commit 9f05f56

41 files changed

Lines changed: 1258 additions & 344 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": "add declarativeTemplate for auto-resolving <f-template> markup",
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: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ The `ObserverMapConfig` interface accepts an optional `properties` key that maps
103103
MyElement.define(
104104
{
105105
name: "my-element",
106-
templateOptions: "defer-and-hydrate",
106+
template: declarativeTemplate(),
107107
},
108108
[
109109
observerMap({
@@ -482,27 +482,25 @@ sequenceDiagram
482482
participant EC as ElementController
483483
participant Callbacks as HydrationLifecycleCallbacks
484484
485-
App->>FER: MyElement.define({name:'my-el', ...})
486-
note over FER: partial definition stored
485+
App->>FER: await MyElement.define({name:'my-el', template: declarativeTemplate()})
486+
note over FER: definition composed; resolver waits for template
487487
488-
App->>TE: TemplateElement.define({name:'f-template'})
489488
App->>TE: TemplateElement.config(callbacks)
490489
App->>TE: TemplateElement.options({'my-el':{observerMap:{}}})
491490
492491
DOM->>TE: f-template connected to DOM
493-
TE->>FER: FASTElementDefinition.register('my-el')
494-
FER-->>TE: resolves with MyElement class
492+
TE->>FER: bridge matches registry + name
495493
TE->>Callbacks: elementDidRegister('my-el')
496494
TE->>Callbacks: templateWillUpdate('my-el')
497-
TE->>TE: parse template → ViewTemplate
498-
TE->>FER: registeredFastElement.template = viewTemplate
495+
TE->>TE: parse template → schema → maps → ViewTemplate
496+
TE->>FER: return viewTemplate to resolver
499497
TE->>Callbacks: templateDidUpdate('my-el')
500-
FER->>FER: compose() → platform registry
498+
FER->>FER: customElements.define('my-el', MyElement)
501499
FER->>Callbacks: elementDidDefine('my-el')
502500
503501
DOM->>EC: element instance connects with existing shadow root
504502
EC->>EC: isPrerendered = true (existing shadow root detected)
505-
EC->>EC: template-pending guard: wait if no template yet
503+
EC->>EC: concrete template already attached
506504
EC->>Callbacks: hydrationStarted()
507505
EC->>Callbacks: elementWillHydrate(element)
508506
EC->>EC: template.hydrate() — maps existing DOM to binding targets
@@ -550,21 +548,21 @@ tagged templates produce.
550548
| `children(prop)` | FAST directive used for `f-children` |
551549
| `ref(prop)` | FAST directive used for `f-ref` |
552550

553-
### Template attachment after define
551+
### Deferred template attachment via define
554552

555-
Standard `FASTElement.define()` returns a `Promise` that resolves immediately once the definition has been composed and any async template resolver has settled. Declarative HTML can define a host element without an initial template and let `<f-template>` attach the template later through `FASTElementDefinition.template`. This unified API replaces the previous `defineAsync()` / `composeAsync()` methods.
553+
Standard `FASTElement.define()` returns a `Promise` that resolves immediately when a concrete template is provided at definition time. When `template: declarativeTemplate()` is used, the `Promise` resolves after the matching `<f-template>` supplies a concrete template through the bridge. This unified API replaces the previous `defineAsync()` / `composeAsync()` methods.
556554

557555
---
558556

559557
## Hydration Model
560558

561-
For declarative hydration, the server must render:
559+
When declarative templates are used, the server must render:
562560

563561
1. The custom element tag with its attributes and initial state.
564562
2. A `<template shadowrootmode="open">` containing pre-rendered HTML annotated with FAST's hydration markers.
565563
3. An `<f-template>` element somewhere in the page that carries the template definition.
566564

567-
If a template is attached after an element has already connected, the observable `template` update recreates the controller so hydration can proceed against the existing prerendered markup. The `defer-hydration` and `needs-hydration` attributes are no longer needed in server-rendered markup.
565+
With `declarativeTemplate()`, connection gating happens before platform registration: the resolver waits for the matching `<f-template>` and keeps the definition concrete before elements can connect. Hydration can therefore start immediately when `ElementController.connect()` runs. The `defer-hydration` and `needs-hydration` attributes are no longer needed in server-rendered markup.
568566

569567
### Hydration marker formats
570568

packages/fast-element/DECLARATIVE_HTML.md

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ the package-level hydration overview, see the
2020
After registering the declarative entrypoint as shown in the README, templates
2121
are associated with an element through
2222
`<f-template name="[custom-element-name]"><template>...</template></f-template>`.
23-
The host custom element should be defined before the declarative runtime
24-
processes the matching `<f-template>`.
23+
The host custom element should be defined with
24+
`template: declarativeTemplate()`. This automatically defines `<f-template>` in
25+
the relevant registry and waits for the matching declarative template when it is
26+
already present or inserted later.
2527

2628
Example:
2729
```html
@@ -208,7 +210,7 @@ For finer control, pass a configuration object with a `properties` key that maps
208210
UserProfile.define(
209211
{
210212
name: "user-profile",
211-
templateOptions: "defer-and-hydrate",
213+
template: declarativeTemplate(),
212214
},
213215
[
214216
observerMap({
@@ -225,8 +227,6 @@ UserProfile.define(
225227
}),
226228
],
227229
);
228-
229-
TemplateElement.define({ name: "f-template" });
230230
```
231231

232232
Each path entry can be:
@@ -271,12 +271,10 @@ Properties already decorated with `@attr` or `@observable` on the class are left
271271
MyElement.define(
272272
{
273273
name: "my-element",
274-
templateOptions: "defer-and-hydrate",
274+
template: declarativeTemplate(),
275275
},
276276
[attributeMap()],
277277
);
278-
279-
TemplateElement.define({ name: "f-template" });
280278
```
281279

282280
With the template:
@@ -305,16 +303,14 @@ The `attribute-name-strategy` configuration option controls how template binding
305303
MyElement.define(
306304
{
307305
name: "my-element",
308-
templateOptions: "defer-and-hydrate",
306+
template: declarativeTemplate(),
309307
},
310308
[
311309
attributeMap({
312310
"attribute-name-strategy": "camelCase",
313311
}),
314312
],
315313
);
316-
317-
TemplateElement.define({ name: "f-template" });
318314
```
319315

320316
With the template:

packages/fast-element/DECLARATIVE_MIGRATION.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ See the [`@microsoft/fast-element` MIGRATION.md](../fast-element/MIGRATION.md#hy
3535

3636
### Migration steps
3737

38-
1. Replace `RenderableFASTElement(MyComponent).defineAsync({...})` with `MyComponent.define({...})`.
38+
1. Replace `RenderableFASTElement(MyComponent).defineAsync({...})` with
39+
`await MyComponent.define({...})` and use `declarativeTemplate()` for
40+
declarative templates. `declarativeTemplate()` is the waiting behavior: it
41+
resolves the matching `<f-template>` before `define()` completes.
3942

4043
```typescript
4144
// Before
@@ -46,9 +49,9 @@ See the [`@microsoft/fast-element` MIGRATION.md](../fast-element/MIGRATION.md#hy
4649
});
4750

4851
// After
49-
MyComponent.define({
52+
await MyComponent.define({
5053
name: "my-component",
51-
templateOptions: "defer-and-hydrate",
54+
template: declarativeTemplate(),
5255
});
5356
```
5457

packages/fast-element/DECLARATIVE_RENDERING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,16 @@ export class MyComponent extends FASTElement {
9191

9292
MyComponent.define({
9393
name: "my-component",
94+
template: declarativeTemplate(),
9495
});
9596
```
9697

97-
When the element connects, `ElementController` automatically detects the existing shadow root from SSR and sets `isPrerendered = true`. If the template is attached after the element has already connected, the observable `template` update recreates the controller so hydration can proceed. The `defer-hydration` and `needs-hydration` attributes are no longer needed.
98+
When the element connects, `ElementController` automatically detects the
99+
existing shadow root from SSR and sets `isPrerendered = true`.
100+
`declarativeTemplate()` keeps the definition template concrete before
101+
registration completes, so the element can hydrate the prerendered shadow root
102+
immediately. The `defer-hydration` and `needs-hydration` attributes are no
103+
longer needed.
98104

99105
## Hydration Comments and Datasets
100106

packages/fast-element/DECLARATIVE_RENDERING_LIFECYCLE.md

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ core runtime and its declarative entrypoint:
1313
- **`@microsoft/fast-element/declarative.js`**: Provides the `f-template`
1414
custom element that processes HTML templates and attaches them to FAST
1515
elements as a `ViewTemplate` in lieu of an `html` template created during
16-
`FASTElement.define()`. When using `f-template`, the host element can be
17-
defined without an initial template and the declarative runtime attaches the
18-
template later.
16+
`FASTElement.define()`. The preferred path uses `declarativeTemplate()` so
17+
`FASTElement.define()` waits for the matching declarative template and keeps
18+
the definition concrete before registration completes.
1919

2020
## Lifecycle Phases
2121

@@ -37,69 +37,71 @@ Given a DOM which includes an `f-template` and a component:
3737

3838
The following phases will then be kicked off once the JavaScript is parsed.
3939

40-
### Phase 1: Partial Element Registration
40+
### Phase 1: Definition Resolution
4141

42-
Custom elements begin their lifecycle by registering with FAST via the
43-
`define()` method before their declarative template is available.
42+
Custom elements begin their lifecycle by composing a definition that points at
43+
`declarativeTemplate()`. The resolver waits for a matching declarative template
44+
and returns a concrete `ViewTemplate` before the platform registration step.
4445

4546
```typescript
4647
// Custom element class definition
4748
class MyComponent extends FASTElement {
4849
@attr text: string = "";
4950
}
5051

51-
// Register the host element before the declarative template is attached
52-
MyComponent.define({
52+
// Register with the declarative template bridge
53+
await MyComponent.define({
5354
name: "my-component",
55+
template: declarativeTemplate(),
5456
});
5557
```
5658

5759
Key characteristics of this phase:
58-
- Element is registered before template attachment
59-
- The definition is completed later when `<f-template>` assigns `definition.template`
60+
- The element definition stays unresolved until a matching declarative template is available
61+
- The resolved template is concrete before platform registration completes
6062

61-
### Phase 2: Template Element Definition
63+
### Phase 2: Declarative Template Bridge
6264

63-
The `f-template` custom element from
64-
`@microsoft/fast-element/declarative.js` is defined and becomes available in
65-
the DOM:
66-
67-
```typescript
68-
import { TemplateElement } from "@microsoft/fast-element/declarative.js";
69-
70-
TemplateElement.define({
71-
name: "f-template",
72-
});
73-
```
65+
`declarativeTemplate()` from `@microsoft/fast-element/declarative.js`
66+
automatically ensures that `f-template` is defined in the same registry as the
67+
FAST element being composed.
7468

7569
### Phase 3: Template Processing and Attachment
7670

7771
When an `f-template` element is connected to the DOM, it initiates the template attachment process.
7872

7973
The lifecycle flow during this phase:
8074

81-
1. **Template Element Connection**: The `f-template` element's `connectedCallback()` is invoked
82-
2. **Async Registration Lookup**: Uses `FASTElementDefinition.register(this.name)` to find the partial element definition
83-
3. **Template Processing**: Processes the HTML template, resolving data bindings, directives, and other template features into the `ViewTemplate` model which is also used by the `@microsoft/fast-element` `html` tag template
84-
4. **Template Attachment**: Attaches the processed template to the partial element definition via `registeredFastElement.template = resolvedTemplate`
75+
1. **Template Discovery**: The resolver waits for a matching
76+
`<f-template name="...">` in the same registry as the element definition.
77+
2. **Template Element Connection**: The matching `f-template` element's
78+
`connectedCallback()` registers it with the declarative template bridge.
79+
3. **Template Processing**: The bridge reads and transforms the markup, builds
80+
the schema, applies `observerMap()` / `attributeMap()` behavior, and resolves
81+
data bindings, directives, and other template features into the `ViewTemplate`
82+
model which is also used by the `@microsoft/fast-element` `html` tag template.
83+
4. **Template Attachment**: The concrete `ViewTemplate` is returned to
84+
`FASTElement.define()`, which assigns it to the definition before platform
85+
registration completes.
8586

86-
### Phase 4: Template Activation
87+
### Phase 4: Composition Completion
8788

88-
Once the template is attached to the registered definition, FAST activates it
89-
for both future and already-connected elements:
89+
Once the template is attached to the partial definition, the element completes its composition:
9090

91-
1. **Definition Update**: `TemplateElement` assigns the parsed `ViewTemplate` to `registeredFastElement.template`
92-
2. **Observable Notification**: Connected elements observing the definition recreate their controller when the `template` property changes
93-
3. **Future Connections**: New element instances use the attached template immediately
91+
1. **`compose()` Execution**: The element definition internally completes its composition process
92+
2. **Platform Registration**: The completed element definition is fully registered with the platform's custom element registry
9493

9594
### Phase 5: Element Instantiation and Hydration
9695

9796
When custom elements are instantiated in the DOM, the following occurs:
9897

9998
1. **Element Creation**: The platform creates instances of the custom element
10099
2. **Prerendered Content Detection**: `ElementController` detects the existing shadow root from SSR and sets `isPrerendered = true`
101-
3. **Late Template Attachment**: If an element connected before its template was attached, the observable `template` change recreates its controller.
102-
4. **Hydration**: Once the template is available, `ElementController` uses `template.hydrate()` to create a `HydrationView` that maps existing DOM nodes to binding targets using `fe:b` / `fe:/b` markers
100+
3. **Concrete Template Ready**: Because `declarativeTemplate()` resolved during
101+
definition, `connect()` starts with the final template already attached.
102+
4. **Hydration**: `ElementController` uses `template.hydrate()` to create a
103+
`HydrationView` that maps existing DOM nodes to binding targets using `fe:b`
104+
/ `fe:/b` markers
103105

104106
The DOM after hydration should look like this:
105107

@@ -126,7 +128,7 @@ The `fastElementRegistry` serves as the central coordination point between the t
126128
Both packages use the Observable pattern for coordination:
127129

128130
- `FASTElementDefinition.register()` uses `Observable.getNotifier()` to notify when elements are registered
129-
- Template attachment triggers observable `template` notifications so connected elements can complete rendering or hydration
131+
- Template attachment triggers observable notifications to complete the lifecycle
130132

131133
## Error Handling
132134

@@ -226,10 +228,6 @@ TemplateElement.config({
226228
console.log('All elements hydrated');
227229
}
228230
});
229-
230-
TemplateElement.define({
231-
name: "f-template",
232-
});
233231
```
234232

235233
### Use Cases

packages/fast-element/MIGRATION.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ available and is applied automatically by
125125
### Changed behavior
126126

127127
- **`attributeChangedCallback` during upgrade**: When `isPrerendered` is true and the element has not yet connected, attribute change callbacks are suppressed. After connection, all attribute changes are processed normally.
128-
- **Late template attachment**: Connected elements no longer rely on `templateOptions` to pause connection. If a definition receives a template later, the observable `template` update recreates the controller so rendering or hydration can continue. No `defer-hydration` attribute is needed.
128+
- **Declarative template resolution**: `declarativeTemplate()` waits for the
129+
matching `<f-template>` before `define()` completes, so connected elements
130+
hydrate with a concrete template. No `defer-hydration` attribute is needed.
129131
- **Binding evaluation with existing shadow root**: When an existing shadow root is detected, `attribute` and `booleanAttribute` bindings skip their initial DOM update. All other binding types (event, content, property, tokenList) run normally.
130132

131133
### New APIs
@@ -200,7 +202,10 @@ This is a **breaking change** for SSR output format. Any system that produces or
200202

201203
### Changed behavior
202204

203-
- **`FASTElement.define()`** now returns `Promise<TType>`. When a template is provided at definition time — or when no template is provided — the Promise resolves immediately. If the definition uses an async template resolver, the Promise resolves after that resolver settles.
205+
- **`FASTElement.define()`** now returns `Promise<TType>`. When a concrete
206+
template is provided at definition time, the Promise resolves immediately.
207+
When `template: declarativeTemplate()` is used, the Promise resolves after
208+
the matching `<f-template>` supplies the concrete template.
204209
- **`FASTElement.compose()`** now returns `Promise<FASTElementDefinition>`. The Promise always resolves immediately.
205210
- **`FASTElementDefinition.compose()`** now returns `Promise<FASTElementDefinition>`. The Promise always resolves immediately.
206211
- **`@customElement` decorator** calls `define()` internally but does not return the Promise (fire-and-forget). For complete definitions with a template, the element is registered via a microtask.
@@ -217,8 +222,9 @@ This is a **breaking change** for SSR output format. Any system that produces or
217222
});
218223

219224
// After
220-
MyElement.define({
225+
await MyElement.define({
221226
name: "my-element",
227+
template: declarativeTemplate(),
222228
});
223229
```
224230

0 commit comments

Comments
 (0)