From 66149ab830c58e05de621fd16456677495ed8d5e Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Thu, 16 Feb 2017 23:30:13 -0600 Subject: [PATCH] Add change reporting to the fake DOM. --- .../projector/spec/document.nondom.spec.js | 185 ++++++++++++++++++ packages/projector/spec/support/jasmine.json | 2 +- packages/projector/src/document.js | 65 +++++- packages/projector/src/index.js | 2 +- packages/projector/src/projector.js | 2 +- 5 files changed, 249 insertions(+), 7 deletions(-) create mode 100644 packages/projector/spec/document.nondom.spec.js diff --git a/packages/projector/spec/document.nondom.spec.js b/packages/projector/spec/document.nondom.spec.js new file mode 100644 index 0000000..e723330 --- /dev/null +++ b/packages/projector/spec/document.nondom.spec.js @@ -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 }]); + }); + }); +}); diff --git a/packages/projector/spec/support/jasmine.json b/packages/projector/spec/support/jasmine.json index b52c9e6..5d5bfaf 100644 --- a/packages/projector/spec/support/jasmine.json +++ b/packages/projector/spec/support/jasmine.json @@ -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 diff --git a/packages/projector/src/document.js b/packages/projector/src/document.js index 984050d..2d6d4a1 100644 --- a/packages/projector/src/document.js +++ b/packages/projector/src/document.js @@ -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; } } diff --git a/packages/projector/src/index.js b/packages/projector/src/index.js index dbbdca6..407a61a 100644 --- a/packages/projector/src/index.js +++ b/packages/projector/src/index.js @@ -1,2 +1,2 @@ export { Projector } from './projector.js'; -// export { Scanner } from './scanner.js'; +export { CreateDocument } from './document.js'; diff --git a/packages/projector/src/projector.js b/packages/projector/src/projector.js index 2485576..eebeb85 100644 --- a/packages/projector/src/projector.js +++ b/packages/projector/src/projector.js @@ -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);