Last active
July 21, 2017 11:30
-
-
Save GnsP/5ebc4fb6866a90a9f7f4f5a96406c7dc to your computer and use it in GitHub Desktop.
VirtualDOM implementation in Ironscript
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
_sync "VDOM : The Virtual DOM implementation in Ironscript" | |
(_include "stdlib") | |
[defun h (type props :children) { | |
[.props = props] | |
[parse-type-string = @{ | |
var type = args[0]; | |
var idDelim = args[1]; | |
var classDelim = args[2]; | |
if (!idDelim) idDelim = '#'; | |
if (!classDelim) classDelim = '.'; | |
function parseClass (str) { | |
var tarr = str.split(classDelim); | |
var type = "div"; | |
if (classDelim !== str[0]) type = tarr[0]; | |
return [type, tarr.slice(1).join(' ')]; | |
} | |
function parseId (str) { | |
var tarr = str.split(idDelim); | |
if (idDelim === str[0]) return ["div", "", tarr[1]]; | |
else if (tarr.length > 1) { | |
let ret = parseClass(tarr[0]); | |
ret.push(tarr[1]); | |
return ret; | |
} | |
else return parseClass(tarr[0]); | |
} | |
$return (parseId(type)); | |
}@ ] | |
[t = (parse-type-string type) ] | |
[if ["object" === (typeof t)] | |
then (_begin | |
[.type = t.0] | |
[props.class = t.1] | |
[props.id = t.2] | |
) | |
else [.type = t] | |
] | |
[if children [.children = (_seq :children)] ] | |
} ] | |
[_rho (H @tag) (h @tag {}) ] | |
[_rho (H @tag @props) (h @tag @props) ] | |
[_rho (H @tag @props :@children) (h @tag @props :@children) ] | |
[_rho (T @tag :@children) (h @tag {} :@children) ] | |
[_rho (T @tag) (h @tag {}) ] | |
[COMPONENTS = {}] | |
[NOOP = (_fn () NIL) ] | |
[PORT = (_fn () (_stream NOOP NIL) ) ] | |
[defun vnode (node id) (_begin | |
[type = node.type] | |
[if type | |
then (_begin | |
[componentfn = (_dot COMPONENTS type)] | |
[if componentfn | |
then (_begin | |
[nd = (componentfn node.props)] | |
[if node.props.id then (vnode nd node.props.id) else (vnode nd ".")] | |
) | |
else { | |
[.type = type] | |
[.props = node.props] | |
[if .props.id then [.id = .props.id] else [.id = "."] ] | |
[if node.children | |
[.children = (_map node.children (_fn (child index) | |
(vnode child (concat .id "." index) ) | |
) ) ] | |
] | |
} | |
] | |
) | |
else node | |
] | |
)] | |
[EVENTS = { | |
[init = @{ | |
var capt = true; | |
if (args[0] === "false") capt = false; | |
("keypress keydown keyup click dbclick scroll focus blur change search select submit input invalid".split(" ")) | |
.forEach (function (e) { | |
document.body.addEventListener(e, function (ev) {$yield(ev); ev.stopPropagation();}, capt) ; | |
}); | |
}@ ] | |
[.event-stream = (_stream init "true")] | |
[listeners = {}] | |
[defun .listen (id event port) | |
[(_dot listeners (concat id ":" event) ) = port] | |
] | |
[defun .mute (id event) [(concat id ":" event) = NIL] ] | |
[defun .remove (id) ( @{ | |
var obj = args[0]; | |
var id = args[1]; | |
Object.keys(obj).forEach(function (name) { | |
if (name.startsWith(id+'.') || name.startsWith(id+':')) obj[name] = undefined; | |
}); | |
}@ listeners id ) ] | |
(_do (_on .event-stream | |
(_begin | |
[e = (_pull .event-stream)] | |
[port = (_dot listeners (concat e.target.id ":" e.type ) ) ] | |
[if port (_push port e)] | |
) | |
) ) | |
} ] | |
[update-fn-internal = (_fx ( | |
@{ | |
function isEventProp (name) { return /^on/.test(name); } | |
function extractEventName (name) { return name.slice(2).toLowerCase(); } | |
function isCustomProp (name) { | |
if (name === "id") return true; | |
if (isEventProp(name)) return true; | |
return false; | |
} | |
function setBooleanProp ($target, name, value) { | |
if (value) $target.setAttribute(name, value); | |
$target[name] = value; | |
} | |
function setProp ($target, name, value) { | |
if (isCustomProp(name)) return; | |
else if (typeof value === 'boolean') setBooleanProp ($target, name, value); | |
else $target.setAttribute (name, value); | |
} | |
function setProps ($target, props) { | |
if (typeof props === 'object') Object.keys(props).forEach(function (name) { | |
setProp($target, name, props[name]); | |
}); | |
} | |
function removeBooleanProp ($target, name) { | |
$target.removeAttribute (name); | |
$target[name] = false; | |
} | |
function removeProp ($target, name, value) { | |
if (isCustomProp(name)) return; | |
else if (typeof value === 'boolean') removeBooleanProp ($target, name); | |
else $target.removeAttribute (name); | |
} | |
function updateProp ($target, name, newVal, oldVal) { | |
if (!newVal) removeProp ($target, name, oldVal); | |
else if (!oldval || newVal !== oldVal) setProp ($target, name, newVal); | |
else return; | |
} | |
function updateProps ($target, newProps, oldProps) { | |
if (!oldProps) oldProps = {}; | |
var props = Object.assign ({}, newProps, oldProps); | |
Object.keys(props).forEach (function (name) { | |
updateProp ($target, name, newProps[name], oldProps[name]); | |
}); | |
} | |
function addEventListeners (id, props) { | |
if (typeof props === 'object') Object.keys(props).forEach(function (name) { | |
if (isEventProp(name)) addPatch.push({id: id, type: extractEventName(name), port: props[name]}); | |
}); | |
} | |
function createElement (node) { | |
if (typeof node === 'string') return document.createTextNode(node); | |
var $el = document.createElement (node.type); | |
$el.setAttribute("id", node.id); | |
setProps ($el, node.props); | |
addEventListeners(node.id, node.props); | |
if (node.children) node.children.arr.map(createElement).forEach($el.appendChild.bind($el)); | |
return $el; | |
} | |
function changed (node1, node2) { | |
return typeof node1 !== typeof node2 | |
|| typeof node1 === 'string' && node1 !== node2 | |
|| node1.type !== node2.type | |
|| node1.id !== node2.id; | |
} | |
function Patch () { | |
this.arr = []; | |
this.push = function (x) {this.arr.push(x); }.bind(this); | |
} | |
var addPatch = []; | |
var removePatch = []; | |
function updateElement ($par, newNode, oldNode, index=0) { | |
if (!oldNode) { | |
$par.appendChild (createElement (newNode)); | |
} | |
else if (!newNode) { | |
removePatch.push(oldNode.id); | |
$par.removeChild($par.childNodes[index]); | |
} | |
else if (changed(newNode, oldNode)) { | |
removePatch.push(oldNode.id); | |
$par.replaceChild(createElement(newNode), $par.childNodes[index]); | |
} | |
else if (newNode.type) { | |
updateProps($par.childNodes[index], newNode.props, oldNode.props); | |
var newLen = newNode.children.arr.length; | |
var oldLen = oldNode.children.arr.length; | |
for (var i=0; i<newLen || i<oldLen; i++) { | |
updateElement ($par.childNodes[index], newNode.children.arr[i], oldNode.children.arr[i], i); | |
} | |
} | |
} | |
function update (id, newNode, oldNode) { | |
var $par = document.getElementById (id); | |
addPatch = []; | |
removePatch = []; | |
updateElement ($par, newNode, oldNode); | |
return {add:addPatch, remove:removePatch}; | |
} | |
$return (update); | |
}@ ) ) ] | |
[defun update (id newNode oldNode) | |
(_begin | |
[vOldNode = [if oldNode then (vnode oldNode) else NIL]] | |
[vNewNode = [if newNode then (vnode newNode) else NIL]] | |
[patches = (update-fn-internal id vNewNode vOldNode ) ] | |
(_map patches.remove (_fn (item) (EVENTS.remove item) ) ) | |
(_map patches.add (_fn (item) (EVENTS.listen item.id item.type item.port) ) ) | |
) | |
] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment