Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/LiveComponent/assets/src/live_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ export default class LiveControllerDefault extends Controller<HTMLElement> imple
}

connect() {
// Stimulus only calls initialize() once per controller instance. When
// the same DOM element is disconnected then reconnected (e.g. a parent
// morphs it back in with new props), connect() is called again on the
// existing controller. If the props now differ from what the Component
// was built with, the ValueStore is stale and would reject any model
// matching the new server-rendered HTML — so rebuild the Component.
if (JSON.stringify(this.propsValue) !== JSON.stringify(this.component.valueStore.getOriginalProps())) {
this.createComponent();
}

this.connectComponent();

this.mutationObserver.observe(this.element, {
Expand Down
30 changes: 29 additions & 1 deletion src/LiveComponent/assets/test/unit/controller/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Component from '../../../src/Component';
import { findComponents } from '../../../src/ComponentRegistry';
import { htmlToElement } from '../../../src/dom_utils';
import { getComponent } from '../../../src/live_controller';
import { createTest, initComponent, shutdownTests, startStimulus } from '../../tools';
import { createTest, getStimulusApplication, initComponent, shutdownTests, startStimulus } from '../../tools';

describe('LiveController Basic Tests', () => {
afterEach(() => {
Expand Down Expand Up @@ -52,4 +52,32 @@ describe('LiveController Basic Tests', () => {
await expect(getComponent(test.element)).rejects.toThrow('Component not found for element');
expect(findComponents(test.component, false, null)).toEqual([]);
});

it('rebuilds the Component on reconnect when props changed in between', async () => {
const test = await createTest({ greeting: 'aloha' }, (data: any) => `<div ${initComponent(data)}></div>`);

const controller = getStimulusApplication().getControllerForElementAndIdentifier(test.element, 'live') as any;
const originalComponent = controller.component;
expect(originalComponent.valueStore.getOriginalProps()).toEqual({ greeting: 'aloha' });

// simulate a parent morph: same controller instance, fresh props from the server
controller.disconnect();
controller.propsValue = { greeting: 'hello' };
controller.connect();

expect(controller.component).not.toBe(originalComponent);
expect(controller.component.valueStore.getOriginalProps()).toEqual({ greeting: 'hello' });
});

it('keeps the existing Component on reconnect when props are unchanged', async () => {
const test = await createTest({ greeting: 'aloha' }, (data: any) => `<div ${initComponent(data)}></div>`);

const controller = getStimulusApplication().getControllerForElementAndIdentifier(test.element, 'live') as any;
const originalComponent = controller.component;

controller.disconnect();
controller.connect();

expect(controller.component).toBe(originalComponent);
});
});
Loading