Skip to content

fix: DSD hydration fails on literal curly-brace binding text in light DOM #7520

@janechu

Description

@janechu

🐛 Bug Report

DSD hydration fails when light DOM text of a component contains text that resembles template binding syntax (double curly braces wrapping a property name), even when that text is not intended as a binding.

When using Declarative Shadow DOM (DSD) with RenderableFASTElement and defineAsync, FAST's hydration fails if a component's light DOM contains double-curly-brace interpolation patterns as display text. This is a blocker for any SSR/DSD application that shows code examples containing template syntax.

💻 Repro or Code Sample

The reproduction consists of two components:

  1. code-sample - A BTR component with a slot that displays code examples
  2. my-app - A parent BTR component whose template contains a code-sample with light DOM text that includes binding syntax

code-sample.html (template):

<div class="code-block">
  <button type="button" @click="{toggleCode()}">
    { {toggleLabel} }
  </button>
  <div class="code-container">
    <pre><slot></slot></pre>
  </div>
</div>

Note: In actual code, there is no space between the curly braces. Spaces added here to prevent GitHub from interpreting them.

code-sample.ts:

import { FASTElement, observable } from '@microsoft/fast-element';
import { RenderableFASTElement } from '@microsoft/fast-html';

class CodeSample extends RenderableFASTElement(FASTElement) {
  @observable codeOpen = false;
  @observable toggleLabel = 'Show Code';

  toggleCode(): void {
    this.codeOpen = !this.codeOpen;
    this.toggleLabel = this.codeOpen ? 'Hide Code' : 'Show Code';
  }
}

CodeSample.defineAsync({
  name: 'code-sample',
  templateOptions: 'defer-and-hydrate',
});

my-app.html (parent template - this triggers the bug):

<h2>Click Handler Example</h2>
<div class="preview">
  <button @click="{onClick()}">Click Me</button>
  <span>{ {clickMessage} }</span>
</div>

<code-sample>
  <code>
    button @click="{onClick()}" Click Me /button
    span { {clickMessage} } /span

    @observable clickMessage = '';
    onClick() { this.clickMessage = 'Clicked!'; }
  </code>
</code-sample>

my-app.ts:

import { FASTElement, observable } from '@microsoft/fast-element';
import { RenderableFASTElement } from '@microsoft/fast-html';
import './code-sample.js';

class MyApp extends RenderableFASTElement(FASTElement) {
  @observable clickMessage = '';

  onClick(_event: Event | null): void {
    this.clickMessage = 'Clicked!';
  }
}

MyApp.defineAsync({
  name: 'my-app',
  templateOptions: 'defer-and-hydrate',
});

After SSR rendering, FAST's template compiler creates binding markers for the double-curly-brace text inside the code element within code-sample, even though that text is meant to be literal display content slotted into a child component, not a binding expression.

🤔 Expected Behavior

  • The double-curly-brace text inside the code element should be treated as literal text. It is light DOM content slotted into code-sample and is not a binding expression.
  • Hydration should succeed without errors.
  • The code example text should be visible as-is in the rendered page.

😯 Current Behavior

Hydration produces console errors like:

FAST: Binding 14 was not found in the DOM. Hydration failed.

The hydration process creates binding nodes for the double-curly-brace text inside the code element, but the pre-rendered DSD does not have corresponding binding markers at those positions, causing a mismatch.

Workarounds attempted:

Approach Result
HTML entity escaping the curly braces Hydration errors persist - entities are decoded before FAST processes them
HTML comments wrapping the text No hydration errors, but text is invisible in the UI
script type=text/plain wrapper SSR renderer still processes content inside script tags
Modifying code-sample connectedCallback Adding connectedCallback causes a new hydration mismatch

No viable workaround exists that both prevents hydration errors AND keeps the code text visible.

💁 Possible Solution

Consider one of:

  1. Escape syntax: Provide an official escape sequence for double-curly-braces in templates so developers can include literal text without triggering the binding parser.

  2. Opt-out attribute: Allow elements to mark their content as non-bindable, e.g. a data-fast-no-bind attribute. The template compiler would skip binding processing for children of such elements.

  3. Semantic element awareness: The template compiler could skip binding processing for text content inside code and pre elements, since these are semantically preformatted/literal content.

We would be happy to contribute a fix or test if there is a preferred direction.

🔦 Context

We are building a component gallery (similar to Storybook) as an SSR/DSD application using @microsoft/fast-html. The gallery displays code examples for each component showing how to use template syntax (event bindings, interpolations). Every code sample that contains double-curly-brace patterns triggers hydration failures, making it impossible to show template syntax examples in a DSD-rendered app. This affects approximately 240 code sample blocks across 33 component story pages.

🌍 Your Environment

  • OS & Device: Windows 11 on PC
  • Browser: Microsoft Edge (Chromium)
  • Version: @microsoft/fast-element 2.10.4, @microsoft/fast-html 1.0.0-alpha.50

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No fields configured for Bug.

    Projects

    Status
    Todo

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions