Skip to content

Commit 111b992

Browse files
committed
Mutation Observer to clean up after Containers automatically
1 parent 693e260 commit 111b992

File tree

6 files changed

+137
-78
lines changed

6 files changed

+137
-78
lines changed

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
],
2626
"license": "MIT",
2727
"dependencies": {
28+
"es6-weak-map": "^2.0.2",
2829
"lodash.camelcase": "^4.3.0",
2930
"lodash.trim": "^4.5.1",
31+
"mutation-observer": "^1.0.2",
3032
"object-assign": "^4.1.1",
3133
"postcss": "^6.0.0",
3234
"raf": "^3.3.2",
@@ -65,7 +67,7 @@
6567
"prepublish": "yarn run build",
6668
"precommit": "lint-staged",
6769
"eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check",
68-
"prettify": "prettier --write --tab-width=4 '{src|__mocks__}/**/*.js'",
70+
"prettify": "prettier --write --tab-width 4 '{src,__mocks__}/**/*.js'",
6971
"release": "yarn run build && git tag $npm_package_version && git push --follow-tags && yarn publish"
7072
},
7173
"lint-staged": {

src/runtime/Container.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,22 @@ import processConfig from "./processConfig";
22
import adjustContainer from "./adjustContainer";
33
import objectAssign from "object-assign";
44
import ResizeObserver from "resize-observer-polyfill";
5-
import {
6-
getContainerByElement,
7-
addContainerToRegistry
8-
} from "./ContainerRegistry";
5+
import MutationObserver from "mutation-observer";
6+
import WeakMap from "es6-weak-map";
97
import raf from "raf";
108

11-
const observer = new ResizeObserver(entries => {
9+
const containerRegistry = new WeakMap();
10+
11+
const resizeObserver = new ResizeObserver(entries => {
1212
if (!Array.isArray(entries)) {
1313
return;
1414
}
1515

1616
entries.forEach(entry => {
17-
const container = getContainerByElement(entry.target);
17+
const container = containerRegistry.get(entry.target);
1818

1919
if (
20-
container === null ||
20+
typeof container === "undefined" ||
2121
typeof container !== "object" ||
2222
typeof container.adjust !== "function"
2323
) {
@@ -35,6 +35,20 @@ const observer = new ResizeObserver(entries => {
3535
});
3636
});
3737

38+
const mutationObserver = new MutationObserver(mutationsRecords => {
39+
mutationsRecords.forEach(mutationsRecord => {
40+
// Remove container element from registry and unobserve resize changes
41+
mutationsRecord.removedNodes.forEach(node => {
42+
if (containerRegistry.has(node) === false) {
43+
return;
44+
}
45+
46+
resizeObserver.unobserve(node);
47+
containerRegistry.delete(node);
48+
});
49+
});
50+
});
51+
3852
/**
3953
* @class
4054
* @property {Element} containerElement
@@ -58,7 +72,10 @@ export default class Container {
5872
this.unobserveResize = this.unobserveResize.bind(this);
5973
this.adjust = this.adjust.bind(this);
6074

61-
addContainerToRegistry(containerElement, this);
75+
containerRegistry.set(containerElement, this);
76+
mutationObserver.observe(this.containerElement.parentNode, {
77+
childList: true
78+
});
6279

6380
if (this.opts.adjustOnResize) {
6481
this.observeResize();
@@ -73,14 +90,14 @@ export default class Container {
7390
* Starts observing resize changes.
7491
*/
7592
observeResize() {
76-
observer.observe(this.containerElement);
93+
resizeObserver.observe(this.containerElement);
7794
}
7895

7996
/**
8097
* Stops observing resize changes.
8198
*/
8299
unobserveResize() {
83-
observer.unobserve(this.containerElement);
100+
resizeObserver.unobserve(this.containerElement);
84101
}
85102

86103
/**

src/runtime/Container.spec.js

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,33 @@ console.warn = jest.fn();
44
jest.mock("./processConfig", () => jest.fn(config => config));
55
jest.mock("./adjustContainer", () => jest.fn());
66
jest.mock("raf", () => jest.fn(cb => cb()));
7+
jest.mock("es6-weak-map", () => {
8+
const mock = jest.fn();
9+
10+
mock.prototype.set = jest.fn();
11+
mock.prototype.get = jest.fn();
12+
mock.prototype.has = jest.fn();
13+
mock.prototype.delete = jest.fn();
14+
15+
return mock;
16+
});
717

818
jest.mock("resize-observer-polyfill", () => {
919
const mock = jest.fn(cb => {
10-
mock.triggerResizeEvent = cb;
20+
mock.triggerEvent = cb;
21+
});
22+
23+
mock.prototype.observe = jest.fn();
24+
mock.prototype.unobserve = jest.fn();
25+
26+
return mock;
27+
});
28+
29+
jest.mock("mutation-observer", () => {
30+
const mock = jest.fn(cb => {
31+
mock.triggerEvent = cb;
1132
});
1233

13-
// mock.default = mock;
1434
mock.prototype.observe = jest.fn();
1535
mock.prototype.unobserve = jest.fn();
1636

@@ -28,7 +48,10 @@ test("should instantiate properly", () => {
2848
const adjustContainer = require("./adjustContainer");
2949
const raf = require("raf");
3050

31-
const containerElement = {};
51+
const containerElement = {
52+
parentNode: document.createElement("div")
53+
};
54+
3255
const config = {};
3356
const processedConfig = {};
3457

@@ -54,7 +77,9 @@ test("should be able to observe resize events and switch off initial adjust call
5477
const raf = require("raf");
5578
const adjustContainer = require("./adjustContainer");
5679

57-
const containerElement = {};
80+
const containerElement = {
81+
parentNode: document.createElement("div")
82+
};
5883
const config = {};
5984

6085
const containerInstance = new Container(containerElement, config, {
@@ -85,19 +110,34 @@ test("should be able to observe resize events and switch off initial adjust call
85110
});
86111

87112
test("should call adjust() on resize changes", () => {
113+
const WeakMap = require("es6-weak-map");
114+
WeakMap.prototype.set = jest.fn();
115+
WeakMap.prototype.get.mockImplementationOnce(() => undefined);
116+
WeakMap.prototype.get.mockImplementationOnce(element => {
117+
expect(element).toBe(containerElement);
118+
119+
return containerInstance;
120+
});
88121
const ResizeObserver = require("resize-observer-polyfill");
89-
const containerElement = "<ContainerElement>";
122+
const parentElement = document.createElement("div");
123+
const containerElement = document.createElement("div");
124+
parentElement.appendChild(containerElement);
90125
const config = {};
91126
const containerInstance = new Container(containerElement, config, {
92127
adjustOnInstantiation: false,
93128
adjustOnResize: true
94129
});
130+
expect(WeakMap.prototype.set).toHaveBeenCalledTimes(1);
131+
expect(WeakMap.prototype.set).toHaveBeenCalledWith(
132+
containerElement,
133+
containerInstance
134+
);
95135

96136
expect(ResizeObserver).toHaveBeenCalledTimes(1);
97-
expect(typeof ResizeObserver.triggerResizeEvent).toBe("function");
98-
expect(() => ResizeObserver.triggerResizeEvent()).not.toThrow();
137+
expect(typeof ResizeObserver.triggerEvent).toBe("function");
138+
expect(() => ResizeObserver.triggerEvent()).not.toThrow();
99139
expect(() => {
100-
ResizeObserver.triggerResizeEvent([
140+
ResizeObserver.triggerEvent([
101141
{
102142
target: "<HTMLElement>"
103143
}
@@ -107,7 +147,7 @@ test("should call adjust() on resize changes", () => {
107147

108148
containerInstance.adjust = jest.fn();
109149
expect(() => {
110-
ResizeObserver.triggerResizeEvent([
150+
ResizeObserver.triggerEvent([
111151
{
112152
target: containerElement,
113153
contentRect: {
@@ -118,9 +158,62 @@ test("should call adjust() on resize changes", () => {
118158
]);
119159
}).not.toThrow();
120160
expect(console.warn).toHaveBeenCalledTimes(1);
161+
expect(WeakMap.prototype.get).toHaveBeenCalledTimes(2);
121162
expect(containerInstance.adjust).toHaveBeenCalledTimes(1);
122163
expect(containerInstance.adjust).toHaveBeenCalledWith({
123164
width: 1,
124165
height: 2
125166
});
126167
});
168+
169+
test("should clean up after container element is detached from the DOM", () => {
170+
const WeakMap = require("es6-weak-map");
171+
WeakMap.prototype.set = jest.fn();
172+
WeakMap.prototype.has = jest.fn(() => true);
173+
const MutationObserver = require("mutation-observer");
174+
const ResizeObserver = require("resize-observer-polyfill");
175+
ResizeObserver.prototype.unobserve = jest.fn();
176+
const parentElement = document.createElement("div");
177+
const containerElement = document.createElement("div");
178+
parentElement.appendChild(containerElement);
179+
const config = {};
180+
const containerInstance = new Container(containerElement, config, {
181+
adjustOnInstantiation: false,
182+
adjustOnResize: false
183+
});
184+
expect(WeakMap.prototype.set).toHaveBeenCalledTimes(1);
185+
expect(WeakMap.prototype.set).toHaveBeenCalledWith(
186+
containerElement,
187+
containerInstance
188+
);
189+
190+
let mutationRecords = [
191+
{
192+
removedNodes: [containerElement]
193+
}
194+
];
195+
196+
MutationObserver.triggerEvent(mutationRecords);
197+
198+
expect(WeakMap.prototype.has).toHaveBeenCalledTimes(1);
199+
expect(WeakMap.prototype.delete).toHaveBeenCalledTimes(1);
200+
expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledTimes(1);
201+
expect(WeakMap.prototype.delete).toHaveBeenCalledWith(containerElement);
202+
expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledWith(
203+
containerElement
204+
);
205+
206+
// Should not clean up after non-container elements
207+
mutationRecords = [
208+
{
209+
removedNodes: [document.createElement("div")]
210+
}
211+
];
212+
213+
WeakMap.prototype.has = jest.fn(() => false);
214+
MutationObserver.triggerEvent(mutationRecords);
215+
216+
expect(WeakMap.prototype.has).toHaveBeenCalledTimes(1);
217+
expect(WeakMap.prototype.delete).toHaveBeenCalledTimes(1);
218+
expect(ResizeObserver.prototype.unobserve).toHaveBeenCalledTimes(1);
219+
});

src/runtime/ContainerRegistry.js

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/runtime/ContainerRegistry.spec.js

Lines changed: 0 additions & 19 deletions
This file was deleted.

yarn.lock

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1065,7 +1065,7 @@ [email protected], es6-symbol@^3.1, es6-symbol@^3.1.1, es6-symbol@~3.1, es6-symbo
10651065
d "1"
10661066
es5-ext "~0.10.14"
10671067

1068-
es6-weak-map@^2.0.1:
1068+
es6-weak-map@^2.0.1, es6-weak-map@^2.0.2:
10691069
version "2.0.2"
10701070
resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.2.tgz#5e3ab32251ffd1538a1f8e5ffa1357772f92d96f"
10711071
dependencies:
@@ -2407,6 +2407,10 @@ [email protected]:
24072407
version "2.0.0"
24082408
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
24092409

2410+
mutation-observer@^1.0.2:
2411+
version "1.0.2"
2412+
resolved "https://registry.yarnpkg.com/mutation-observer/-/mutation-observer-1.0.2.tgz#5059b3836180cced1d8f74efd7b3aaf7fa678841"
2413+
24102414
24112415
version "0.0.5"
24122416
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.5.tgz#8fbfabb0a98a253d3184331f9e8deb7372fac6c0"

0 commit comments

Comments
 (0)