From e4593a905b74fb1a458dc446f7038e2a5b88a3a6 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:56:46 -0400 Subject: [PATCH] Re-render dynamic components in append position when the definition changes Previously, when a component definition rendered in append position (e.g. `{{if @isOk Ok Ko}}` or `{{@Foo}}`) changed to a *different* component definition, nothing invalidated the rendered component: the content-type guard only re-renders when the content *type* changes (component -> string, etc.), and the curried component was resolved once on initial render. Emit AssertSame before resolving the curried component in the dynamic append paths (stdlib StdAppend and the dynamic call append in statement compilation), mirroring the existing String/SafeString/ Fragment/Node branches and the Assert that InvokeDynamicComponent already gets via Replayable. When the definition reference's value changes, the enclosing Enter/Exit region is cleared and re-rendered with the new component. Fixes #21042 Co-Authored-By: Claude Fable 5 --- .../test/strict-mode-test.ts | 85 +++++++++++++++++++ .../lib/opcode-builder/helpers/stdlib.ts | 1 + .../opcode-compiler/lib/syntax/statements.ts | 2 + 3 files changed, 88 insertions(+) diff --git a/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts b/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts index f00d6096ea1..6aacd407591 100644 --- a/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts +++ b/packages/@glimmer-workspace/integration-tests/test/strict-mode-test.ts @@ -636,6 +636,91 @@ class DynamicStrictModeTest extends RenderTest { this.assertStableRerender(); } + @test + 'Can use a dynamic component with a changing definition (append position)'(assert: Assert) { + const Foo = defineComponent({}, 'Hello, world!', { + definition: class extends GlimmerishComponent { + override willDestroy() { + assert.step('willDestroy 1 called'); + } + }, + }); + + const Bar = defineComponent({}, 'Hello, earth!', { + definition: class extends GlimmerishComponent { + override willDestroy() { + assert.step('willDestroy 2 called'); + } + }, + }); + + const Baz = defineComponent({}, '{{@Foo}}'); + + let args = trackedObj({ Foo }); + + this.renderComponent(Baz, args); + this.assertHTML('Hello, world!'); + this.assertStableRerender(); + + args['Foo'] = Bar; + + this.rerender(); + this.assertHTML('Hello, earth!'); + this.assertStableRerender(); + assert.verifySteps(['willDestroy 1 called']); + + args['Foo'] = undefined; + + this.rerender(); + this.assertHTML(''); + this.assertStableRerender(); + assert.verifySteps(['willDestroy 2 called']); + } + + @test + 'Can use a dynamic component with a changing definition (append position, with args)'() { + const Foo = defineComponent({}, 'Hello, {{@value}}!'); + const Bar = defineComponent({}, 'Goodbye, {{@value}}!'); + const Baz = defineComponent({}, '{{@Foo value="world"}}'); + + let args = trackedObj({ Foo }); + + this.renderComponent(Baz, args); + this.assertHTML('Hello, world!'); + this.assertStableRerender(); + + args['Foo'] = Bar; + + this.rerender(); + this.assertHTML('Goodbye, world!'); + this.assertStableRerender(); + } + + @test + 'Can use an inline if to swap components in append position'() { + const Ok = defineComponent({}, 'Ok'); + const Ko = defineComponent({}, 'Ko'); + const Foo = defineComponent({ Ok, Ko }, '{{if @isOk Ok Ko}}'); + + let args = trackedObj({ isOk: true }); + + this.renderComponent(Foo, args); + this.assertHTML('Ok'); + this.assertStableRerender(); + + args['isOk'] = false; + + this.rerender(); + this.assertHTML('Ko'); + this.assertStableRerender(); + + args['isOk'] = true; + + this.rerender(); + this.assertHTML('Ok'); + this.assertStableRerender(); + } + @test 'Can use a dynamic component in block position'() { const Foo = defineComponent({}, 'Hello, {{yield}}'); diff --git a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts index 466add49cc9..4f4b29746ea 100644 --- a/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts +++ b/packages/@glimmer/opcode-compiler/lib/opcode-builder/helpers/stdlib.ts @@ -56,6 +56,7 @@ export function StdAppend( if (typeof nonDynamicAppend === 'number') { when(ContentType.Component, () => { + op(VM_ASSERT_SAME_OP); op(VM_RESOLVE_CURRIED_COMPONENT_OP); op(VM_PUSH_DYNAMIC_COMPONENT_INSTANCE_OP); InvokeBareComponent(op); diff --git a/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts b/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts index 4a57b28a48c..cfafe8dd329 100644 --- a/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts +++ b/packages/@glimmer/opcode-compiler/lib/syntax/statements.ts @@ -6,6 +6,7 @@ import type { WireFormat, } from '@glimmer/interfaces'; import { + VM_ASSERT_SAME_OP, VM_CLOSE_ELEMENT_OP, VM_COMMENT_OP, VM_COMPONENT_ATTR_OP, @@ -218,6 +219,7 @@ STATEMENTS.add(SexpOpcodes.Append, (op, [, value]) => { }, (when) => { when(ContentType.Component, () => { + op(VM_ASSERT_SAME_OP); op(VM_RESOLVE_CURRIED_COMPONENT_OP); op(VM_PUSH_DYNAMIC_COMPONENT_INSTANCE_OP); InvokeNonStaticComponent(op, {