Skip to content

Commit cb8a41a

Browse files
committed
Fixing bug where polling doesn't stop after data-poll is removed
1 parent b21e882 commit cb8a41a

File tree

2 files changed

+103
-14
lines changed

2 files changed

+103
-14
lines changed

src/LiveComponent/assets/src/live_controller.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,7 @@ export default class extends Controller implements LiveController {
108108
throw new Error('Invalid Element Type');
109109
}
110110

111-
if (this.element.dataset.poll !== undefined) {
112-
this._initiatePolling(this.element.dataset.poll);
113-
}
111+
this._initiatePolling();
114112

115113
window.addEventListener('beforeunload', this.markAsWindowUnloaded);
116114
this._startAttributesMutationObserver();
@@ -123,9 +121,7 @@ export default class extends Controller implements LiveController {
123121
}
124122

125123
disconnect() {
126-
this.pollingIntervals.forEach((interval) => {
127-
clearInterval(interval);
128-
});
124+
this._stopAllPolling();
129125

130126
window.removeEventListener('beforeunload', this.markAsWindowUnloaded);
131127
this.element.removeEventListener('live:update-model', this.handleUpdateModelEvent);
@@ -794,7 +790,14 @@ export default class extends Controller implements LiveController {
794790
this._updateModelFromElement(target, 'change')
795791
}
796792

797-
_initiatePolling(rawPollConfig: string) {
793+
_initiatePolling() {
794+
this._stopAllPolling();
795+
796+
if ((this.element as HTMLElement).dataset.poll === undefined) {
797+
return;
798+
}
799+
800+
const rawPollConfig = (this.element as HTMLElement).dataset.poll;
798801
const directives = parseDirectives(rawPollConfig || '$render');
799802

800803
directives.forEach((directive) => {
@@ -959,7 +962,10 @@ export default class extends Controller implements LiveController {
959962
}
960963

961964
/**
962-
* Re-establishes the data-original-data attribute if missing.
965+
* Helps "re-normalize" certain root element attributes after a re-render.
966+
*
967+
* 1) Re-establishes the data-original-data attribute if missing.
968+
* 2) Stops or re-initializes data-poll
963969
*
964970
* This happens if a parent component re-renders a child component
965971
* and morphdom *updates* child. This commonly happens if a parent
@@ -979,9 +985,13 @@ export default class extends Controller implements LiveController {
979985

980986
this.mutationObserver = new MutationObserver((mutations) => {
981987
mutations.forEach((mutation) => {
982-
if (mutation.type === 'attributes' && !element.dataset.originalData) {
983-
this.originalDataJSON = this.valueStore.asJson();
984-
this._exposeOriginalData();
988+
if (mutation.type === 'attributes') {
989+
if (!element.dataset.originalData) {
990+
this.originalDataJSON = this.valueStore.asJson();
991+
this._exposeOriginalData();
992+
}
993+
994+
this._initiatePolling();
985995
}
986996
});
987997
});
@@ -994,6 +1004,12 @@ export default class extends Controller implements LiveController {
9941004
private getDefaultDebounce(): number {
9951005
return this.hasDebounceValue ? this.debounceValue : DEFAULT_DEBOUNCE;
9961006
}
1007+
1008+
private _stopAllPolling() {
1009+
this.pollingIntervals.forEach((interval) => {
1010+
clearInterval(interval);
1011+
});
1012+
}
9971013
}
9981014

9991015
/**

src/LiveComponent/assets/test/controller/poll.test.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010
'use strict';
1111

1212
import { shutdownTest, createTest, initComponent } from '../tools';
13-
import { createEvent, fireEvent, getByText, waitFor } from '@testing-library/dom';
14-
import userEvent from '@testing-library/user-event';
15-
import fetchMock from 'fetch-mock-jest';
13+
import { waitFor } from '@testing-library/dom';
1614

1715
describe('LiveController polling Tests', () => {
1816
afterEach(() => {
@@ -112,4 +110,79 @@ describe('LiveController polling Tests', () => {
112110
timeout: 600
113111
});
114112
});
113+
114+
// check polling stops after disconnect
115+
116+
it('polling should stop if data-poll is removed', async () => {
117+
const test = await createTest({ keepPolling: true, renderCount: 0 }, (data: any) => `
118+
<div ${initComponent(data)} ${data.keepPolling ? 'data-poll="delay(500)|$render"' : ''}>
119+
<span>Render count: ${data.renderCount}</span>
120+
</div>
121+
`);
122+
123+
// poll 1
124+
test.expectsAjaxCall('get')
125+
.expectSentData(test.initialData)
126+
.serverWillChangeData((data: any) => {
127+
data.renderCount = 1;
128+
})
129+
.init();
130+
// poll 2
131+
test.expectsAjaxCall('get')
132+
.expectSentData({keepPolling: true, renderCount: 1})
133+
.serverWillChangeData((data: any) => {
134+
data.renderCount = 2;
135+
data.keepPolling = false;
136+
})
137+
.init();
138+
139+
// only wait for about 500ms this time
140+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 1'), {
141+
timeout: 600
142+
});
143+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 2'), {
144+
timeout: 600
145+
});
146+
// wait 1 more second... no more Ajax calls should be made
147+
const timeoutPromise = new Promise((resolve) => {
148+
setTimeout(() => {
149+
resolve(true);
150+
}, 1000);
151+
});
152+
await waitFor(() => timeoutPromise, {
153+
timeout: 1500
154+
});
155+
});
156+
157+
it('stops polling after it disconnects', async () => {
158+
const test = await createTest({ renderCount: 0 }, (data: any) => `
159+
<div ${initComponent(data)} data-poll="delay(500)|$render">
160+
<span>Render count: ${data.renderCount}</span>
161+
</div>
162+
`);
163+
164+
// poll 1
165+
test.expectsAjaxCall('get')
166+
.expectSentData(test.initialData)
167+
.serverWillChangeData((data: any) => {
168+
data.renderCount = 1;
169+
})
170+
.init();
171+
172+
// only wait for about 500ms this time
173+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 1'), {
174+
timeout: 600
175+
});
176+
// "remove" our controller from the page
177+
document.body.innerHTML = '<div>something else</div>';
178+
// wait 1 more second... no more Ajax calls should be made
179+
const timeoutPromise = new Promise((resolve) => {
180+
setTimeout(() => {
181+
resolve(true);
182+
}, 1000);
183+
});
184+
await waitFor(() => timeoutPromise, {
185+
timeout: 1500
186+
});
187+
});
115188
});

0 commit comments

Comments
 (0)