Add change reporting to the fake DOM.

This commit is contained in:
Timothy Farrell 2017-02-16 23:30:13 -06:00
parent 3230a3ef5a
commit 66149ab830
5 changed files with 249 additions and 7 deletions

View File

@ -0,0 +1,185 @@
const { CreateDocument } = require('../lib/document.js');
describe('Document', () => {
let doc;
const changes = [];
beforeEach(() => {
doc = new CreateDocument(change => changes.push(change));
changes.splice(0, changes.length);
});
describe('element insertion', () => {
it('to do nothing on creation', () => {
const el = doc.createElement('div');
expect(changes).toEqual([]);
});
it('to produce a patch upon append', () => {
const el = doc.createElement('div');
expect(changes).toEqual([]);
doc.body.appendChild(el);
expect(changes).toEqual([
[
0,
doc.body._id,
{
t: 1,
n: 'DIV',
p: [],
i: doc.body._id + 1,
c: []
},
undefined
]
]);
});
it('to produce a patch upon append with a full tree', () => {
const el1 = doc.createElement('div');
const el2 = doc.createElement('span');
expect(changes).toEqual([]);
el1.appendChild(el2);
expect(changes).toEqual([]);
doc.body.appendChild(el1);
expect(changes).toEqual([
[
0,
doc.body._id,
{
t: 1,
n: 'DIV',
p: [],
i: doc.body._id + 1,
c: [
{
t: 1,
n: 'SPAN',
p: [],
i: doc.body._id + 2,
c: []
}
]
},
undefined
]
]);
expect(changes.length).toEqual(1);
});
it('to produce a patch upon insert', () => {
const el1 = doc.createElement('div');
const el2 = doc.createElement('span');
expect(changes).toEqual([]);
doc.body.appendChild(el1);
expect(changes).toEqual([
[
0,
doc.body._id,
{
t: 1,
n: 'DIV',
p: [],
i: doc.body._id + 1,
c: []
},
undefined
]
]);
doc.body.insertBefore(el2, el1);
expect(changes.length).toEqual(2);
expect(changes[1]).toEqual([
0,
doc.body._id,
{
t: 1,
n: 'SPAN',
p: [],
i: doc.body._id + 2,
c: []
},
doc.body._id + 1
]);
});
});
describe('element removal', () => {
it('to propagate no changes after removal', () => {
const el1 = doc.createElement('div');
const el2 = doc.createElement('span');
expect(changes).toEqual([]);
el1.appendChild(el2);
expect(changes).toEqual([]);
doc.body.appendChild(el1);
expect(changes).toEqual([
[
0,
doc.body._id,
{
t: 1,
n: 'DIV',
p: [],
i: doc.body._id + 1,
c: [
{
t: 1,
n: 'SPAN',
p: [],
i: doc.body._id + 2,
c: []
}
]
},
undefined
]
]);
expect(changes.length).toEqual(1);
doc.body.removeChild(el1);
expect(changes.length).toEqual(2);
expect(changes[1]).toEqual([2, doc.body._id + 1]);
el1.removeChild(el2);
expect(changes.length).toEqual(2);
});
});
describe('element attribute changes', () => {
it('to propagate when attached', () => {
const el1 = doc.createElement('div');
const el2 = doc.createElement('span');
el1.setAttribute('class', '1');
expect(changes).toEqual([]);
el1.appendChild(el2);
expect(changes).toEqual([]);
doc.body.appendChild(el1);
expect(changes).toEqual([
[
0,
doc.body._id,
{
t: 1,
n: 'DIV',
p: [{ ns: null, name: 'class', value: '1' }],
i: doc.body._id + 1,
c: [
{
t: 1,
n: 'SPAN',
p: [],
i: doc.body._id + 2,
c: []
}
]
},
undefined
]
]);
expect(changes.length).toEqual(1);
el2.setAttribute('class', '2');
expect(changes.length).toEqual(2);
expect(changes[1]).toEqual([1, doc.body._id + 2, { ns: null, name: 'class', value: '2' }]);
el2.removeAttribute('class');
expect(changes.length).toEqual(3);
expect(changes[2]).toEqual([1, doc.body._id + 2, { name: 'class', value: null }]);
});
});
});

View File

@ -1,6 +1,6 @@
{
"spec_dir": "spec",
"spec_files": ["**/*[sS]pec.js"],
"spec_files": ["**/*.nondom.[sS]pec.js"],
"helpers": ["helpers/**/*.js"],
"stopSpecOnExpectationFailure": false,
"random": false

View File

@ -15,6 +15,8 @@ const NODE_TYPES = {
};
*/
let COUNTER = 0;
function toLower(str) {
return String(str).toLowerCase();
}
@ -38,7 +40,7 @@ function createAttributeFilter(ns, name) {
/** Create a minimally viable DOM Document
* @returns {Document} document
*/
export function CreateDocument() {
export function CreateDocument(onChange) {
function isElement(node) {
return node.nodeType === 1;
}
@ -48,6 +50,8 @@ export function CreateDocument() {
this.nodeType = nodeType;
this.nodeName = nodeName;
this.childNodes = [];
this._id = COUNTER++;
this._attached = false;
}
get nextSibling() {
let p = this.parentNode;
@ -63,14 +67,35 @@ export function CreateDocument() {
get lastChild() {
return this.childNodes[this.childNodes.length - 1];
}
_attach(attach) {
this._attached = attach;
this.childNodes.forEach(n => n._attach(attach));
}
_toDataObj() {
return {
t: this.nodeType,
n: this.nodeName,
p: {},
i: this._id,
c: this.childNodes.map(n => n._toDataObj())
};
}
appendChild(child) {
this.insertBefore(child);
}
insertBefore(child, ref) {
child.remove();
child.parentNode = this;
if (!ref) this.childNodes.push(child);
else splice(this.childNodes, ref, child);
if (!ref) {
this.childNodes.push(child);
} else {
splice(this.childNodes, ref, child);
}
if (this._attached) {
child._attach(true);
onChange([0, this._id, child._toDataObj(), ref && ref._id]);
}
}
replaceChild(child, ref) {
if (ref.parentNode === this) {
@ -80,9 +105,15 @@ export function CreateDocument() {
}
removeChild(child) {
splice(this.childNodes, child);
if (this._attached) {
child._attach(false);
onChange([2, child._id]);
}
}
remove() {
if (this.parentNode) this.parentNode.removeChild(this);
if (this.parentNode) {
this.parentNode.removeChild(this);
}
}
}
@ -91,6 +122,15 @@ export function CreateDocument() {
super(3, '#text'); // TEXT_NODE
this.nodeValue = text;
}
_toDataObj() {
return {
t: this.nodeType,
n: this.nodeName,
p: { textContent: this.nodeValue },
i: this._id,
c: []
};
}
set textContent(text) {
this.nodeValue = text;
}
@ -110,6 +150,16 @@ export function CreateDocument() {
return this.childNodes.filter(isElement);
}
_toDataObj() {
return {
t: this.nodeType,
n: this.nodeName,
p: this.attributes,
i: this._id,
c: this.childNodes.map(n => n._toDataObj())
};
}
setAttribute(key, value) {
this.setAttributeNS(null, key, value);
}
@ -124,6 +174,9 @@ export function CreateDocument() {
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
if (!attr) this.attributes.push((attr = { ns, name }));
attr.value = String(value);
if (this._attached) {
onChange([1, this._id, attr]);
}
}
getAttributeNS(ns, name) {
let attr = findWhere(this.attributes, createAttributeFilter(ns, name));
@ -131,6 +184,9 @@ export function CreateDocument() {
}
removeAttributeNS(ns, name) {
splice(this.attributes, createAttributeFilter(ns, name));
if (this._attached) {
onChange([1, this._id, { name: name, value: null }]);
}
}
addEventListener(type, handler) {
@ -158,6 +214,7 @@ export function CreateDocument() {
class Document extends Element {
constructor() {
super(9, '#document'); // DOCUMENT_NODE
this._attached = true;
}
}

View File

@ -1,2 +1,2 @@
export { Projector } from './projector.js';
// export { Scanner } from './scanner.js';
export { CreateDocument } from './document.js';

View File

@ -45,7 +45,7 @@ export function Projector(domRoot) {
}
function setAttributes(element, props) {
Object.entries(props).forEach(([name, value]) => {
props.forEach(({ name, value }) => {
if (name in element) {
if (name.startsWith('on')) {
const eventName = name.substr(2);