Skip to content

Instantly share code, notes, and snippets.

@MidnightDesign
Created February 21, 2017 23:09
Show Gist options
  • Save MidnightDesign/40dd5b33ab09bcb2c342e78607cd79d4 to your computer and use it in GitHub Desktop.
Save MidnightDesign/40dd5b33ab09bcb2c342e78607cd79d4 to your computer and use it in GitHub Desktop.
Update DOM
function updateElement(target: Element, source: Element): Element {
target = updateTagName(target, source.tagName);
updateAttributes(target, source);
updateChildNodes(target, source);
return target;
}
function updateAttributes(target: Element, source: Element): void {
const sourceAttributes = Array.from(source.attributes);
const targetAttributes = Array.from(target.attributes);
targetAttributes.concat(sourceAttributes)
.map(attribute => attribute.name)
.filter((value, index, attributes) => attributes.indexOf(value) === index)
.forEach(attribute => updateAttribute(target, source, attribute));
}
function updateTagName(target: Element, tagName: string): Element {
if (target.tagName === tagName) {
return target;
}
const newTarget = target.ownerDocument.createElement(tagName);
for (let i = 0; i < target.childNodes.length; i++) {
newTarget.appendChild(target.childNodes[i].cloneNode(true));
}
for (let i = target.attributes.length - 1; i >= 0; --i) {
newTarget.attributes.setNamedItem(<Attr>target.attributes[i].cloneNode());
}
if (target.parentNode === null) {
throw `Unable to update tag name because the target node has no parent node.`;
}
target.parentNode.replaceChild(newTarget, target);
return newTarget;
}
function updateNode(target: Node, source: Node): Node {
if (target.nodeType !== source.nodeType) {
if (target.parentNode === null) {
throw `Unable to update target node because it has no parent.`;
}
const newTarget = source.cloneNode(true);
target.parentNode.replaceChild(newTarget, target);
return newTarget;
}
if (target instanceof Element && source instanceof Element) {
return updateElement(target, source);
}
return target;
}
function updateChildNodes(target: Element, source: Element) {
const max = Math.max(target.childNodes.length, source.childNodes.length);
for (let i = 0; i < max; i++) {
const targetNode = target.childNodes[i];
const sourceNode = source.childNodes[i];
if (targetNode === undefined) {
target.appendChild(sourceNode.cloneNode(true));
return;
}
if (sourceNode === undefined) {
target.removeChild(targetNode);
return;
}
updateNode(targetNode, sourceNode);
}
}
function updateAttribute(target: Element, source: Element, attribute: string): void {
const sourceAttribute = source.getAttribute(attribute);
if (sourceAttribute === null) {
target.removeAttribute(attribute);
return;
}
target.setAttribute(attribute, sourceAttribute);
}
(function tests() {
type Test = () => void;
type Attributes = {[attribute: string]: string};
function createChildElement(parent: Element, tagName: string, attributes?: Attributes): Element {
const element = createElement(tagName, attributes);
parent.appendChild(element);
return element;
}
function createElement(tagName: string, attributes?: Attributes): Element {
const element = document.createElement(tagName);
if (attributes !== undefined) {
for (const attribute in attributes) {
element.setAttribute(attribute, attributes[attribute]);
}
}
return element;
}
function fail(failMessage?: string) {
throw failMessage || 'Assertion failed';
}
function assert(assertion: boolean, failMessage?: string): void {
if (assertion) {
return;
}
fail(failMessage);
}
function assertSame(expected: any, actual: any) {
assert(expected === actual, `Failed asserting that "${actual}" is the same as "${expected}".`);
}
function testContainer(): Element {
const ID = 'test-container';
let container = document.getElementById(ID);
if (container !== null && container.parentNode !== null) {
container.parentNode.removeChild(container);
}
container = document.createElement('div');
container.id = ID;
document.body.appendChild(container);
return container;
}
const tests: Test[] = [
function addAttribute() {
let target = createElement('div');
target = updateElement(target, createElement('div', {foo: 'bar'}));
assertSame('bar', target.getAttribute('foo'));
},
function removeAttribute() {
let target = createElement('div', {foo: 'bar'});
target = updateElement(target, createElement('div'));
assertSame(null, target.getAttribute('foo'));
},
function updateAttribute() {
let target = createElement('div', {foo: 'bar'});
target = updateElement(target, createElement('div', {foo: 'baz'}));
assertSame('baz', target.getAttribute('foo'));
},
function updateTagName() {
let target = createChildElement(document.body, 'div');
target = updateElement(target, createElement('span'));
assertSame('SPAN', target.tagName);
},
function updateTagNameRemovesOriginalElement() {
let target = createChildElement(testContainer(), 'div');
target = updateElement(target, createElement('span'));
assertSame(1, (<Element>target.parentNode).childNodes.length);
},
function updateTagNameKeepsChildren() {
let target = createChildElement(testContainer(), 'div');
createChildElement(target, 'div');
const source = createElement('span');
createChildElement(source, 'div');
target = updateElement(target, source);
assertSame(1, target.childNodes.length);
assertSame('DIV', (<Element>target.childNodes[0]).tagName);
assertSame(0, (<Element>target.childNodes[0]).attributes.length);
},
function updateChildElement() {
const target = createElement('div');
createChildElement(target, 'div', {foo: 'bar'});
const source = createElement('div');
createChildElement(source, 'div', {foo: 'baz'});
const newTarget = updateElement(target, source);
assertSame('baz', newTarget.children.item(0).getAttribute('foo'));
},
function addChildElement() {
const target = createElement('div');
const source = createElement('div');
createChildElement(source, 'div', {foo: 'baz'});
const newTarget = updateElement(target, source);
assertSame(1, newTarget.childNodes.length);
assertSame('baz', newTarget.children.item(0).getAttribute('foo'));
},
function removeChildElement() {
const target = createElement('div');
createChildElement(target, 'div', {foo: 'baz'});
const source = createElement('div');
const newTarget = updateElement(target, source);
assertSame(0, newTarget.childNodes.length);
},
function replaceElementWithText() {
const target = createChildElement(testContainer(), 'div');
const source = document.createTextNode('Test');
const newTarget = updateNode(target, source);
assert(newTarget instanceof Text, `Failed asserting that the new target is a text node.`);
assertSame('Test', newTarget.textContent);
},
function replaceTextWithElement() {
const target = document.createTextNode('Test');
testContainer().appendChild(target);
const source = createElement('div');
const newTarget = updateNode(target, source);
assert(newTarget instanceof Element, `Failed asserting that the new target is an element.`);
},
];
function run(test: Test) {
try {
test();
} catch (e) {
console.error(`${test.name} failed: ${e}`);
return;
}
console.info(`${test.name} successful`);
}
tests.forEach(run);
}());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment