Skip to content

feat(parser): propagate template host attrs onto declarative shadow DOM host (FAST plugins)#305

Draft
janechu wants to merge 1 commit into
mainfrom
users/janechu/propagate-template-attrs-onto-declarative-shadow-dom
Draft

feat(parser): propagate template host attrs onto declarative shadow DOM host (FAST plugins)#305
janechu wants to merge 1 commit into
mainfrom
users/janechu/propagate-template-attrs-onto-declarative-shadow-dom

Conversation

@janechu
Copy link
Copy Markdown
Contributor

@janechu janechu commented May 19, 2026

Summary

Mirrors microsoft/fast#7521 in webui. When the FAST v2 or FAST v3 parser plugin is active and the Shadow DOM strategy is in use, static attributes declared on the inner root <template> of a component definition are now propagated onto every host custom-element opening tag during SSR.

Behavior

  • Author wins on conflict. Attributes set at the usage site take precedence over template host attrs. Conflict detection strips leading : or ? from the author name so binding forms (:foo, ?disabled) suppress the static propagation. @ event prefixes do not suppress (they target a different namespace).
  • Client-only directives are skipped: names starting with @, :, or ?, and the literal names f-ref, f-slotted, f-children.
  • Template-internal attrs are skipped: shadowrootmode, shadowrootadoptedstylesheets.
  • Dynamic {{ ... }} host attribute values are out of scope for this pass — only fully static template host attrs propagate.
  • Only the root <template> wrapper of the component definition is consulted; nested templates are ignored.
  • Light DOM rendering is unaffected (no declarative shadow root, so no propagation).
  • WebUI parser plugin keeps the default of false — propagation is FAST-only by design.

Implementation

  • New default-false hook on ParserPlugin:
    fn propagate_template_host_attrs(&self) -> bool { false }
    FAST v2 and FAST v3 plugins override to return true.
  • HtmlParser::template_host_attrs_cache: HashMap<String, Vec<TemplateHostAttr>> parses each component's host attributes exactly once per unique tag name.
  • New helpers (extract_template_host_attrs, propagate_template_host_attrs_to_host, etc.) use tree-sitter with iterative traversal — no recursion, no regex per the performance contract.
  • Propagation is spliced after process_tag_attributes and before the plugin's finish_element hook so FAST binding markers remain after the real host attributes on the rendered opening tag.

Docs

  • DESIGN.md — documented the new trait hook and the propagation contract under the Parser Plugin System section.
  • docs/guide/concepts/plugins/index.md — added the hook to the public ParserPlugin trait reference with an opt-in description.

Tests

  • Per-plugin overrides for FAST v2, FAST v3, WebUI verifying the opt-in value.
  • Core unit tests for extract_template_host_attrs: no template wrapper, static attr extraction, skipping of @/:/?/f-*/shadowroot* names, skipping of dynamic {{…}} values, root-only behavior.
  • End-to-end propagation tests: FAST v2/v3 propagation, WebUI no-propagation, author-wins conflict (incl. ? normalization), @click does not suppress an unrelated static attribute, Light DOM gating, and per-tag caching observed via the cache field.

Quality gate

cargo xtask check passes locally (license-headers → fmt → clippy → deny → test → build → examples → bench validate → docs).


Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Copy link
Copy Markdown
Contributor

@mohamedmansour mohamedmansour left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot support this magic, I don't believe it is the right call. This will cause more maintenance overhead maintaining an allow list of attributes we don't want to propagate. This is like extending all the elements with the additional attributes they add to which doesn't conform with the w3c spec. In WebUI we are closely aligning to w3c spec, and all we are doing is an SSR for html elements with no magic.

Instead we need to build a plugin based solution where it allows the plugin to mutate the rendered element. Before you do that, we need to discuss in an issue regarding this topic.

@janechu
Copy link
Copy Markdown
Contributor Author

janechu commented May 19, 2026

We cannot support this magic, I don't believe it is the right call. This will cause more maintenance overhead maintaining an allow list of attributes we don't want to propagate. This is like extending all the elements with the additional attributes they add to which doesn't conform with the w3c spec. In WebUI we are closely aligning to w3c spec, and all we are doing is an SSR for html elements with no magic.

Instead we need to build a plugin based solution where it allows the plugin to mutate the rendered element. Before you do that, we need to discuss in an issue regarding this topic.

The issue shows up in the use of autofocus which requires a custom element has tabindex="0". If the initial page render does not include tabindex but does include autofocus then it won't autofocus.

I will say we are also scanning the template for shadowroot prefixed attributes and applying them to the template for DSD, I would put these problems in the same bucket.

Right now CSR works with reflecting attributes because it is fast enough, however that's just a quirk of the browser, in theory it should not work.

@janechu
Copy link
Copy Markdown
Contributor Author

janechu commented May 19, 2026

@mohamedmansour actually the reason it might be working in CSR is that we don't define the component until we have evaluated the template and applied attributes.

janechu added a commit that referenced this pull request May 19, 2026
…ST plugins use them)

Refactor of #305 in response to review feedback: the WebUI parser core no
longer contains FAST-specific knowledge. Three generic plugin extension
points now let any framework plugin mutate the rendered custom-element
opening tag and the inner <template> wrapper. The FAST v2 and FAST v3
plugins use these hooks to implement the host-attribute propagation
behavior; the WebUI plugin uses the safe defaults.

New ParserPlugin trait hooks (all default no-op):

  fn on_template_root_attributes(
      &mut self,
      tag_name: &str,
      attributes: &[TemplateRootAttribute],
  ) {}

  fn host_element_attributes(
      &mut self,
      tag_name: &str,
      author_attr_names: &[&str],
  ) -> Option<String> { None }

  fn template_element_attributes(&mut self, tag_name: &str) -> Option<String> {
      None
  }

TemplateRootAttribute carries each attribute's source-preserved name,
optional value (quotes stripped), and verbatim raw text (without a leading
space — the parser inserts a single separator).

Parser-core changes (crates/webui-parser/src/lib.rs):

  * Removed FAST-specific helpers: TEMPLATE_INTERNAL_HOST_ATTRS,
    CLIENT_ONLY_HOST_ATTR_NAMES, is_client_only_host_attr_prefix,
    normalize_author_attr_name_for_conflict,
    collect_author_attr_names_normalized, extract_template_host_attrs,
    template_host_attrs_for, propagate_template_host_attrs_to_host,
    TemplateHostAttr.
  * Added a generic extract_template_root_attributes helper that performs
    structural extraction only (no skip-list policy, no normalization).
  * Added a generic collect_author_attr_names helper returning
    source-preserved attribute names.
  * Added a captured_template_root_attrs HashSet so the parser invokes
    on_template_root_attributes at most once per unique component tag.
  * process_component_directive now captures and notifies BEFORE emitting
    the host opening tag so first-usage host_element_attributes calls see
    populated plugin state (fix for an ordering bug in the previous
    implementation).
  * build_component_template/process_component_template now thread
    tag_name through and splice template_element_attributes(tag_name) into
    the inner <template shadowrootmode="open"> wrapper.

FAST plugin changes (crates/webui-parser/src/plugin/fast_v2.rs,
fast_v3.rs):

  * New shared crates/webui-parser/src/plugin/fast_host_attrs.rs module
    owns all FAST-specific policy: skip lists (@/:/?, f-ref/f-slotted/
    f-children, shadowroot*), {{ ... }} dynamic skip, Shadow-DOM gate,
    author-conflict normalization, and per-tag caching.
  * FastV2ParserPlugin and FastV3ParserPlugin now compose a
    FastHostAttrs instance and expose set_dom_strategy().
  * on_template_root_attributes captures via FastHostAttrs::capture.
  * host_element_attributes delegates to FastHostAttrs::produce_for_host,
    which returns None outside Shadow DOM and suppresses any propagated
    attribute whose name conflicts with an author attribute at the usage
    site.

WebUI plugin change: imports updated; new test verifies the default
no-injection behavior of the generic hooks.

webui crate (crates/webui/src/lib.rs): build_protocol_inner now calls
set_dom_strategy(options.dom) on FAST v2 and FAST v3 plugin instances so
the propagation gate is wired through the public build pipeline.

Tests:

  * fast_host_attrs.rs: 10 unit tests covering the capture/produce flow,
    skip list, conflict normalization, DOM strategy gate, and dynamic
    {{ ... }} suppression.
  * fast_v2.rs, fast_v3.rs: replace the prior propagate_template_host_attrs
    bool test with 4 new tests per plugin exercising the new hooks
    directly (capture+inject, light-DOM skip, author conflict suppression,
    FAST client-only filtering).
  * webui.rs: replaces the prior opt-out test with one verifying both new
    hooks default to None.
  * lib.rs end-to-end: extract_template_host_attrs_* removed in favor of
    extract_template_root_attributes_* tests targeting the generic
    structural extractor; the existing FAST E2E propagation tests are
    re-wired through the new hooks and continue to cover Shadow-DOM
    propagation, WebUI no-propagation, author conflict (including ?
    boolean-binding normalization), @event-handler non-suppression,
    Light-DOM gating, and per-tag capture-once semantics (now observed
    via captured_template_root_attrs instead of the removed cache field).

Docs:
  * DESIGN.md: replaced the trait sketch with the three new hooks and
    updated the hook invocation table.
  * docs/guide/concepts/plugins/index.md: replaced the
    propagate_template_host_attrs reference with on_template_root_attributes,
    host_element_attributes, and template_element_attributes.

cargo xtask check passes locally (license-headers, fmt, clippy, deny,
test, build, examples, bench validate, docs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu force-pushed the users/janechu/propagate-template-attrs-onto-declarative-shadow-dom branch from f44c3ba to 5b4b335 Compare May 19, 2026 19:27
@janechu
Copy link
Copy Markdown
Contributor Author

janechu commented May 19, 2026

@mohamedmansour Thanks for the review — agreed completely on the 'magic in the parser core' concern. I've refactored to remove all FAST-specific knowledge from the WebUI parser. The parser now exposes three generic plugin extension points that any framework plugin can use to mutate the rendered custom-element opening tag and the inner <template> wrapper:

fn on_template_root_attributes(
    &mut self,
    tag_name: &str,
    attributes: &[TemplateRootAttribute],
) {}

fn host_element_attributes(
    &mut self,
    tag_name: &str,
    author_attr_names: &[&str],
) -> Option<String> { None }

fn template_element_attributes(&mut self, tag_name: &str) -> Option<String> { None }

What moved out of the parser core:

  • Skip lists (@/:/? prefixes, f-ref/f-slotted/f-children, shadowroot*)
  • Conflict normalization (:/? prefix stripping, @ preservation)
  • The {{ ... }} dynamic-value skip
  • The Shadow DOM gate

All of that now lives in a shared crates/webui-parser/src/plugin/fast_host_attrs.rs module used only by fast_v2 and fast_v3 plugins. The parser core only knows how to: capture structural attributes off a <template> root, hand them to the plugin once per tag, and splice plugin-supplied text into the host opening tag and the <template shadowrootmode="open"> wrapper with a single separator space.

Also fixed an ordering bug in the previous commit: on_template_root_attributes now fires before the host opening tag is emitted (previously, on a component's first usage, the propagation hook would have been called too late).

cargo xtask check passes locally (license-headers, fmt, clippy, deny, test, build, examples, bench validate, docs).

…ST plugins use them)

Refactor of #305 in response to review feedback: the WebUI parser core no
longer contains FAST-specific knowledge. Three generic plugin extension
points now let any framework plugin mutate the rendered custom-element
opening tag and the inner <template> wrapper. The FAST v2 and FAST v3
plugins use these hooks to implement the host-attribute propagation
behavior; the WebUI plugin uses the safe defaults.

New ParserPlugin trait hooks (all default no-op):

  fn on_template_root_attributes(
      &mut self,
      tag_name: &str,
      attributes: &[TemplateRootAttribute],
  ) {}

  fn host_element_attributes(
      &mut self,
      tag_name: &str,
      author_attr_names: &[&str],
  ) -> Option<String> { None }

  fn template_element_attributes(&mut self, tag_name: &str) -> Option<String> {
      None
  }

TemplateRootAttribute carries each attribute's source-preserved name,
optional value (quotes stripped), and verbatim raw text (without a leading
space — the parser inserts a single separator).

Parser-core changes (crates/webui-parser/src/lib.rs):

  * Removed FAST-specific helpers: TEMPLATE_INTERNAL_HOST_ATTRS,
    CLIENT_ONLY_HOST_ATTR_NAMES, is_client_only_host_attr_prefix,
    normalize_author_attr_name_for_conflict,
    collect_author_attr_names_normalized, extract_template_host_attrs,
    template_host_attrs_for, propagate_template_host_attrs_to_host,
    TemplateHostAttr.
  * Added a generic extract_template_root_attributes helper that performs
    structural extraction only (no skip-list policy, no normalization).
  * Added a generic collect_author_attr_names helper returning
    source-preserved attribute names.
  * Added a captured_template_root_attrs HashSet so the parser invokes
    on_template_root_attributes at most once per unique component tag.
  * process_component_directive now captures and notifies BEFORE emitting
    the host opening tag so first-usage host_element_attributes calls see
    populated plugin state (fix for an ordering bug in the previous
    implementation).
  * build_component_template/process_component_template now thread
    tag_name through and splice template_element_attributes(tag_name) into
    the inner <template shadowrootmode="open"> wrapper.

FAST plugin changes (crates/webui-parser/src/plugin/fast_v2.rs,
fast_v3.rs):

  * New shared crates/webui-parser/src/plugin/fast_host_attrs.rs module
    owns all FAST-specific policy: skip lists (@/:/?, f-ref/f-slotted/
    f-children, shadowroot*), {{ ... }} dynamic skip, Shadow-DOM gate,
    author-conflict normalization, and per-tag caching.
  * FastV2ParserPlugin and FastV3ParserPlugin now compose a
    FastHostAttrs instance and expose set_dom_strategy().
  * on_template_root_attributes captures via FastHostAttrs::capture.
  * host_element_attributes delegates to FastHostAttrs::produce_for_host,
    which returns None outside Shadow DOM and suppresses any propagated
    attribute whose name conflicts with an author attribute at the usage
    site.

WebUI plugin change: imports updated; new test verifies the default
no-injection behavior of the generic hooks.

webui crate (crates/webui/src/lib.rs): build_protocol_inner now calls
set_dom_strategy(options.dom) on FAST v2 and FAST v3 plugin instances so
the propagation gate is wired through the public build pipeline.

Tests:

  * fast_host_attrs.rs: 10 unit tests covering the capture/produce flow,
    skip list, conflict normalization, DOM strategy gate, and dynamic
    {{ ... }} suppression.
  * fast_v2.rs, fast_v3.rs: replace the prior propagate_template_host_attrs
    bool test with 4 new tests per plugin exercising the new hooks
    directly (capture+inject, light-DOM skip, author conflict suppression,
    FAST client-only filtering).
  * webui.rs: replaces the prior opt-out test with one verifying both new
    hooks default to None.
  * lib.rs end-to-end: extract_template_host_attrs_* removed in favor of
    extract_template_root_attributes_* tests targeting the generic
    structural extractor; the existing FAST E2E propagation tests are
    re-wired through the new hooks and continue to cover Shadow-DOM
    propagation, WebUI no-propagation, author conflict (including ?
    boolean-binding normalization), @event-handler non-suppression,
    Light-DOM gating, and per-tag capture-once semantics (now observed
    via captured_template_root_attrs instead of the removed cache field).

Docs:
  * DESIGN.md: replaced the trait sketch with the three new hooks and
    updated the hook invocation table.
  * docs/guide/concepts/plugins/index.md: replaced the
    propagate_template_host_attrs reference with on_template_root_attributes,
    host_element_attributes, and template_element_attributes.

cargo xtask check passes locally (license-headers, fmt, clippy, deny,
test, build, examples, bench validate, docs).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@janechu janechu force-pushed the users/janechu/propagate-template-attrs-onto-declarative-shadow-dom branch from 5b4b335 to 2c4f87c Compare May 19, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants