Skip to content

Commit 4f68951

Browse files
lifeartclaude
andcommitted
fix(gxt-backend): compare _templateFn for route template identity
GXT's runtime template factory returns a fresh wrapper object on every `templateFactory(owner)` invocation, and `buildRenderState` calls the factory on each route render. The root-outlet re-render fast-path used strict object identity (`lastRouteTemplate !== newTemplate`) to detect a template swap, so even renders of the SAME route template were treated as a swap and forced a full re-render (innerHTML = ''). That destroyed DOM node identity across `/colors/red -> /colors/green` style transitions and broke the "stable DOM when the model changes" invariant. Compare the underlying `_templateFn` (the compiled function shared across fresh wrappers) so two wrappers around the same template stay on the in-place fast-path. Falls back to object identity for non-runtime templates so the test-helpers fresh-compile case (different `_templateFn`) still triggers a full re-render. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent 03c4948 commit 4f68951

1 file changed

Lines changed: 19 additions & 2 deletions

File tree

  • packages/@ember/-internals/glimmer/lib/templates

packages/@ember/-internals/glimmer/lib/templates/root.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1008,8 +1008,25 @@ export default function createRootTemplate(_owner: any) {
10081008
// ('index') but hand us a fresh `compile()` template on each render.
10091009
// If we stayed on the fast-path in that case, we would only update
10101010
// model cells and miss the new template's DOM content.
1011-
const routeTemplateChanged =
1012-
lastRouteTemplate !== null && newTemplate && lastRouteTemplate !== newTemplate;
1011+
//
1012+
// NOTE: GXT's runtime template factory (precompileTemplate) returns a
1013+
// fresh wrapper object for every `templateFactory(owner)` invocation,
1014+
// and `buildRenderState` calls the factory on every route render. As a
1015+
// result, `lastRouteTemplate !== newTemplate` is `true` even when the
1016+
// route renders the SAME template (the underlying `_templateFn` is the
1017+
// same instance). Treating that as a template swap forces a full
1018+
// re-render that destroys DOM node identity and breaks the "stable DOM
1019+
// when the model changes" invariant. Compare the underlying
1020+
// `_templateFn` (set on every gxt-runtime-template wrapper) so two
1021+
// wrappers around the same compiled function are recognized as equal.
1022+
const routeTemplateChanged = (() => {
1023+
if (lastRouteTemplate === null || !newTemplate) return false;
1024+
if (lastRouteTemplate === newTemplate) return false;
1025+
const lastFn = (lastRouteTemplate as any)?._templateFn;
1026+
const newFn = (newTemplate as any)?._templateFn;
1027+
if (lastFn && newFn && lastFn === newFn) return false;
1028+
return true;
1029+
})();
10131030

10141031
// If same route template AND nested outlets haven't changed, try to
10151032
// update existing cells in-place to preserve DOM node identity.

0 commit comments

Comments
 (0)