Skip to content

Instantly share code, notes, and snippets.

@DylanPiercey
Created June 5, 2016 05:59
Show Gist options
  • Save DylanPiercey/80b307f817bcbdadfe4e4622ee1a239c to your computer and use it in GitHub Desktop.
Save DylanPiercey/80b307f817bcbdadfe4e4622ee1a239c to your computer and use it in GitHub Desktop.
'use strict'
var TEXT_TYPE = 3
var ELEMENT_TYPE = 1
var NODE_KEY = '__set-dom-key__'
var NODE_INDEX = '__set-dom-index__'
var HTML_ELEMENT = document.createElement('html')
var BODY_ELEMENT = document.createElement('body')
module.exports = setDOM
/**
* @description
* Updates existing dom to match a new dom.
*
* @param {HTMLEntity} prev - The html entity to update.
* @param {String|HTMLEntity} next - The updated html(entity).
*/
function setDOM (prev, next) {
// Alias document element with document.
if (prev === document) prev = document.documentElement
// If a string was provided we will parse it as dom.
if (typeof next === 'string') {
if (prev === document.documentElement) {
HTML_ELEMENT.innerHTML = next
next = HTML_ELEMENT
} else {
BODY_ELEMENT.innerHTML = next
next = BODY_ELEMENT.firstChild
}
}
// Update the node.
setNode(prev, next)
}
/**
* @private
* @description
* Updates a specific htmlNode and does whatever it takes to convert it to another one.
*
* @param {HTMLEntity} prev - The previous HTMLNode.
* @param {HTMLEntity} next - The updated HTMLNode.
*/
function setNode (prev, next) {
// Handle text node update.
if (next.nodeType === TEXT_TYPE) {
if (prev.nodeType !== TEXT_TYPE) {
// we have to replace the node.
prev.parentNode.replaceChild(next, prev)
} else if (prev.nodeValue !== next.nodeValue) {
// If both are text nodes we can update directly.
prev.nodeValue = next.nodeValue
}
return
}
// Update all children (and subchildren).
setChildNodes(prev, next)
// Update the elements attributes / tagName.
if (prev.nodeName === next.nodeName) {
// If we have the same nodename then we can directly update the attributes.
setAttributes(prev, next)
} else {
// Otherwise clone the new node to use as the existing node.
var newPrev = next.cloneNode()
// Copy over all existing children from the original node.
while (prev.firstChild) newPrev.appendChild(prev.firstChild)
// Replace the original node with the new one with the right tag.
prev.parentNode.replaceChild(newPrev, prev)
}
}
/*
* @private
* @description
* Utility that will update one nodes list of attributes to match another node.
*
* @param {Attributes} prev - The previous node.
* @param {Attributes} next - The updated node.
*/
function setAttributes (prev, next) {
var i, prevAttr, nextAttr, ns
var prevAttrs = prev.attributes
var nextAttrs = next.attributes
// Remove old attributes.
for (i = prevAttrs.length; i--;) {
prevAttr = prevAttrs[i]
ns = prevAttr.namespaceURI
nextAttr = nextAttrs.getNamedItemNS(ns, prevAttr.name)
if (!nextAttr) {
prevAttrs.removeNamedItemNS(ns, prevAttr.name)
}
}
// Set new attributes.
for (i = nextAttrs.length; i--;) {
nextAttr = nextAttrs[i]
ns = nextAttr.namespaceURI
prevAttr = prevAttrs.getNamedItemNS(ns, nextAttr.name)
if (!prevAttr) {
// Add a new attribute.
nextAttrs.removeNamedItemNS(ns, nextAttr.name)
prevAttrs.setNamedItemNS(nextAttr)
} else if (prevAttr.value !== nextAttr.value) {
// Update existing attribute.
prevAttr.value = nextAttr.value
}
}
}
/*
* @private
* @description
* Utility that will update one nodes list of childNodes to match another node.
*
* @param {NodeList} prevChildNodes - The previous node.
* @param {NodeList} nextChildNodes - The updated node.
*/
function setChildNodes (prev, next) {
var i, len, prevChild, nextChild
var parent = prev
// Convert nodelists into a usuable map.
var prevChildren = keyNodes(prev)
var nextChildren = keyNodes(next)
// Remove old nodes.
for (i = prevChildren.length; i--;) {
prevChild = prevChildren[i]
nextChild = nextChildren[prevChild[NODE_KEY]]
if (!nextChild || prevChild[NODE_KEY] !== nextChild[NODE_KEY]) {
// Remove node from dom.
parent.removeChild(prevChild)
// Remove node from children map.
prevChildren.splice(prevChild[NODE_INDEX], 1)
}
}
// Set new nodes.
for (i = 0, len = nextChildren.length; i < len; i++) {
nextChild = nextChildren[i]
prevChild = prevChildren[nextChild[NODE_KEY]]
if (prevChild) {
// Update an existing node.
setNode(prevChild, nextChild)
// Check if the node has moved in the tree.
if (prevChildren[i] === prevChild) continue
// Reposition node.
parent.insertBefore(prevChild, prevChildren[i])
// Update childnode's position in the children map.
prevChildren.splice(prevChild[NODE_INDEX], 1)
prevChildren.splice(i, 0, prevChild)
} else {
// Append the new node.
parent.appendChild(nextChild)
}
}
}
/**
* @private
* @description
* Converts an elements childNodes into a keyed map.
* This is used for diffing while keeping elements with 'data-key' or 'id' if possible.
*
* @param {HTMLEntity} parent - The parent element with childNodes to convert.
* @return {Array}
*/
function keyNodes (parent) {
var result = []
var el = parent.firstChild
var i = 0
var key
while (el) {
result.push(el)
key = getKey(el)
el[NODE_KEY] = key || i
el[NODE_INDEX] = i
if (key) result[key] = el
el = el.nextSibling
i++
}
return result
}
/**
* @private
* @description
* Utility to try to pull a key out of an element.
* (Uses 'id' if possible and falls back to 'data-key')
*
* @param {HTMLEntity} node - The node to get the key for.
* @return {String}
*/
function getKey (node) {
if (node.nodeType !== ELEMENT_TYPE) return
return node.id || node.getAttribute('data-key')
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment