Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "add declarativeTemplate for auto-resolving <f-template> markup",
"packageName": "@microsoft/fast-element",
"email": "7559015+janechu@users.noreply.github.com",
"dependentChangeType": "none"
}
26 changes: 12 additions & 14 deletions packages/fast-element/DECLARATIVE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ The `ObserverMapConfig` interface accepts an optional `properties` key that maps
MyElement.define(
{
name: "my-element",
templateOptions: "defer-and-hydrate",
template: declarativeTemplate(),
},
[
observerMap({
Expand Down Expand Up @@ -482,27 +482,25 @@ sequenceDiagram
participant EC as ElementController
participant Callbacks as HydrationLifecycleCallbacks

App->>FER: MyElement.define({name:'my-el', ...})
note over FER: partial definition stored
App->>FER: await MyElement.define({name:'my-el', template: declarativeTemplate()})
note over FER: definition composed; resolver waits for template

App->>TE: TemplateElement.define({name:'f-template'})
App->>TE: TemplateElement.config(callbacks)
App->>TE: TemplateElement.options({'my-el':{observerMap:{}}})

DOM->>TE: f-template connected to DOM
TE->>FER: FASTElementDefinition.register('my-el')
FER-->>TE: resolves with MyElement class
TE->>FER: bridge matches registry + name
TE->>Callbacks: elementDidRegister('my-el')
TE->>Callbacks: templateWillUpdate('my-el')
TE->>TE: parse template → ViewTemplate
TE->>FER: registeredFastElement.template = viewTemplate
TE->>TE: parse template → schema → maps → ViewTemplate
TE->>FER: return viewTemplate to resolver
TE->>Callbacks: templateDidUpdate('my-el')
FER->>FER: compose() → platform registry
FER->>FER: customElements.define('my-el', MyElement)
FER->>Callbacks: elementDidDefine('my-el')

DOM->>EC: element instance connects with existing shadow root
EC->>EC: isPrerendered = true (existing shadow root detected)
EC->>EC: template-pending guard: wait if no template yet
EC->>EC: concrete template already attached
EC->>Callbacks: hydrationStarted()
EC->>Callbacks: elementWillHydrate(element)
EC->>EC: template.hydrate() — maps existing DOM to binding targets
Expand Down Expand Up @@ -550,21 +548,21 @@ tagged templates produce.
| `children(prop)` | FAST directive used for `f-children` |
| `ref(prop)` | FAST directive used for `f-ref` |

### Template attachment after define
### Deferred template attachment via define

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.
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.

---

## Hydration Model

For declarative hydration, the server must render:
When declarative templates are used, the server must render:

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

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.
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.

### Hydration marker formats

Expand Down
18 changes: 7 additions & 11 deletions packages/fast-element/DECLARATIVE_HTML.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ server-side rendering, see the
After registering the declarative entrypoint as shown in the README, templates
are associated with an element through
`<f-template name="[custom-element-name]"><template>...</template></f-template>`.
The host custom element should be defined before the declarative runtime
processes the matching `<f-template>`.
The host custom element should be defined with
`template: declarativeTemplate()`. This automatically defines `<f-template>` in
the relevant registry and waits for the matching declarative template when it is
already present or inserted later.

Example:
```html
Expand Down Expand Up @@ -211,7 +213,7 @@ For finer control, pass a configuration object with a `properties` key that maps
UserProfile.define(
{
name: "user-profile",
templateOptions: "defer-and-hydrate",
template: declarativeTemplate(),
},
[
observerMap({
Expand All @@ -228,8 +230,6 @@ UserProfile.define(
}),
],
);

TemplateElement.define({ name: "f-template" });
```

Each path entry can be:
Expand Down Expand Up @@ -274,12 +274,10 @@ Properties already decorated with `@attr` or `@observable` on the class are left
MyElement.define(
{
name: "my-element",
templateOptions: "defer-and-hydrate",
template: declarativeTemplate(),
},
[attributeMap()],
);

TemplateElement.define({ name: "f-template" });
```

With the template:
Expand Down Expand Up @@ -308,16 +306,14 @@ The `attribute-name-strategy` configuration option controls how template binding
MyElement.define(
{
name: "my-element",
templateOptions: "defer-and-hydrate",
template: declarativeTemplate(),
},
[
attributeMap({
"attribute-name-strategy": "camelCase",
}),
],
);

TemplateElement.define({ name: "f-template" });
```

With the template:
Expand Down
9 changes: 6 additions & 3 deletions packages/fast-element/DECLARATIVE_MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ See the [`@microsoft/fast-element` MIGRATION.md](../fast-element/MIGRATION.md#hy

### Migration steps

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

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

// After
MyComponent.define({
await MyComponent.define({
name: "my-component",
templateOptions: "defer-and-hydrate",
template: declarativeTemplate(),
});
```

Expand Down
8 changes: 7 additions & 1 deletion packages/fast-element/DECLARATIVE_RENDERING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,16 @@ export class MyComponent extends FASTElement {

MyComponent.define({
name: "my-component",
template: declarativeTemplate(),
});
```

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.
When the element connects, `ElementController` automatically detects the
existing shadow root from SSR and sets `isPrerendered = true`.
`declarativeTemplate()` keeps the definition template concrete before
registration completes, so the element can hydrate the prerendered shadow root
immediately. The `defer-hydration` and `needs-hydration` attributes are no
longer needed.

## Hydration Comments and Datasets

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

## Lifecycle Phases

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

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

### Phase 1: Partial Element Registration
### Phase 1: Definition Resolution

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

```typescript
// Custom element class definition
class MyComponent extends FASTElement {
@attr text: string = "";
}

// Register the host element before the declarative template is attached
MyComponent.define({
// Register with the declarative template bridge
await MyComponent.define({
name: "my-component",
template: declarativeTemplate(),
});
```

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

### Phase 2: Template Element Definition
### Phase 2: Declarative Template Bridge

The `f-template` custom element from
`@microsoft/fast-element/declarative.js` is defined and becomes available in
the DOM:

```typescript
import { TemplateElement } from "@microsoft/fast-element/declarative.js";

TemplateElement.define({
name: "f-template",
});
```
`declarativeTemplate()` from `@microsoft/fast-element/declarative.js`
automatically ensures that `f-template` is defined in the same registry as the
FAST element being composed.

### Phase 3: Template Processing and Attachment

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

The lifecycle flow during this phase:

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

### Phase 4: Template Activation
### Phase 4: Composition Completion

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

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

### Phase 5: Element Instantiation and Hydration

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. **Late Template Attachment**: If an element connected before its template was attached, the observable `template` change recreates its controller.
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
3. **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
`HydrationView` that maps existing DOM nodes to binding targets using `fe:b`
/ `fe:/b` markers

The DOM after hydration should look like this:

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

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

## Error Handling

Expand Down Expand Up @@ -226,10 +228,6 @@ TemplateElement.config({
console.log('All elements hydrated');
}
});

TemplateElement.define({
name: "f-template",
});
```

### Use Cases
Expand Down
12 changes: 9 additions & 3 deletions packages/fast-element/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ available and is applied automatically by
### Changed behavior

- **`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.
- **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.
- **Declarative template resolution**: `declarativeTemplate()` waits for the
matching `<f-template>` before `define()` completes, so connected elements
hydrate with a concrete template. No `defer-hydration` attribute is needed.
- **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.

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

### Changed behavior

- **`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.
- **`FASTElement.define()`** now returns `Promise<TType>`. When a concrete
template is provided at definition time, the Promise resolves immediately.
When `template: declarativeTemplate()` is used, the Promise resolves after
the matching `<f-template>` supplies the concrete template.
- **`FASTElement.compose()`** now returns `Promise<FASTElementDefinition>`. The Promise always resolves immediately.
- **`FASTElementDefinition.compose()`** now returns `Promise<FASTElementDefinition>`. The Promise always resolves immediately.
- **`@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.
Expand All @@ -217,8 +222,9 @@ This is a **breaking change** for SSR output format. Any system that produces or
});

// After
MyElement.define({
await MyElement.define({
name: "my-element",
template: declarativeTemplate(),
});
```

Expand Down
Loading
Loading