diff --git a/change/@microsoft-fast-element-36cdefd5-ec17-4ec7-936e-fe9c6d22c775.json b/change/@microsoft-fast-element-36cdefd5-ec17-4ec7-936e-fe9c6d22c775.json new file mode 100644 index 00000000000..4df141ed527 --- /dev/null +++ b/change/@microsoft-fast-element-36cdefd5-ec17-4ec7-936e-fe9c6d22c775.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Prevent stale server-rendered structural views from being duplicated during hydration.", + "packageName": "@microsoft/fast-element", + "email": "863023+radium-v@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/fast-element/DESIGN.md b/packages/fast-element/DESIGN.md index 5e6a8a8c6fb..cc6a1653bdc 100644 --- a/packages/fast-element/DESIGN.md +++ b/packages/fast-element/DESIGN.md @@ -108,9 +108,11 @@ The previous `FAST.getById()` slot registry, `FASTGlobal` type, and `KernelServi - During hydration, server-rendered markup is treated as an optimisation over the client template rather than as a source of blank output. If a `render()` directive has an expected binding target but no SSR view boundaries, it creates - and binds the client view at the hydrated location. `repeat()` hydrates the - overlapping SSR/client item ranges, creates client views for missing SSR - ranges, and removes extra SSR ranges when the client item count is smaller. + and binds the client view at the hydrated location. A content binding removes + an SSR structural range when its initial client value has no template, preventing + later updates from composing beside stale DOM. `repeat()` hydrates the overlapping + SSR/client item ranges, creates client views for missing SSR ranges, and removes + extra SSR ranges when the client item count is smaller. Malformed or untargetable markers still surface structured hydration errors — see [Hydration mismatch diagnostics](#hydration-mismatch-diagnostics) below. @@ -171,6 +173,7 @@ and call `installHydrationDiagnostic` (or, more commonly, wrap your formatter in a custom `HydrationDebugger` and pass it through `enableHydration({ debugger })`) if you want to route diagnostics into logging, telemetry, or a devtools panel. + - **Hydration tracking**: Hydration is opt-in via `enableHydration()` from `@microsoft/fast-element/hydration.js`, which creates a `HydrationTracker` and installs a pluggable hydration hook on `ElementController` via `ElementController.installHydrationHook()`. Until this is called, `renderTemplate()` always uses the client-side path — even if the element has a pre-existing shadow root. `HydrationTracker` manages a `Set` of pending elements and resolves the returned controller's `whenHydrated()` promise via a debounced `setTimeout(0)` after the last element finishes binding — ensuring all async template batches settle first. It also resolves tag-specific `whenHydrated(tagName)` promises when work for that FAST element tag completes. By default, the hook no-ops for later prerendered batches after hydration completes; `enableHydration({ stopHydration: StopHydration.never })` keeps the hook active for streamed Declarative Shadow DOM so new elements continue checking for an existing shadow root and hydrate instead of re-rendering it. In that mode, the global `whenHydrated()` promise intentionally remains pending. - On `disconnect()`: calls `disconnectedCallback` on behaviors, unbinds the view. - `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. diff --git a/packages/fast-element/SIZES.md b/packages/fast-element/SIZES.md index 92d854e5126..1b48086fe26 100644 --- a/packages/fast-element/SIZES.md +++ b/packages/fast-element/SIZES.md @@ -4,7 +4,7 @@ Bundle sizes for `@microsoft/fast-element` exports. | Export | Minified | Gzip | Brotli | |--------|----------|------|--------| -| CDN Rollup Bundle | 78.61 KB | 23.40 KB | 20.77 KB | +| CDN Rollup Bundle | 78.79 KB | 23.43 KB | 20.84 KB | | FASTElement (@microsoft/fast-element/fast-element.js) | 23.11 KB | 7.12 KB | 6.41 KB | | Updates (@microsoft/fast-element/updates.js) | 473 B | 335 B | 290 B | | Observable (@microsoft/fast-element/observable.js) | 6.75 KB | 2.51 KB | 2.23 KB | @@ -15,11 +15,11 @@ Bundle sizes for `@microsoft/fast-element` exports. | slotted (@microsoft/fast-element/slotted.js) | 4.66 KB | 1.81 KB | 1.59 KB | | volatile (@microsoft/fast-element/volatile.js) | 6.84 KB | 2.54 KB | 2.26 KB | | when (@microsoft/fast-element/when.js) | 1.88 KB | 731 B | 589 B | -| html (@microsoft/fast-element/html.js) | 27.73 KB | 8.96 KB | 8.02 KB | +| html (@microsoft/fast-element/html.js) | 27.91 KB | 8.98 KB | 8.04 KB | | repeat (@microsoft/fast-element/repeat.js) | 31.80 KB | 10.00 KB | 9.03 KB | | css (@microsoft/fast-element/css.js) | 2.43 KB | 1.00 KB | 911 B | -| enableHydration (@microsoft/fast-element/hydration.js) | 46.53 KB | 13.91 KB | 12.49 KB | -| declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.15 KB | 19.46 KB | 17.42 KB | +| enableHydration (@microsoft/fast-element/hydration.js) | 46.71 KB | 13.94 KB | 12.51 KB | +| declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.33 KB | 19.49 KB | 17.46 KB | | ArrayObserver (@microsoft/fast-element/arrays.js) | 12.55 KB | 4.46 KB | 4.03 KB | | observerMap (@microsoft/fast-element/observer-map.js) | 21.96 KB | 7.73 KB | 6.97 KB | | attributeMap (@microsoft/fast-element/attribute-map.js) | 15.31 KB | 5.41 KB | 4.88 KB | diff --git a/packages/fast-element/src/templating/html-binding-directive.ts b/packages/fast-element/src/templating/html-binding-directive.ts index a315d11b184..36558c85db1 100644 --- a/packages/fast-element/src/templating/html-binding-directive.ts +++ b/packages/fast-element/src/templating/html-binding-directive.ts @@ -19,6 +19,7 @@ import { } from "./html-directive.js"; import { HydrationStage } from "./hydration-view.js"; import { Markup } from "./markup.js"; +import { removeNodeSequence } from "./view.js"; type UpdateTarget = ( this: HTMLBindingDirective, @@ -179,6 +180,19 @@ function updateContent( } } + if ( + view === void 0 && + isHydratable(controller) && + controller.hydrationStage !== HydrationStage.hydrated + ) { + const viewNodes = controller.bindingViewBoundaries[this.targetNodeId]; + + if (viewNodes !== void 0) { + removeNodeSequence(viewNodes.first, viewNodes.last); + delete controller.bindingViewBoundaries[this.targetNodeId]; + } + } + target.textContent = value; } } diff --git a/packages/fast-element/test/declarative/fixtures/scenarios/README.md b/packages/fast-element/test/declarative/fixtures/scenarios/README.md index 9f395237018..d01749620e3 100644 --- a/packages/fast-element/test/declarative/fixtures/scenarios/README.md +++ b/packages/fast-element/test/declarative/fixtures/scenarios/README.md @@ -5,4 +5,4 @@ Fixtures for complex scenarios that may involve multiple features interacting to | Fixture | Description | |---|---| | `duplicate-template-names` | Duplicate connected `` publishers with the same `name` attribute keep the first template assignment for a simple bound element. | -| `nested-elements` | Nested custom elements with state propagation through shadow boundaries, event handling inside `f-repeat` with `$c.parent` context access, and `f-when` conditions within repeated content. | +| `nested-elements` | Nested custom elements with state propagation through shadow boundaries, parent-to-child property binding hydration, event handling inside `f-repeat` with `$c.parent` context access, and `f-when` conditions within repeated content. | diff --git a/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/entry.html b/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/entry.html index 4f9fd81df19..51569da4d2a 100644 --- a/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/entry.html +++ b/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/entry.html @@ -11,6 +11,7 @@ + diff --git a/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/index.html b/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/index.html index c33f9306580..5458ba344af 100644 --- a/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/index.html +++ b/packages/fast-element/test/declarative/fixtures/scenarios/nested-elements/index.html @@ -66,6 +66,20 @@

Single Item

+ + + + + + + + + + + + +