Skip to content

Instantly share code, notes, and snippets.

@thomaswilburn
Last active July 3, 2018 22:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomaswilburn/76311d1f8296f03102629e6ac7ab800f to your computer and use it in GitHub Desktop.
Save thomaswilburn/76311d1f8296f03102629e6ac7ab800f to your computer and use it in GitHub Desktop.
One-way databinding
/*
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