Created
February 21, 2017 23:09
-
-
Save MidnightDesign/40dd5b33ab09bcb2c342e78607cd79d4 to your computer and use it in GitHub Desktop.
Update DOM
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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