Skip to content

Commit 1a617b5

Browse files
committed
bug #385 [LiveComponents] Stop polling if data-poll disappears (weaverryan)
This PR was squashed before being merged into the 2.x branch. Discussion ---------- [LiveComponents] Stop polling if data-poll disappears | Q | A | ------------- | --- | Bug fix? | yes | New feature? | no | Tickets | Fixes #382 | License | MIT Polling now stops if, on re-render, `data-poll` disappears. Or, if `data-poll` changes, the new values would be used. Commits ------- 6942053 [LiveComponents] Stop polling if data-poll disappears
2 parents 91fb6d6 + 6942053 commit 1a617b5

File tree

2 files changed

+215
-11
lines changed

2 files changed

+215
-11
lines changed

src/LiveComponent/assets/src/live_controller.ts

+27-11
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
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
/*
2+
* This file is part of the Symfony package.
3+
*
4+
* (c) Fabien Potencier <[email protected]>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
'use strict';
11+
12+
import { shutdownTest, createTest, initComponent } from '../tools';
13+
import { waitFor } from '@testing-library/dom';
14+
15+
describe('LiveController polling Tests', () => {
16+
afterEach(() => {
17+
shutdownTest();
18+
})
19+
20+
it('starts a poll', async () => {
21+
const test = await createTest({ renderCount: 0 }, (data: any) => `
22+
<div ${initComponent(data)} data-poll>
23+
<span>Render count: ${data.renderCount}</span>
24+
</div>
25+
`);
26+
27+
// poll 1
28+
test.expectsAjaxCall('get')
29+
.expectSentData(test.initialData)
30+
.serverWillChangeData((data: any) => {
31+
data.renderCount = 1;
32+
})
33+
.init();
34+
// poll 2
35+
test.expectsAjaxCall('get')
36+
.expectSentData({renderCount: 1})
37+
.serverWillChangeData((data: any) => {
38+
data.renderCount = 2;
39+
})
40+
.init();
41+
42+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 1'), {
43+
timeout: 2100
44+
});
45+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 2'), {
46+
timeout: 2100
47+
});
48+
});
49+
50+
it('is controllable via modifiers', async () => {
51+
const test = await createTest({ renderCount: 0 }, (data: any) => `
52+
<div ${initComponent(data)} data-poll="delay(500)|$render">
53+
<span>Render count: ${data.renderCount}</span>
54+
</div>
55+
`);
56+
57+
// poll 1
58+
test.expectsAjaxCall('get')
59+
.expectSentData(test.initialData)
60+
.serverWillChangeData((data: any) => {
61+
data.renderCount = 1;
62+
})
63+
.init();
64+
// poll 2
65+
test.expectsAjaxCall('get')
66+
.expectSentData({renderCount: 1})
67+
.serverWillChangeData((data: any) => {
68+
data.renderCount = 2;
69+
})
70+
.init();
71+
72+
// only wait for about 500ms this time
73+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 1'), {
74+
timeout: 600
75+
});
76+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 2'), {
77+
timeout: 600
78+
});
79+
});
80+
81+
it('can also call a live action', async () => {
82+
const test = await createTest({ renderCount: 0 }, (data: any) => `
83+
<div ${initComponent(data)} data-poll="delay(500)|saveAction">
84+
<span>Render count: ${data.renderCount}</span>
85+
</div>
86+
`);
87+
88+
// poll 1
89+
test.expectsAjaxCall('post')
90+
.expectSentData(test.initialData)
91+
.expectActionCalled('saveAction')
92+
.serverWillChangeData((data: any) => {
93+
data.renderCount = 1;
94+
})
95+
.init();
96+
// poll 2
97+
test.expectsAjaxCall('post')
98+
.expectSentData({renderCount: 1})
99+
.expectActionCalled('saveAction')
100+
.serverWillChangeData((data: any) => {
101+
data.renderCount = 2;
102+
})
103+
.init();
104+
105+
// only wait for about 500ms this time
106+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 1'), {
107+
timeout: 600
108+
});
109+
await waitFor(() => expect(test.element).toHaveTextContent('Render count: 2'), {
110+
timeout: 600
111+
});
112+
});
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+
});
188+
});

0 commit comments

Comments
 (0)