Skip to content

Commit

Permalink
Merge pull request #70 from matthewp/vbug
Browse files Browse the repository at this point in the history
Use MutationObserver to teardown virtual components
  • Loading branch information
matthewp authored Mar 1, 2019
2 parents 3ac7d43 + 5fd8bc9 commit 2ec93f1
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ before_install:
- "export DISPLAY=:99.0"
- "sh -e /etc/init.d/xvfb start"
addons:
firefox: "63.0"
chrome: stable
1 change: 1 addition & 0 deletions lit-html
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"scripts": {
"build": "make",
"preversion": "make",
"testee": "testee --browsers firefox test/test.html",
"testee": "testee --browsers chrome test/test.html --config=testee.json",
"test": "npm run build && npm run testee"
},
"files": [
Expand Down
42 changes: 39 additions & 3 deletions src/virtual.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { Container } from './core.js';
import { directive } from './lit.js';

const partToContainer = new WeakMap();
const containerToPart = new WeakMap();

class DirectiveContainer extends Container {
constructor(renderer, part) {
super(renderer, part);
Expand All @@ -11,17 +14,25 @@ class DirectiveContainer extends Container {
this.host.setValue(result);
this.host.commit();
}

teardown() {
super.teardown();
let part = containerToPart.get(this);
partToContainer.delete(part);
}
}

const map = new WeakMap();


function withHooks(renderer) {
function factory(...args) {
return part => {
let cont = map.get(part);
let cont = partToContainer.get(part);
if(!cont) {
cont = new DirectiveContainer(renderer, part);
map.set(part, cont);
partToContainer.set(part, cont);
containerToPart.set(cont, part);
teardownOnRemove(cont, part);
}
cont.args = args;
cont.update();
Expand All @@ -31,4 +42,29 @@ function withHooks(renderer) {
return directive(factory);
}

const includes = Array.prototype.includes;

function teardownOnRemove(cont, part, node = part.startNode) {
let frag = node.parentNode;
let mo = new MutationObserver(mutations => {
for(let mutation of mutations) {
if(includes.call(mutation.removedNodes, node)) {
mo.disconnect();

if(node.parentNode instanceof ShadowRoot) {
teardownOnRemove(cont, part);
} else {
cont.teardown();
}
break;
} else if(includes.call(mutation.addedNodes, node.nextSibling)) {
mo.disconnect();
teardownOnRemove(cont, part, node.nextSibling);
break;
}
}
});
mo.observe(frag, { childList: true });
}

export { withHooks, withHooks as virtual }
63 changes: 63 additions & 0 deletions test/debug/virtual-teardown.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Haunted</title>
</head>
<body>
<counter-app></counter-app>
<script type="module">
import { html, render } from "https://unpkg.com/lit-html/lit-html.js";
import {
component,
useEffect,
virtual,
useState
} from "../src/haunted.js";

const Counter = () => {
useEffect(() => {
console.log("connected component");
return () => {
console.log("disconnected component");
};
}, []);
const [count, setCount] = useState(0);

return html`
<button type="button" @click="${() => setCount(count + 1)}">
Count: ${count}
</button>
`;
};

customElements.define("component-counter", component(Counter));

const Main = () => {
const [show, toggle] = useState(true);
const [show2, toggle2] = useState(true);
return html`
Component:
<button @click="${() => toggle(!show)}">${show ? "Hide" : "Show"}</button>
${
show
? html`
<component-counter></component-counter>
`
: undefined
} <br /><br />
Virtual:
<button @click="${() => toggle2(!show2)}">
${show2 ? "Hide" : "Show"}
</button>
${show2 ? virtual(Counter)() : undefined}
`;
};

customElements.define("counter-app", component(Main));

</script>
</body>
</html>
2 changes: 1 addition & 1 deletion test/test-shadow.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { html } from '../node_modules/lit-html/lit-html.js';
import { html } from '../lit-html/lit-html.js';
import { component } from '../web.js';
import { attach, cycle } from './helpers.js';

Expand Down
43 changes: 40 additions & 3 deletions test/test-virtual.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { html, render, useState, useEffect, withHooks } from '../web.js';
import { cycle } from './helpers.js';
import { component, html, render, useState, useEffect, withHooks, virtual } from '../web.js';
import { attach, cycle } from './helpers.js';

describe('withHooks()', () => {
describe('virtual()', () => {
it('Creates virtual components', async () => {
let el = document.createElement('div');
let set;
Expand Down Expand Up @@ -130,4 +130,41 @@ describe('withHooks()', () => {
await cycle();
assert.equal(effect, true, 'Effect ran within the virtual component');
});

it('Teardown is invoked', async () => {
const tag = 'app-with-virtual-teardown';
let teardownCalled = 0;
let set;

const Counter = () => {
useEffect(() => {
console.log("connected component");
return () => {
console.log("disconnected component");
teardownCalled++;
};
}, []);
return html`<div>STUFF</div>`;
};

const Main = () => {
const [show2, toggle2] = useState(true);
set = toggle2;
return html`
Virtual:
${show2 ? virtual(Counter)() : undefined}
`;
};

customElements.define(tag, component(Main));

let teardown = attach(tag);
await cycle();

set(false);
await cycle();
teardown();

assert.equal(teardownCalled, 1, 'Use effect teardown called');
});
});
8 changes: 8 additions & 0 deletions testee.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"browser": "chrome",
"args": [
"--headless",
"--disable-gpu",
"--remote-debugging-port=9222"
]
}

0 comments on commit 2ec93f1

Please sign in to comment.