Skip to content
Open
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": "Render declarative host attributes from f-host and HTML-standard shadowroot attributes from f-shadow, retaining legacy shadowroot configuration temporarily.",
"packageName": "@microsoft/fast-build",
"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 f-host declarations for host attributes and bindings and f-shadow declarations using HTML-standard shadowroot attributes, retaining legacy shadowroot configuration temporarily.",
"packageName": "@microsoft/fast-element",
"email": "7559015+janechu@users.noreply.github.com",
"dependentChangeType": "none"
}
57 changes: 47 additions & 10 deletions crates/microsoft-fast-build/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ render_template(template, state_str)
| `expression.rs` | Boolean expression evaluator for `<f-when value="{{…}}">` |
| `hydration.rs` | `HydrationScope` β€” binding index tracking and v3 data-free or opt-in v2 indexed marker generation per template scope |
| `json.rs` | Hand-rolled JSON parser producing `JsonValue` |
| `locator.rs` | `Locator` struct β€” maps element names to template strings; glob scanner; `<f-template>` parser. Stored template bodies are first run through `escape_code_sample_elements` so `{`/`}` characters and the angle brackets of FAST directive tags (`<f-when>`, `<f-repeat>`) inside `<code>` elements are entity-escaped and therefore not interpreted as binding delimiters or directives. Also captures the inner `<template>` element's attributes as **host attributes** for propagation onto the rendered host element opening tag. |
| `locator.rs` | `Locator` struct β€” maps element names to template strings; glob scanner; fallible `<f-template>` parser. Stored bodies use `escape_code_sample_elements`. A single optional direct `<f-host>` supplies host attributes; an optional single direct `<f-shadow>` supplies static shadow-root configuration; the inner `<template>` supplies content only. |
| `code_escape.rs` | `escape_code_sample_elements` β€” auto-escape preprocessor used by both `renderer.rs` and `locator.rs`. Walks the HTML and, inside every `<code>` element (including nested ones and attribute values of descendants), replaces `{` β†’ `&#123;` and `}` β†’ `&#125;` so that binding-like syntax in code samples renders literally. Additionally rewrites the `<` / `>` of every FAST directive tag (`<f-when>`, `</f-when>`, `<f-repeat>`, `</f-repeat>`) it finds inside `<code>` as `&lt;` / `&gt;`, so authors can write directives literally without manual entity escaping; tag-name matching is case-insensitive. Real HTML elements (`<button>`) and custom elements (`<my-widget>`) inside `<code>` keep their angle brackets and continue to render as live DOM elements. The brace half of the escape mirrors the JavaScript-side `escapeBracesInCodeElements` in `@microsoft/fast-html`; the directive-tag angle escape is server-only because the DOM serializer re-encodes `<`/`>` in text content so the client never sees a raw directive tag inside `<code>`. Modeled on Microsoft WebUI's `webui-press` markdown renderer, which auto-escapes the same characters inside code spans and code fences |
| `error.rs` | `RenderError` enum with `Display` impl and helpers |
| `f_template.rs` | `compose_f_template_styles` β€” in-memory insertion of a `<style>` element into an `<f-template>` definition's inner `<template>`, with no filesystem requirement |
Expand Down Expand Up @@ -136,7 +136,7 @@ DOM as soon as the element is parsed.
Streaming custom-element rendering uses the same directive helpers as the
non-streamed path: child state is built from root state plus author attributes,
the host opening tag is produced by `build_element_open_tag`, and inner
`<template>` host attributes are merged with `merge_template_host_attrs`.
`<f-host>` attributes are merged with `merge_template_host_attrs`.
Author attributes still win, client-only host attributes are skipped, and
template-host bindings resolve against the child state.

Expand Down Expand Up @@ -403,7 +403,7 @@ A custom element is any opening tag whose name contains a hyphen, excluding `f-w
- **Client-only attrs** (`@event`, `:prop`, attribute directives) β€” stripped as usual.
- No `data-fe` marker is added β€” root elements at entry level have no parent hydration scope.
- **Nested custom elements** (`is_entry: false`): `strip_client_only_attrs` removes client-only attrs after binding resolution. If the element carries `{{expr}}` or `{expr}` attribute bindings and is inside a parent hydration scope, those bindings are counted and `data-fe="N"` is injected.
8. **Template host attribute propagation.** Attributes declared on the inner `<template>` element of the source `<f-template>` are merged onto the rendered host opening tag built in step 7. `merge_template_host_attrs` (in `directive.rs`) splices the surviving attributes in just before the closing `>`, resolving any bindings against `child_root` β€” the same child state used to render the shadow template (the root state with the host element's HTML attributes overlaid).
8. **Template host attribute propagation.** Attributes declared on the direct `<f-host>` child of the source `<f-template>` are merged onto the rendered host opening tag built in step 7. `merge_template_host_attrs` (in `directive.rs`) splices the surviving attributes in just before the closing `>`, resolving any bindings against `child_root` β€” the same child state used to render the shadow template (the root state with the host element's HTML attributes overlaid).
- **Client-only attrs are skipped**: any template host attr whose name starts with `@` (event) or `:` (property binding), or whose lowercased name is `f-ref`, `f-slotted`, or `f-children`, is dropped β€” none of these have any meaning on a server-rendered host element.
- **Author attributes win.** Attributes already present on the host element opening tag (the author markup) are preserved verbatim. Template host attrs whose lowercased name matches an existing author attribute are skipped. For `?name="{{expr}}"` template attrs, the dedupe key is the bare `name` (without the leading `?`).
- **Static `name="value"`** β†’ emitted verbatim with the value HTML-escaped. **Static boolean `name`** (no `=`) β†’ emitted bare.
Expand All @@ -415,7 +415,8 @@ A custom element is any opening tag whose name contains a hyphen, excluding `f-w
*Worked example.* Given the template
```html
<f-template name="my-el">
<template tabindex="0" ?disabled="{{isDisabled}}">…</template>
<f-host tabindex="0" ?disabled="{{isDisabled}}"></f-host>
<template>…</template>
</f-template>
```
the author markup `<my-el disabled></my-el>` and state `{"isDisabled": false}` render as
Expand All @@ -431,7 +432,12 @@ A custom element is any opening tag whose name contains a hyphen, excluding `f-w
[light DOM children]
</my-button>
```
The `<template>` receives any `shadowroot*` attributes declared on the source `<f-template>`. The renderer normalizes `shadowrootmode` and legacy `shadowroot` for compatibility: when neither has a non-empty value, it emits `shadowrootmode="open" shadowroot="open"`; when exactly one has a non-empty value, that value is mirrored to the other; when both have explicit non-empty values, both are preserved as authored, even if they conflict.
The native DSD `<template>` receives normalized configuration from the
optional single direct `<f-shadow>`, using the same HTML-standard attribute
names: `shadowrootmode`, `shadowrootdelegatesfocus`, `shadowrootclonable`,
and `shadowrootserializable`.
The metadata declaration itself is not rendered or registered as a custom
element.

When a nested element has attribute bindings (`{{expr}}` or `{expr}` values) and is being rendered inside another element's shadow (i.e., `parent_hydration` is `Some`), those bindings are counted, `data-fe="N"` is added to the element's opening tag, and the binding indices are allocated from the parent scope.

Expand Down Expand Up @@ -639,11 +645,36 @@ For each glob pattern:
- Verifies the character after `<f-template` is ASCII whitespace, `/`, or `>` to avoid matching non-`f-template` tags such as `<f-template-x>`.
- Extracts the attribute string between `<f-template` and `>`, allowing ASCII whitespace before the closing `>`.
- Uses `parse_element_attributes` to get the `name` attribute value (supports both `"` and `'` quoting).
- Collects all unique `shadowroot`-prefixed attributes from the `<f-template>` opening tag, preserving boolean attributes as `None` and lowercasing attribute names.
- Reads an optional single direct `<f-shadow>` as static configuration, with an
explicit closing tag. `shadowrootmode` defaults to `open` and accepts `open`/`closed`;
`shadowrootdelegatesfocus`, `shadowrootclonable`, and `shadowrootserializable`
are presence-based flags.
Attribute names are case-insensitive and bindings are unsupported.
When present, it replaces the complete legacy shadow configuration rather
than merging. Normalized SSR names populate `shadowrootAttributes`, with
boolean attributes represented as `None`.
- Extracts the inner HTML between `>` and a matching `</f-template>` end tag, allowing ASCII whitespace before the closing `>`.
- Calls `extract_template_content` on the inner HTML to get the content **and the attributes** declared on the inner `<template>` element. The returned attribute list is preserved as the template's **host attributes**, available later via `Locator::get_host_attributes` and merged onto the rendered host opening tag by `merge_template_host_attrs` (see the **Custom elements** section above).

Returns a `Vec<FTemplate>` containing the optional name, template content, the collected shadowroot attributes, and the inner `<template>` element's host attributes. The locator stores the shadowroot attributes with the template definition so custom-element rendering can apply them to the emitted Declarative Shadow DOM `<template>`, and stores the host attributes so they can be merged onto the rendered host element opening tag.
- Scans direct children, matching nested template closing tags and skipping comments/raw-text bodies. At most one direct `<f-host>` supplies host attributes. Only the inner `<template>` body supplies rendered content; host children are discarded. Definition attributes and other inner-template attributes are never forwarded as host attributes.

Returns `Result<Vec<FTemplate>, RenderError>` with optional names, content,
shadow-root attributes, and unchanged host-attribute metadata. Duplicate direct
hosts and malformed/unclosed declarations return located
`InvalidTemplateMetadata` errors; file loading propagates them and WASM converts
them to structured errors. No custom-element registration is added.

Binding collection visits the host opening tag and content in authored order,
regardless of which child comes first, so the forward UTF-16 location cursor
continues to preserve original content spans. Inner-template and publisher
attributes no longer contribute host bindings. The version-1 metadata shape and
explicit programmatic host-attribute APIs are unchanged.

Definition discovery remains permissive about missing or multiple inner
templates so browser-diagnostic fixtures can still be built: the first template
is used when present, otherwise the body is retained as legacy fallback content.
An optional direct host and its children are removed from fallback content, with
bindings collected from original source windows in authored order. Only
host-specific structural validation is added; strict inner-template cardinality
checks remain owned by the client, composition, conversion, and slot analysis.

### Glob matching

Expand Down Expand Up @@ -747,7 +778,13 @@ Calls `locator::parse_f_templates` (the same function used by `Locator::from_pat
]
```

`name` is `null` when the `<f-template>` element has no `name` attribute. The `shadowrootAttributes` array preserves forwarded `shadowroot*` attributes as `{name, value}` metadata, using `null` for boolean attributes. The `@microsoft/fast-build` CLI uses this export to parse HTML files without reimplementing the parsing logic in JavaScript.
`name` is `null` when the `<f-template>` element has no `name` attribute. The
`shadowrootAttributes` array carries normalized `<f-shadow>` options as native
DSD `{name, value}` metadata, using `null` for boolean attributes. The
`@microsoft/fast-build` CLI uses this export to parse HTML files without
reimplementing the parsing logic in JavaScript. The browser runtime consumes
the same authored configuration into `definition.shadowOptions` before
registration.

### `render_with_templates`

Expand Down
56 changes: 49 additions & 7 deletions crates/microsoft-fast-build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ assert_eq!(chunks.join(""), html);
Custom element streaming uses the `*_stream_with_locator` and
`render_entry_*_stream_with_locator` APIs. For entry HTML, a custom element's
opening tag chunk includes the complete Declarative Shadow DOM template; light
DOM content follows in later chunks. Template host attributes declared on the
inner `<template>` element are merged onto the streamed host opening tag with
DOM content follows in later chunks. Host attributes declared on the direct
`<f-host>` child of `<f-template>` are merged onto the streamed host opening tag with
the same author-wins and client-only-attribute filtering rules as normal
rendering.

Expand All @@ -104,14 +104,14 @@ use microsoft_fast_build::{

let definitions = parse_f_templates(
r#"<f-template name="my-card"><template>{{title}}</template></f-template>"#,
);
)?;
let locator = Locator::from_metadata(definitions)?;
let fragment = render_shadow_root_template_with_locator(
"my-card", Some(r#"{"title":"Hello"}"#), &locator, None,
)?;
```

`parse_f_templates` is also a public native API. It returns `Vec<FTemplate>`
`parse_f_templates` is also a public native API. It returns `Result<Vec<FTemplate>, RenderError>`
with `format_version`, `bindings`, name/content, and host/shadow-root attributes.
`BindingMetadata` records direct names, expressions, syntax kinds, lexical
paths, and nested `BindingScope { variable, iterable }` frames. Application
Expand Down Expand Up @@ -451,7 +451,9 @@ Glob patterns support `*` (any characters within one path segment), `**` (any nu
Template HTML files must wrap their content in an `<f-template>` element with a `name` attribute identifying the custom element, and an inner `<template>` element containing the template markup:

```html
<f-template name="my-button">
<f-template hidden name="my-button">
<f-host role="button" tabindex="0" appearance="{{appearance}}"></f-host>
<f-shadow shadowrootmode="open" shadowrootdelegatesfocus></f-shadow>
<template>
<button>{{label}}</button>
</template>
Expand All @@ -475,7 +477,45 @@ A single file may contain multiple templates:

If an `<f-template>` element is missing a `name` attribute, a warning is emitted to stderr and the template is ignored. The parser follows browser tag boundaries for the `<f-template>` and inner `<template>` wrappers, including ASCII whitespace before `>` in opening and closing tags.

Any `shadowroot*` attributes on `<f-template>` are copied to the rendered Declarative Shadow DOM `<template>`. The renderer normalizes `shadowrootmode` and legacy `shadowroot` for compatibility: when neither has a non-empty value, it emits `shadowrootmode="open" shadowroot="open"`; when exactly one has a non-empty value, that value is mirrored to the other; when both have explicit non-empty values, both are preserved as authored, even if they conflict.
An optional single direct-child `<f-host>` declares attributes for the rendered
custom host. It is metadata, not a registered custom element or shadow content;
its children are discarded. Static and reactive attributes use the existing
host rendering semantics. Authored instance attributes win, including bindings
that resolve to absence. Initial host attributes precede DSD in normal and
stream output. The native `host_attributes` and WASM `hostAttributes` APIs remain
unchanged.

**Migration:** Move host attributes from `<f-template>` or the inner `<template>`
to `<f-host>`. Definition-wrapper attributes are metadata only; the inner
`<template>` supplies content only, not host attributes or bindings.
Put static shadow-root configuration on an optional single direct `<f-shadow>`.

`<f-shadow shadowrootmode="open" shadowrootdelegatesfocus shadowrootclonable shadowrootserializable></f-shadow>` accepts
`open`/`closed` mode, defaulting to `open`. Boolean flags use presence and
attribute names are case-insensitive. Only static configuration is supported;
bindings are not allowed. An explicit closing tag is required. When present,
the declaration replaces the complete legacy shadow configuration rather
than merging. It is not rendered into the component or registered as a custom
element.

SSR uses the same lowercase HTML-standard attribute names on native DSD output
templates without renaming them. The browser runtime updates
`definition.shadowOptions` before registration. `hidden` on document-embedded
`<f-template>` keeps metadata out of layout and tab order before JavaScript
loads; it is definition-only and never forwarded to the component host.

`parse_f_templates` now returns a `Result`; update native calls to use `?`.
Duplicate direct hosts, self-closing hosts, malformed opening tags, and unclosed
declarations fail with located `invalid-template-metadata` errors instead of
silently dropping definitions. WASM throws the existing structured JavaScript
`Error`. Host binding spans use original-document coordinates and remain in
authored order, even when `<f-host>` follows the inner template.

Discovery remains permissive about missing or multiple inner templates, so
definitions authored for browser diagnostics can still be built. It selects the
first inner template when present and otherwise retains legacy body content,
excluding the direct host declaration and its children. Client diagnostics and
the stricter composition/conversion/slot-analysis APIs are unchanged.

### Composing CSS into an `<f-template>` β€” `compose_f_template_styles`

Expand Down Expand Up @@ -707,7 +747,9 @@ The renderer wraps the rendered template in Declarative Shadow DOM and adds the

- `shadowrootmode="open"` β€” the standard declarative shadow DOM mode emitted by default.
- `shadowroot="open"` β€” legacy declarative shadow DOM attribute for broader browser compatibility.
- `shadowroot*` attributes declared on the source `<f-template>` are forwarded to the output `<template>`; `shadowrootmode` and `shadowroot` default to `open` when neither has a non-empty value, mirror when exactly one has a non-empty value, and are preserved as authored when both have explicit non-empty values.
- Static `<f-shadow>` configuration supplies the output `shadowrootmode`,
`shadowrootdelegatesfocus`, `shadowrootclonable`, and
`shadowrootserializable` attributes.

Custom elements that have no matching template in the locator are passed through verbatim.

Expand Down
9 changes: 7 additions & 2 deletions crates/microsoft-fast-build/src/binding_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,13 @@ pub(crate) fn collect_bindings(
locations,
bindings: Vec::new(),
};
collector.collect_attributes(host_attrs, host_offset, true, &[]);
collector.scan(&mask_code_sample_elements(content), content_offset, &[]);
if host_offset <= content_offset {
collector.collect_attributes(host_attrs, host_offset, true, &[]);
collector.scan(&mask_code_sample_elements(content), content_offset, &[]);
} else {
collector.scan(&mask_code_sample_elements(content), content_offset, &[]);
collector.collect_attributes(host_attrs, host_offset, true, &[]);
}
collector.bindings
}

Expand Down
Loading
Loading