Last active
July 3, 2018 22:42
-
-
Save thomaswilburn/76311d1f8296f03102629e6ac7ab800f to your computer and use it in GitHub Desktop.
One-way databinding
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
/* | |
A library for wiring a state object up to HTML | |
- [data-bound] = Sets the innerHTML of the element | |
- [:attributeName] or [attr:attributeName] = Sets the value of attributeName | |
- [class:className] = Toggles className based on truthiness | |
- [on:event] = Bind event listeners to this element | |
It's like an unholy fusion of Vue and Backbone. Call set() on the state object to trigger a render. | |
*/ | |
var $ = require("./lib/qsa"); | |
// attribute prefixes | |
var prefix = { | |
attr: /^(attr)?:/, | |
className: /^class:/, | |
event: /^on:/ | |
}; | |
// used to bind attributes to camelcase props | |
var upcase = { | |
innerhtml: "innerHTML" | |
}; | |
// utility for deep object references | |
var getPath = function(path, target) { | |
var parts = path.split("."); | |
for (var i = 0; i < parts.length; i++) { | |
var part = parts[i]; | |
if (!target[part]) return undefined; | |
target = target[part]; | |
} | |
return target; | |
}; | |
var setPath = function(path, target, value) { | |
var parts = path.split("."); | |
var final = parts.pop(); | |
parts.forEach(p => target = target[p] ? target[p] : target[p] = {}); | |
target[final] = value; | |
}; | |
// constructors for rendering callbacks | |
var factories = { | |
contents: function(element) { | |
return v => element.innerHTML = v; | |
}, | |
attr: function(element, attribute) { | |
attribute = upcase[attribute] || attribute; | |
if (typeof element[attribute] != "undefined") { | |
return v => element[attribute] = v; | |
} else { | |
return v => element.setAttribute(attribute, v); | |
} | |
}, | |
classToggle: function(element, className) { | |
return v => element.classList[v ? "add" : "remove"](className); | |
} | |
} | |
var createBinding = function(root, state) { | |
// callbacks are stored under a key specifying the state value lookup path | |
// each is a function expecting a single value, with their context memoized | |
var callbacks = {}; | |
var addCallback = function(k, v) { | |
if (!callbacks[k]) callbacks[k] = []; | |
callbacks[k].push(v); | |
} | |
// bind all innerHTML references | |
$(`[data-bound]`, root).forEach(el => addCallback(el.getAttribute("data-bound"), factories.contents(el))); | |
// search for binding attributes on all elements and connect them | |
var all = $("*", root); | |
all.push(root); | |
all.forEach(function(element) { | |
for (var i = 0; i < element.attributes.length; i++) { | |
var attr = element.attributes[i]; | |
if (attr.name.match(prefix.attr)) { | |
var k = attr.name.replace(prefix.attr, ""); | |
var v = attr.value; | |
addCallback(v, factories.attr(element, k)); | |
} | |
if (attr.name.match(prefix.className)) { | |
var k = attr.name.replace(prefix.className, ""); | |
var v = attr.value; | |
addCallback(v, factories.classToggle(element, k)); | |
} | |
if (attr.name.match(prefix.event)) { | |
var e = attr.name.replace(prefix.event, ""); | |
var v = attr.value; | |
// wrapper in case the state object changes or is updated | |
var listener = function(event) { | |
if (!state[v]) return; | |
state[v].call(element, event); | |
} | |
element.addEventListener(e, listener); | |
} | |
} | |
}); | |
// render is always delayed until the next tick | |
var scheduled = null; | |
var render = function() { | |
if (scheduled) return; | |
scheduled = requestAnimationFrame(function() { | |
scheduled = null; | |
for (var k in callbacks) { | |
var v = getPath(k, state); | |
callbacks[k].forEach(fn => fn(v)); | |
} | |
}); | |
}; | |
// call state.set() with a key/value pair or an object | |
// deep keypaths are supported as keys in order to do narrow replacement | |
Object.defineProperty(state, "set", { | |
value: function(key, value) { | |
if (typeof key == "object") { | |
return Object.keys(key).forEach(k => state.set(k, key[k])); | |
} | |
setPath(key, state, value); | |
render(); | |
} | |
}); | |
render(); | |
return state; | |
}; | |
module.exports = createBinding; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment