Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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": "major",
"comment": "Remove TemplateOptions from fast-element definitions and drop templateOptions-based connection/define waiting.",
"packageName": "@microsoft/fast-element",
"email": "7559015+janechu@users.noreply.github.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "add function-based template resolver sequencing",
"packageName": "@microsoft/fast-element",
"email": "7559015+janechu@users.noreply.github.com",
"dependentChangeType": "none"
}
8 changes: 4 additions & 4 deletions packages/fast-element/DECLARATIVE_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,21 +533,21 @@ tagged templates produce.
| `children(prop)` | FAST directive used for `f-children` |
| `ref(prop)` | FAST directive used for `f-ref` |

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

Standard `FASTElement.define()` returns a `Promise` that resolves immediately when a template is provided at definition time. When `templateOptions` is `"defer-and-hydrate"` and no template is provided, the `Promise` resolves after a `<f-template>` supplies one via `register()`. This unified API replaces the previous `defineAsync()` / `composeAsync()` methods.
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.

---

## Hydration Model

When `templateOptions: "defer-and-hydrate"` is used, the server must render:
For declarative hydration, 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.

Connection gating is handled by the template-pending guard in `ElementController.connect()`. When `templateOptions` is `"defer-and-hydrate"` and no template is available yet, `connect()` returns early. An Observable subscription on `"template"` retriggers `connect()` when the template arrives. The `defer-hydration` and `needs-hydration` attributes are no longer needed in server-rendered markup.
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.

### Hydration marker formats

Expand Down
4 changes: 2 additions & 2 deletions packages/fast-element/DECLARATIVE_HTML.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ the package-level hydration overview, 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 with
`templateOptions: "defer-and-hydrate"`.
The host custom element should be defined before the declarative runtime
processes the matching `<f-template>`.

Example:
```html
Expand Down
3 changes: 1 addition & 2 deletions packages/fast-element/DECLARATIVE_RENDERING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,10 @@ export class MyComponent extends FASTElement {

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

When the element connects, `ElementController` automatically detects the existing shadow root from SSR and sets `isPrerendered = true`. The template-pending guard in `ElementController.connect()` ensures the element waits for its template before hydrating. The `defer-hydration` and `needs-hydration` attributes are no longer needed โ€” connection gating is handled internally by the template-pending guard.
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.

## Hydration Comments and Datasets

Expand Down
29 changes: 16 additions & 13 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 `FASTElement.define()`
method is called with `templateOptions: "defer-and-hydrate"` to defer
template attachment.
`FASTElement.define()`. When using `f-template`, the host element can be
defined without an initial template and the declarative runtime attaches the
template later.

## Lifecycle Phases

Expand All @@ -39,23 +39,24 @@ The following phases will then be kicked off once the JavaScript is parsed.

### Phase 1: Partial Element Registration

Custom elements begin their lifecycle by registering as partial definitions with the FAST Element Registry using the `define()` method with `templateOptions: "defer-and-hydrate"`. This allows the element to be registered before its template is available.
Custom elements begin their lifecycle by registering with FAST via the
`define()` method before their declarative template is available.

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

// Register as partial definition - element is registered but incomplete
// Register the host element before the declarative template is attached
MyComponent.define({
name: "my-component",
});
```

Key characteristics of this phase:
- Element is in a "partial" state waiting for template attachment
- `templateOptions` allows for hydration options to be provided. TBD see [this issue](https://github.com/orgs/microsoft/projects/240/views/17?pane=issue&itemId=127653173&issue=microsoft%7Cfast%7C7173).
- Element is registered before template attachment
- The definition is completed later when `<f-template>` assigns `definition.template`

### Phase 2: Template Element Definition

Expand All @@ -82,20 +83,22 @@ The lifecycle flow during this phase:
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`

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

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

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

### 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. **Template-Pending Guard**: If `templateOptions` is `"defer-and-hydrate"` and no template is available yet, `connect()` returns early. An Observable subscription on `"template"` retriggers `connect()` when the template arrives.
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

The DOM after hydration should look like this:
Expand Down Expand Up @@ -123,7 +126,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 notifications to complete the lifecycle
- Template attachment triggers observable `template` notifications so connected elements can complete rendering or hydration

## Error Handling

Expand Down
14 changes: 6 additions & 8 deletions packages/fast-element/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ registry.
- Holds the element's `FASTElementDefinition` (name, template, styles, observed attributes).
- Manages a `Stages` state machine: `disconnected โ†’ connecting โ†’ connected โ†’ disconnecting โ†’ disconnected`.
- Exposes `isPrerendered: Promise<boolean>` 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<boolean>` 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 template into the shadow root, and applies styles. When `templateOptions` is `"defer-and-hydrate"` and no template is available yet, `connect()` returns early; an Observable subscription on `"template"` retriggers `connect()` when the template arrives (template-pending guard).
- 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.
- **Client-side**: `renderClientSide()` clones the compiled fragment, binds, and appends to the host โ€” the standard path with no prerender logic.
Expand All @@ -103,11 +103,11 @@ registry.
- `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`).

`FASTElementDefinition` wraps all the metadata for a custom element class: its tag name, template, styles, and observed attribute list. It is created by `FASTElement.compose()` (which returns `Promise<FASTElementDefinition>`, always resolving immediately) and registered globally via `fastElementRegistry`. `FASTElement.define()` returns `Promise<TType>` โ€” resolving immediately for complete definitions or deferring when `templateOptions` is `"defer-and-hydrate"` and no template is provided. `FASTElementDefinition.register()` returns `Promise<Function>` โ€” resolving when a definition with the given name has been registered.
`FASTElementDefinition` wraps all the metadata for a custom element class: its tag name, template, styles, and observed attribute list. It is created by `FASTElement.compose()` (which returns `Promise<FASTElementDefinition>`, always resolving immediately) and registered globally via `fastElementRegistry`. `PartialFASTElementDefinition.template` may be either a concrete `ElementViewTemplate<InstanceType<TType>>` or a `FASTElementTemplateResolver<TType>` function that receives the composed definition and returns the concrete template (sync or async). `FASTElementDefinition.template` always stores the concrete `ElementViewTemplate<InstanceType<TType>>` after composition or resolver settlement. `FASTElement.define()` returns `Promise<TType>` โ€” resolving immediately for complete definitions or definitions without an initial template, and resolving async template resolver functions only after extensions have had a chance to update the definition. `FASTElementDefinition.register()` returns `Promise<Function>` โ€” resolving when a definition with the given name has been registered.

#### Extensions

`FASTElement.define()` and `FASTElementDefinition.define()` accept an optional array of **extension callbacks** (`FASTElementExtension`). Each extension is a function that receives the resolved `FASTElementDefinition` and is invoked **before** the element is registered with the platform via `customElements.define()`. This allows plugins to inspect or act on the definition metadata (name, template, styles, attributes) before any existing DOM elements are upgraded.
`FASTElement.define()` and `FASTElementDefinition.define()` accept an optional array of **extension callbacks** (`FASTElementExtension`). Each extension is a function that receives the resolved `FASTElementDefinition` and is invoked **before** the element is registered with the platform via `customElements.define()`. `FASTElement.define()` also invokes extensions before any template resolver function is asked to produce a concrete `ElementViewTemplate`, so plugins can attach state that affects template resolution without leaving non-template values on `definition.template`.

```typescript
type FASTElementExtension = (definition: FASTElementDefinition) => void;
Expand Down Expand Up @@ -375,9 +375,7 @@ flowchart TD
CTOR[constructor] --> EC[ElementController.forCustomElement creates or locates the controller]
EC --> ATTACH[Controller captures element + definition, sets $fastController]

CONN[connectedCallback] --> TGUARD{templateOptions = defer-and-hydrate\nAND no template yet?}
TGUARD -->|yes| WAIT[Return early โ€” Observable subscription\non 'template' retriggers connect]
TGUARD -->|no| STAGE[stage = connecting]
CONN[connectedCallback] --> STAGE[stage = connecting]
STAGE --> PRERENDER{Existing shadow root\nfrom SSR/DSD?}
PRERENDER -->|yes| SETFLAG[isPrerendered = true]
PRERENDER -->|no| NORMAL[isPrerendered = false]
Expand Down Expand Up @@ -499,7 +497,7 @@ Below is a conceptual map of the major subsystems and their relationships:
1. Developer writes a class extending `FASTElement`, decorates properties with `@observable` / `@attr`, and calls `FASTElement.define({ name, template, styles })`.
2. `FASTElement.define` โ†’ `FASTElementDefinition.compose(...).define()` registers the element with the Custom Element Registry.
3. When the browser upgrades the element, `ElementController.forCustomElement(element)` is called in the constructor.
4. On `connectedCallback`, the controller renders the template into the shadow root. If the element already has a shadow root from SSR (prerendered content), `renderPrerendered()` uses `template.hydrate()` to map existing DOM nodes to binding targets instead of cloning new DOM. If `templateOptions` is `"defer-and-hydrate"` and no template is available yet, `connect()` returns early and retriggers when the template arrives. Compilation is lazy: the first render call triggers `Compiler.compile()`, subsequent calls clone the already-compiled `DocumentFragment`.
4. On `connectedCallback`, the controller renders the template into the shadow root. If the element already has a shadow root from SSR (prerendered content), `renderPrerendered()` uses `template.hydrate()` to map existing DOM nodes to binding targets instead of cloning new DOM. If no template is available yet, the element connects without rendering until a later `definition.template` update recreates the controller. Compilation is lazy: the first render call triggers `Compiler.compile()`, subsequent calls clone the already-compiled `DocumentFragment`.
5. `HTMLView.bind(source)` wires up each `ViewBehavior`. `oneWay` bindings create `ExpressionNotifier`s that track observable dependencies automatically.
6. When an observed property changes, its notifier fans out to all subscribers. Each binding enqueues a DOM update via `Updates`. The next animation frame drains the queue and applies the mutations.
7. On `disconnectedCallback`, `HTMLView.unbind()` tears down all bindings; behaviors disconnect; styles are removed.
Expand Down Expand Up @@ -550,7 +548,7 @@ src/
โ”œโ”€โ”€ components/
โ”‚ โ”œโ”€โ”€ fast-element.ts # FASTElement, @customElement
โ”‚ โ”œโ”€โ”€ element-controller.ts # ElementController, Stages
โ”‚ โ”œโ”€โ”€ fast-definitions.ts # FASTElementDefinition, TemplateOptions
โ”‚ โ”œโ”€โ”€ fast-definitions.ts # FASTElementDefinition
โ”‚ โ””โ”€โ”€ attributes.ts # AttributeDefinition, @attr, converters
โ”œโ”€โ”€ di/
โ”‚ โ””โ”€โ”€ di.ts # DI container, decorators, resolvers, Registration
Expand Down
Loading
Loading
โšก