Open tunnckoCore opened 6 years ago
mindom
/**
* @copyright 2017-present, Charlike Mike Reagent <olsten.larck@gmail.com>
* @license Apache-2.0
*/
const NO_SUFFIX = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i
function decamelize (str) {
return str
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2')
.toLowerCase()
}
function toLower (str) {
return String(str).toLowerCase()
}
function isObject (val) {
return val && typeof val === 'object' && !Array.isArray(val)
}
/* eslint-disable max-params, fp/no-loops */
function splice (arr, item, add, byValueOnly) {
let i = arr ? findWhere(arr, item, true, byValueOnly) : -1
if (~i) add ? arr.splice(i, 0, add) : arr.splice(i, 1)
return i
}
function findWhere (arr, fn, returnIndex, byValueOnly) {
let i = arr.length
while (i--) {
if (typeof fn === 'function' && !byValueOnly ? fn(arr[i]) : arr[i] === fn) {
break
}
}
return returnIndex ? i : arr[i]
}
function createAttributeFilter (ns, name) {
return (o) => o.namespaceURI === ns && toLower(o.name) === toLower(name)
}
function addStyle (node, value, attribute) {
if (isObject(value)) {
let cssText = ''
Object.keys(value).forEach((k) => {
let val = value[k]
const suffix = typeof val === 'number' && NO_SUFFIX.test(k) === false
val = suffix ? val + 'px' : val
cssText += `${decamelize(k)}:${val};`
this.style[k] = val
this.style.cssText = cssText
attribute.value = cssText
// node.attributes.style is `Attribute` object,
// so the `.value` is same as `node.style.cssText`
this.attributes.style.value = cssText
})
} else {
this.style.cssText = String(value)
}
}
class Attribute {
constructor (name, value, ns) {
this.nodeType = 2
this.name = name
this.value = value
this.namespaceURI = ns || null
}
}
class Node {
constructor (nodeType, nodeName, namespaceURI) {
this.namespaceURI = namespaceURI || null
this.nodeType = nodeType
this.nodeName = nodeName
this.childNodes = []
}
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)
}
replaceChild (child, ref) {
if (ref.parentNode === this) {
this.insertBefore(child, ref)
ref.remove()
}
}
removeChild (child) {
splice(this.childNodes, child)
}
remove () {
if (this.parentNode) this.parentNode.removeChild(this)
}
}
class Text extends Node {
constructor (text) {
super(3, '#text') // TEXT_NODE
this.nodeValue = text
}
set textContent (text) {
this.nodeValue = text
}
get textContent () {
return this.nodeValue
}
}
class Element extends Node {
constructor (nodeName, namespaceURI) {
super(1, nodeName, namespaceURI)
this.__handlers = {}
this.attributes = []
this.style = {}
Object.defineProperty(this, 'className', {
set: (val) => {
this.setAttribute('class', val)
},
get: () => this.getAttribute('class'),
})
}
get children () {
return this.childNodes.filter((node) => node.nodeType === 1)
}
hasAttribute (key) {
return this.hasAttributeNS(null, key)
}
hasAttributeNS (namespaceURI, key) {
return (
this.attributes.hasOwnProperty(key) &&
this.attributes[key].namespaceURI === namespaceURI
)
}
setAttribute (key, value) {
this.setAttributeNS(this.namespaceURI, key, value)
}
setAttributeNS (namespaceURI, name, value) {
const attributeFilter = createAttributeFilter(namespaceURI, name)
let attribute = findWhere(this.attributes, attributeFilter)
if (!attribute) {
attribute = new Attribute(name, value, namespaceURI)
this.attributes.push(attribute)
this.attributes[attribute.name] = attribute
} else {
attribute.value = String(value)
if (attribute.name === 'style') {
addStyle(this, value, attribute)
}
}
}
getAttribute (key) {
return this.getAttributeNS(null, key)
}
getAttributeNS (namespaceURI, name) {
const attributeFilter = createAttributeFilter(namespaceURI, name)
let attr = findWhere(this.attributes, attributeFilter)
return attr && attr.value
}
removeAttribute (key) {
this.removeAttributeNS(null, key)
}
removeAttributeNS (namespaceURI, name) {
splice(this.attributes, createAttributeFilter(namespaceURI, name))
}
addEventListener (type, handler) {
const name = 'on' + toLower(type)
;(this.__handlers[name] || (this.__handlers[name] = [])).push(handler)
}
removeEventListener (type, handler) {
type = toLower(type)
splice(this.__handlers['on' + type], handler, 0, true)
}
}
function createElement (nodeName) {
return new Element(String(nodeName).toUpperCase())
}
function createElementNS (ns, nodeName) {
return new Element(String(nodeName).toUpperCase(), ns)
}
function createTextNode (text) {
return new Text(text)
}
class Document extends Element {
constructor () {
super(9, '#document')
}
}
export {
Node,
Text,
Element,
Document,
Attribute,
createElement,
createElementNS,
createTextNode,
// expose utils for easier extending
findWhere,
splice,
createAttributeFilter
}
mindom/import ("register" for esm like import 'mindom/import';
/**
* @copyright 2017-present, Charlike Mike Reagent <olsten.larck@gmail.com>
* @license Apache-2.0
*/
import { Element, Document, createElement, createTextNode } from './mindom.mjs'
global.Element = Element
global.document = Object.assign(new Document(), {
createElement,
createTextNode,
})
global.document.body = createElement('body')
minscript
/**
* @copyright 2017-present, Charlike Mike Reagent <olsten.larck@gmail.com>
* @license Apache-2.0
*/
const isString = (val) => typeof val === 'string'
const isObject = (val) => val && typeof val === 'object' && !Array.isArray(val)
export default function hyperscript (tag, ...rest) {
const args = [].concat(...rest)
const node = document.createElement(tag)
args.forEach((item) => {
if (isString(item) || typeof item === 'number') {
node.appendChild(document.createTextNode(item))
} else if (item instanceof Element) {
node.appendChild(item)
} else if (isObject(item)) {
addProps(node, item)
}
})
return node
}
function addProps (node, props) {
Object.keys(props).forEach((name) => {
const value = props[name]
if (name === 'class' || name === 'className') {
addClass(node, value)
} else if (name.startsWith('on')) {
addEvents(node, name, value)
} else {
node.setAttribute(name, value)
}
return node
})
}
function addClass (node, value) {
if (Array.isArray(value)) {
node.setAttribute('class', value.filter(Boolean).join(' '))
} else if (isObject(value)) {
const val = Object.keys(value)
.filter((name) => Boolean(value[name]))
.map((name) => name)
.join(' ')
node.setAttribute('class', val)
} else {
// assume string add explicit check if errors come
node.setAttribute('class', value)
}
}
function addEvents (node, name, value) {
name = name.toLowerCase()
if (name === 'on' && isObject(value)) {
Object.keys(value).forEach((name) => {
const prop = `on${name}`
node.addEventListener(name, value[name])
node[prop] = value[name]
})
} else {
node.addEventListener(name, value)
node[name] = value
}
}
minmorph (biggest one...)
/**
* @copyright 2017-present, Charlike Mike Reagent <olsten.larck@gmail.com>
* @license Apache-2.0
*/
export default function minmorph (left, right) {
if (!left) {
return right
} else if (!right) {
return null
} else if (right.isSameNode && right.isSameNode(left)) {
return left
} else if (right.nodeName !== left.nodeName) {
return right
} else {
morph(left, right)
return left
}
}
function morph (left, right) {
// if Element Node
if (right.nodeType === 1) {
morphProps(left, right)
}
// if Text Node
if (right.nodeType === 3) {
left.nodeValue = right.nodeValue
}
// Some DOM nodes are weird
// https://github.com/patrick-steele-idem/morphdom/blob/master/src/specialElHandlers.js
if (right.nodeName === 'INPUT') {
morphInput(left, right)
}
if (right.nodeName === 'OPTION') {
morphAttribute(left, right, 'selected')
}
if (right.nodeName === 'TEXTAREA') {
morphTextarea(left, right)
}
morphEvents(left, right)
morphChildren(left, right)
}
function morphProps (left, right) {
const isBrowser = typeof global !== 'undefined' && global.document
const newKeys = Object.keys(right.attributes)
const oldKeys = Object.keys(right.attributes)
const rightAttrKeys = isBrowser ? newKeys : newKeys.slice(newKeys.length)
const leftAttrKeys = isBrowser ? oldKeys : oldKeys.slice(oldKeys.length)
rightAttrKeys.forEach((name) => {
const attr = right.attributes[name]
const ns = attr.namespaceURI // always `null` or actual namespace!
if (!left.hasAttributeNS(ns, attr.name)) {
left.setAttributeNS(ns, attr.name, attr.value)
} else {
const value = left.getAttributeNS(ns, attr.name)
if (value !== attr.value) {
// directly set new attribute
left.setAttributeNS(ns, attr.name, attr.value)
// and if input, select/option OR textarea,
// then these will apply something.
// Otherwise they won't touch the old node anyway,
// so it is perfectly safe.
morphInput(left, right)
morphAttribute(left, right, 'selected')
morphTextarea(left, right)
}
}
})
leftAttrKeys.forEach((name) => {
const attr = left.attributes[name] || {}
// attr.namespaceURI is always actual namespace or `null` so...
if (attr.specified && !right.hasAttributeNS(attr.namespaceURI, attr.name)) {
left.removeAttributeNS(attr.namespaceURI, attr.name)
}
})
return left
}
function morphInput (left, right) {
let oldValue = left.value
let newValue = right.value
morphAttribute(left, right, 'checked')
morphAttribute(left, right, 'disabled')
if (oldValue !== newValue) {
left.setAttribute('value', newValue)
left.value = newValue
}
if (newValue === 'null') {
left.removeAttribute('value')
left.value = ''
}
if (!right.hasAttribute('value')) {
left.removeAttribute('value')
} else if (left.type === 'range') {
// this is so elements like slider move their UI thingy
left.value = newValue
}
}
function morphAttribute (left, right, name) {
if (right[name] !== left[name]) {
left[name] = right[name]
if (right[name]) {
left.setAttribute(name, '')
} else {
left.removeAttribute(name)
}
}
}
function morphTextarea (left, right) {
const firstChild = left.childNodes[0]
if (right.value !== left.value) {
left.value = right.value
}
if (firstChild && firstChild.nodeValue !== right.value) {
// Needed for IE. Apparently IE sets the placeholder as the
// node value and vise versa. This ignores an empty update.
if (right.value === '' && firstChild.nodeValue === left.placeholder) {
return
}
firstChild.nodeValue = right.value
}
}
function morphEvents (left, right) {
const { node, eventNames } = getEvents(right)
const leftNode = left.__handlers || left
const leftRemove = (name) => left.removeEventListener(name, leftNode[name])
eventNames.forEach((name) => {
if (node[name]) {
leftRemove(name)
left.addEventListener(name, right[name])
}
if (leftNode[name]) {
leftRemove(name)
}
})
}
/* eslint-disable max-statements, fp/no-loops */
function getEvents (node) {
if (node.__handlers) {
return { node, eventNames: Object.keys(node.__handlers) }
}
const eventNames = []
node = node.__handlers || node
for (let key in node) {
if (key.startsWith('on')) {
eventNames.push(key)
}
}
return { node, eventNames }
}
function morphChildren (newNode, oldNode) {
let oldChild, newChild, morphed, oldMatch
// The offset is only ever increased, and used for [i - offset] in the loop
let offset = 0
for (let i = 0; ; i++) {
oldChild = oldNode.childNodes[i]
newChild = newNode.childNodes[i - offset]
// Both nodes are empty, do nothing
if (!oldChild && !newChild) {
break
// There is no new child, remove old
} else if (!newChild) {
oldNode.removeChild(oldChild)
i--
// There is no old child, add new
} else if (!oldChild) {
oldNode.appendChild(newChild)
offset++
// Both nodes are the same, morph
} else if (same(newChild, oldChild)) {
morphed = morph(oldChild, newChild)
if (morphed && morphed !== oldChild) {
oldNode.replaceChild(morphed, oldChild)
offset++
}
// Both nodes do not share an ID or a placeholder, try reorder
} else {
oldMatch = null
// Try and find a similar node somewhere in the tree
for (let j = i; j < oldNode.childNodes.length; j++) {
if (same(oldNode.childNodes[j], newChild)) {
oldMatch = oldNode.childNodes[j]
break
}
}
// If there was a node with the same ID or placeholder in the old list
if (oldMatch) {
morphed = morph(oldMatch, newChild)
if (morphed !== oldMatch) offset++
oldNode.insertBefore(morphed, oldChild)
// It's safe to morph two nodes in-place if neither has an ID
} else if (!newChild.id && !oldChild.id) {
morphed = morph(oldChild, newChild)
if (morphed && morphed !== oldChild) {
oldNode.replaceChild(morphed, oldChild)
offset++
}
// Insert the node at the index if we couldn't morph or find a matching node
} else {
oldNode.insertBefore(newChild, oldChild)
offset++
}
}
}
}
function same (a, b) {
if (a.nodeType === 3) return a.nodeValue === b.nodeValue
if (a.isSameNode) return a.isSameNode(b)
if (a.attributes.id) return a.attributes.id === b.attributes.id
if (a.nodeName !== b.nodeName) return false
return false
}
all is damn is working. around 1.1kb min+gz
nanomorph tests - all passing w/o 1
const test = require('tape')
const hyperx = require('hyperx')
// eslint-disable-next-line
// require('minda/require')
// eslint-disable-next-line
let h = require('./packages/minscript/dest')
let morph = require('./packages/minmorph/dest')
const { renderToString } = require('./render')
const html = hyperx(h)
test('root level', (t) => {
t.test('should return right if not left', (t) => {
t.plan(1)
let a = html`<p></p>`
let b = html`<p><span>hello world</span></p>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should replace a node', (t) => {
t.plan(1)
let a = html`<p>hello world</p>`
let b = html`<div>hello world</div>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should morph a node', (t) => {
t.plan(1)
let a = html`<p>hello world</p>`
let b = html`<p>hello you</p>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should morph a node with namespaced attribute', (t) => {
t.plan(1)
let a = html`<svg><use xlink:href="#heybooboo"></use></svg>`
let b = html`<svg><use xlink:href="#boobear"></use></svg>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should ignore if node is same', (t) => {
t.plan(1)
let a = html`<p>hello world</p>`
let expected = renderToString(a)
let actual = renderToString(morph(a, a))
t.strictEqual(actual, expected, 'result was expected')
})
})
test('nested', (t) => {
t.test('should replace a tag node', (t) => {
t.plan(1)
let a = html`<main><p>hello world</p></main>`
let b = html`<main><div>hello world</div></main>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should replace a text node', (t) => {
t.plan(1)
let a = html`<main><p>hello world</p></main>`
let b = html`<main><p>hello you</p></main>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should replace a node', (t) => {
t.plan(1)
let a = html`<main><p>hello world</p></main>`
let expected = renderToString(a)
let actual = renderToString(morph(a, a))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should append a node', (t) => {
t.plan(1)
let a = html`<main></main>`
let b = html`<main><p>hello you</p></main>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should remove a node', (t) => {
t.plan(1)
let a = html`<main><p>hello you</p></main>`
let b = html`<main></main>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
})
test('events', (t) => {
t.test('should copy onclick events', (t) => {
t.plan(1)
function fail (e) {
console.log('xxx')
// t.fail('should not be called')
}
function pass (e) {
console.log('zzzzzz')
t.ok('called')
}
let a = html`<button onclick=${fail}>OLD</button>`
let b = html`<button>NEWx</button>`
let res = morph(a, b)
console.log(res.childNodes[0])
res.onclick && res.onclick()
a = html`<button>OLD</button>`
b = html`<button onclick=${pass}>NEW</button>`
res = morph(a, b)
res.onclick()
})
})
test('values', (t) => {
t.test('if new tree has no value and old tree does, remove value', (t) => {
t.plan(2)
let a = html`<input type="text" value="howdy" />`
let b = html`<input type="text" />`
let res = morph(a, b)
t.strictEqual(res.value, undefined)
a = html`<input type="text" value="howdy" />`
b = html`<input type="text" value=${null} />`
res = morph(a, b)
t.strictEqual(res.value, undefined)
})
/* eslint-disable max-statements */
t.test(
'if new tree has value and old tree does too, set value from new tree',
(t) => {
t.plan(4)
let a = html`<input type="text" value="howdy" />`
let b = html`<input type="text" value="hi" />`
let res = morph(a, b)
t.strictEqual(res.value, 'hi')
a = html`<input type="text"/>`
a.value = 'howdy'
b = html`<input type="text"/>`
b.value = 'hi'
res = morph(a, b)
t.strictEqual(res.value, 'hi')
a = html`<input type="text" value="howdy"/>`
b = html`<input type="text"/>`
b.value = 'hi'
res = morph(a, b)
t.strictEqual(res.value, 'hi')
a = html`<input type="text"/>`
a.value = 'howdy'
b = html`<input type="text" value="hi"/>`
res = morph(a, b)
t.strictEqual(res.value, 'hi')
}
)
/* eslint-enable max-statements */
})
test('isSameNode', (t) => {
t.test('should return a if true', (t) => {
t.plan(1)
let a = html`<div>YOLO</div>`
let b = html`<div>FOMO</div>`
b.isSameNode = function (el) {
return true
}
let res = morph(a, b)
t.strictEqual(res.childNodes[0].value, 'YOLO')
})
t.test('should return b if false', (t) => {
t.plan(1)
let a = html`<div>YOLO</div>`
let b = html`<div>FOMO</div>`
b.isSameNode = function (el) {
return false
}
let res = morph(a, b)
t.strictEqual(res.childNodes[0].value, 'FOMO')
})
})
test('lists', (t) => {
t.test('should append nodes', (t) => {
t.plan(1)
let a = html`<ul></ul>`
let b = html`<ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should remove nodes', (t) => {
t.plan(1)
let a = html`<ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>`
let b = html`<ul></ul>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
})
test('selectables', (t) => {
t.test('should append nodes', (t) => {
t.plan(1)
let a = html`<select></select>`
let b = html`<select><option>1</option><option>2</option><option>3</option><option>4</option></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should append nodes (including optgroups)', (t) => {
t.plan(1)
let a = html`<select></select>`
let b = html`<select><optgroup><option>1</option><option>2</option></optgroup><option>3</option><option>4</option></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should remove nodes', (t) => {
t.plan(1)
let a = html`<select><option>1</option><option>2</option><option>3</option><option>4</option></select>`
let b = html`<select></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should remove nodes (including optgroups)', (t) => {
t.plan(1)
let a = html`<select><optgroup><option>1</option><option>2</option></optgroup><option>3</option><option>4</option></select>`
let b = html`<select></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should add selected', (t) => {
t.plan(1)
let a = html`<select><option>1</option><option>2</option></select>`
let b = html`<select><option>1</option><option selected>2</option></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should add selected (xhtml)', (t) => {
t.plan(1)
let a = html`<select><option>1</option><option>2</option></select>`
let b = html`<select><option>1</option><option selected="selected">2</option></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
t.test('should switch selected', (t) => {
t.plan(1)
let a = html`<select><option selected="selected">1</option><option>2</option></select>`
let b = html`<select><option>1</option><option selected="selected">2</option></select>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
})
test('should replace nodes', (t) => {
t.plan(1)
let a = html`<ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>`
let b = html`<ul><div>1</div><li>2</li><p>3</p><li>4</li><li>5</li></ul>`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected, 'result was expected')
})
test('should replace nodes after multiple iterations', (t) => {
t.plan(2)
let a = html`<ul></ul>`
let b = html`<ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li></ul>`
let expected = renderToString(b)
a = morph(a, b)
t.strictEqual(renderToString(a), expected, 'result was expected')
b = html`<ul><div>1</div><li>2</li><p>3</p><li>4</li><li>5</li></ul>`
expected = renderToString(b)
let c = morph(a, b)
t.strictEqual(renderToString(c), expected, 'result was expected')
})
test('use id as a key hint', (t) => {
t.test('append an element', (t) => {
let a = html`<ul><li id="a"></li><li id="b"></li><li id="c"></li></ul>`
let b = html`<ul><li id="a"></li><li id="new"></li><li id="b"></li><li id="c"></li></ul>`
let expected = renderToString(b)
let oldFirst = a.childNodes[0]
let oldSecond = a.childNodes[1]
let oldThird = a.childNodes[2]
let c = morph(a, b)
t.deepEqual(oldFirst, c.childNodes[0], 'first is equal')
t.deepEqual(oldSecond, c.childNodes[1], 'moved second is equal')
t.deepEqual(oldThird, c.childNodes[2], 'moved third is equal')
t.strictEqual(renderToString(c), expected)
t.end()
})
t.test('handle non-id elements', (t) => {
let a = html`<ul>
<li></li>
<li id="a"></li>
<li id="b"></li>
<li id="c"></li>
<li></li>
</ul>`
let b = html`<ul>
<li></li>
<li id="a"></li>
<li id="new"></li>
<li id="b"></li>
<li id="c"></li>
<li></li>
</ul>`
let expected = renderToString(b)
let oldSecond = a.childNodes[2]
let oldThird = a.childNodes[4]
let oldForth = a.childNodes[6]
let c = morph(a, b)
t.deepEqual(oldSecond, c.childNodes[2], 'second is equal')
t.deepEqual(oldThird, c.childNodes[4], 'moved third is equal')
t.deepEqual(oldForth, c.childNodes[6], 'moved forth is equal')
t.strictEqual(renderToString(c), expected)
t.end()
})
t.test('copy over children', (t) => {
let a = html`<section>'hello'</section>`
let b = html`<section><div>hi</div></section>`
let expected = renderToString(b)
let c = morph(a, b)
t.strictEqual(renderToString(c), expected, expected)
t.end()
})
t.test('remove an element', (t) => {
let a = html`<ul><li id="a"></li><li id="b"></li><li id="c"></li></ul>`
let b = html`<ul><li id="a"></li><li id="c"></li></ul>`
let oldFirst = a.childNodes[0]
let oldThird = a.childNodes[2]
let expected = renderToString(b)
let c = morph(a, b)
t.deepEqual(c.childNodes[0], oldFirst, 'first is equal')
t.deepEqual(c.childNodes[1], oldThird, 'second untouched')
t.strictEqual(renderToString(c), expected)
t.end()
})
t.test('swap proxy elements', (t) => {
let nodeA = html`<li id="a"></li>`
let placeholderA = html`<div id="a" data-placeholder=true></div>`
placeholderA.isSameNode = function (el) {
return el === nodeA
}
let nodeB = html`<li id="b"></li>`
let placeholderB = html`<div id="b" data-placeholder=true></div>`
placeholderB.isSameNode = function (el) {
return el === nodeB
}
let a = html`<ul>${nodeA}${nodeB}</ul>`
let b = html`<ul>${placeholderB}${placeholderA}</ul>`
let c = morph(a, b)
t.deepEqual(c.childNodes[0], nodeB, 'c.childNodes[0] === nodeB')
t.deepEqual(c.childNodes[1], nodeA, 'c.childNodes[1] === nodeA')
t.end()
})
t.test('id match still morphs', (t) => {
let a = html`<li id="12">FOO</li>`
let b = html`<li id="12">BAR</li>`
let target = renderToString(b)
let c = morph(a, b)
t.strictEqual(renderToString(c), target)
t.end()
})
t.test('remove orphaned keyed nodes', (t) => {
let a = html`
<div>
<div>1</div>
<li id="a">a</li>
</div>
`
let b = html`
<div>
<div>2</div>
<li id="b">b</li>
</div>
`
let expected = renderToString(b)
let actual = renderToString(morph(a, b))
t.strictEqual(actual, expected)
t.end()
})
t.test('whitespace', (t) => {
let a = html`<ul>
</ul>`
let b = html`<ul><li></li><li></li>
</ul>`
let expected = b
let actual = morph(a, b)
t.deepEqual(actual, expected)
t.end()
})
t.test('nested with id', (t) => {
let child = html`<div id="child"></div>`
let placeholder = html`<div id="child"></div>`
placeholder.isSameNode = function (el) {
return el === child
}
let a = html`<div><div id="parent">${child}</div></div>`
let b = html`<div><div id="parent">${placeholder}</div></div>`
let c = morph(a, b)
t.deepEqual(c.childNodes[0].childNodes[0], child, 'is the same node')
t.end()
})
t.skip('nested without id', (t) => {
let child = html`<div id="child">child</div>`
let placeholder = html`<div id="child">placeholder</div>`
placeholder.isSameNode = function (el) {
return el === child
}
let a = html`<div><div>${child}</div></div>`
let b = html`<div><div>${placeholder}</div></div>`
let c = morph(a, b)
t.deepEqual(c.childNodes[0].childNodes[0], child, 'is the same node')
t.end()
})
})
update: rewritten again, performance is amazing and the benchmarks against current nanomorph is absolutely phenomenal!
/* eslint-disable no-param-reassign, no-multi-assign, no-plusplus, max-lines */
/**
* Helpers
*/
const NO_SUFFIX = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i
function isSameTextNodes (left, right) {
return isTextNode(left) && isTextNode(right) && isEqualText(left, right)
}
function isTextNode (val) {
return val && val.nodeType === 3
}
function isEqualText (left, right) {
return right.nodeValue === left.nodeValue
}
function decamelize (str) {
return str
.replace(/([a-z\d])([A-Z])/g, '$1-$2')
.replace(/([A-Z]+)([A-Z][a-z\d]+)/g, '$1-$2')
.toLowerCase()
}
/**
* left: old node
* right: new node
*/
module.exports = patch
function patch (left, right) {
if (!left) {
return right
} else if (!right) {
return null
} else if (left.nodeName !== right.nodeName) {
return right
} else if (isTextNode(right) && !isEqualText(left, right)) {
return right
} else if (
(right.isSameNode && right.isSameNode(left)) ||
isSameTextNodes(left, right)
) {
return left
}
// probably not needed
// if (isTextNode(left) && isTextNode(right) && isEqualText(left, right)) {
// return left
// }
const props = {}
diff(left, right, props)
morphChilds(left, right)
return left
}
/**
* Here we are sure that `left` and `right` exist and
* they are the same tag names `<div>` and `<div>`.
*
* @param {*} left
* @param {*} right
*/
function diff (left, right, props) {
// if Element Node
if (right.nodeType === 1) {
morphProps(left, right, props)
}
// if Text Nodes
if (right.nodeType === 3) {
left.nodeValue = right.nodeValue
}
if (left.nodeName === 'INPUT') {
updateInput(left, right)
}
if (left.nodeName === 'OPTION') {
updateProp(left, right, 'selected')
}
if (left.nodeName === 'TEXTAREA') {
updateTextarea(left, right)
}
return left
}
function morphProps (left, right, props) {
for (let j = 0; j < left.attributes.length; j++) {
const attr = left.attributes[j]
props[attr.name] = {
name: attr.name,
value: attr.value,
ns: attr.namespaceURI,
}
}
for (let i = 0; i < right.attributes.length; i++) {
const attrNode = right.attributes[i]
const attrValue = attrNode.value
const attrName = attrNode.name
// important: always `null` by default, or actual namespace!
// so just use it instead of checking and using both
// the `(set|get|has)AttributeNS` and `(set|get|has)Attribute` methods.
const ns = attrNode.namespaceURI
morphAttribute({ left, right }, props, {
ns,
attrName,
attrValue,
attrNode,
})
}
// eslint-disable-next-line
for (const name in props) {
const oldAttr = props[name]
if (!right.attributes[name]) {
left.removeAttributeNS(oldAttr.ns, name)
}
}
}
/**
* Important notes from here on:
* - `.nodeName` is always uppercase, no matter of the browser?!
*
* @param {any} { left, right }
* @param {any} props
* @param {any} opts
*/
function morphAttribute ({ left, right }, props, opts) {
if (opts.attrName === 'style') {
updateStyle({ left, right }, props, opts)
} else {
updateAttribute({ left }, props, opts)
}
}
/**
*
*
* @param {any} { left, right }
* @param {any} props
* @param {any} opts
*/
function updateStyle ({ left, right }, props, opts) {
const { attrName, attrValue, attrNode } = opts
// hint: `attrName` is "style"
if (!(attrValue && props[attrName].value !== right.style.cssText)) return
/* eslint-disable no-param-reassign */
if (typeof attrValue === 'object') {
let cssText = ''
/* eslint-disable no-restricted-syntax, guard-for-in */
for (const k in attrValue) {
let val = attrValue[k]
// if the value of the style property, e.g.
// <div style={{ fontSize: 12 }} /> and div style={{ fontSize: 55 }} />
// values 12 and 55 are not equal, right?
if (left.style[k] !== val) {
const suffix = typeof val === 'number' && NO_SUFFIX.test(k) === false
val = suffix ? `${val}px` : val
cssText += `${decamelize(k)}:${val};`
left.style[k] = val
left.style.cssText = attrNode.value = cssText
// left.attributes.style is `Attribute` object too!!
// so the `.value` is same as `left.style.cssText`.
// Update the cache of oldProps a.k.a `props` here of `left`
left.attributes.style.value = props[attrName].value = cssText
}
}
} else {
left.style.cssText = String(attrValue)
}
}
function updateAttribute ({ left }, props, opts) {
const { attrName, attrValue, ns } = opts
const oldProp = props[attrName]
const hasIn = attrName in props
if (!hasIn) {
setAttr({ left }, props, opts)
}
if (hasIn && oldProp.value !== attrValue) {
if (attrValue === 'null' || attrValue === 'undefined') {
left.removeAttributeNS(ns, attrName)
delete props[attrName] // eslint-disable-line no-param-reassign
} else {
setAttr({ left }, props, opts)
}
}
}
function setAttr ({ left }, props, opts) {
const { attrName, attrValue, ns } = opts
left.setAttributeNS(ns, attrName, attrValue)
props[attrName] = props[attrName] || {}
props[attrName].value = attrValue
props[attrName].ns = ns
return left
}
function updateInput (left, right) {
updateProp(left, right, 'value')
updateProp(left, right, 'checked')
updateProp(left, right, 'disabled')
if (right.value === 'null') {
left.removeAttribute('value')
left.value = ''
}
if (!right.hasAttributeNS(null, 'value')) {
left.removeAttribute('value')
} else if (left.type === 'range') {
// this is so elements like slider move their UI thingy
left.value = right.value
}
}
function updateProp (left, right, name) {
if (left[name] !== right[name]) {
left[name] = right[name]
// we don't use setAttribute and removeAttribute
// because we already did that
// in the `morphProps -> morphAttribute -> updateAttribute` step
if (right[name]) {
left.setAttribute(name, '')
} else {
left.removeAttribute(name)
// delete left[name]
}
}
}
function updateTextarea (left, right) {
updateProp(left, right, 'value')
const firstChild = left.childNodes[0]
if (firstChild && firstChild.nodeValue !== right.value) {
// Needed for IE. Apparently IE sets the placeholder as the
// node value and vise versa. This ignores an empty update.
if (right.value === '' && firstChild.nodeValue === left.placeholder) {
return
}
firstChild.nodeValue = right.value
}
}
/* eslint-disable max-statements, max-depth */
function getKey (node) {
if (!node) return null
return node.key || (node.attributes && node.attributes.key) || node.id
}
function morphChilds (left, right) {
let morphed = null
let offset = 0
const oldLen = left.childNodes.length
const newLen = right.childNodes.length
const fragment = left.ownerDocument.createDocumentFragment()
// const keyed = {}
// for (let i = 0; i < oldLen; i++) {
// const oldChild = left.childNodes[i]
// const oldKey = getKey(oldChild)
// if (oldKey) {
// keyed[oldKey] = oldChild
// }
// }
for (let j = 0; j <= newLen; j++) {
if (j === newLen) {
left.appendChild(fragment)
break
}
const oldChild = left.childNodes[j]
const newChild = right.childNodes[j - offset]
if (!oldChild && !newChild) {
break
} else if (!oldChild && newChild) {
fragment.appendChild(newChild)
offset++
} else if (oldChild && !newChild) {
left.removeChild(oldChild)
} else if (isTextNode(oldChild) && isTextNode(newChild)) {
if (!isEqualText(oldChild, newChild)) {
left.replaceChild(newChild, oldChild)
}
} else {
morphed = patch(oldChild, newChild)
if (morphed && morphed !== oldChild) {
left.replaceChild(morphed, oldChild)
offset++
}
}
}
if (newLen === 0) {
const range = left.ownerDocument.createRange()
range.selectNodeContents(left)
range.deleteContents()
}
}
// left: old node / right: new node
// function same (left, right) {
// if (!right || !left) return false
// if (right.id) return right.id === left.id
// // if (right.nodeType === 3) return right.nodeValue === left.nodeValue
// if (right.isSameNode) return right.isSameNode(left)
// if (right.nodeName === left.nodeName) return true
// return false
// }
The 1.1kb library.
https://www.webpackbin.com/bins/-L3NghOrCy3F_f8TxWM8 (updated 21 Jan 2018)
Initially it was the idea behind
mich
project and themich-h
package. But that's a bit different and all of the names are simple and free.minmorph
It is almost exact copy of the
nanomorph
, but a lot simplified and a lot smaller, which passnanomorph
tests.mindom
It is almost the same as
undom
, but is more small (a bit smaller) for only needed things of our purpose, but also exposes things to be extended. It is pending discussionundom
to support some kind of plugins and etc. But still it won't be my thing, unless it just provide access to each class.Not to mention that the size don't matter in my case, since it is only needed in server side, because
minscript
andminmorph
works perfectly in the browser.