Skip to content

Instantly share code, notes, and snippets.

@lewisjb
Last active May 17, 2021 12:25
Show Gist options
  • Save lewisjb/39bad069ed305f2804bbb64b6db8b8b9 to your computer and use it in GitHub Desktop.
Save lewisjb/39bad069ed305f2804bbb64b6db8b8b9 to your computer and use it in GitHub Desktop.
Minimal JS MVVM
/*
* min.js
*
* By lewisjb - 19/1/17
*
* github/lewisjb
* lewisjb.com
*
* ----------------------------------------------------------------------------
* This is meant to be a minimal MVVM written in JS.
* The goals were to make it usable, minimal, and
* not require transpiling.
*
* The usage was heavily inspired by Knockout.js
* ----------------------------------------------------------------------------
*
* JS Usage:
* ----------------------------------------------------------------------------
* $min.o(val) | Creates an observable
* | Getting - `var()` where `var` is an observable
* | Setting - `var(newVal)`
* $min.c(fn) | Creates a computed
* | Getting - `var()` where `var` is a computed
* $min.scope(s, e) | Applies scope `s` (obj/fn) to element `e`
* var.sub(fn) | Adds `fn` as a subscriber to `var` (where var is ob/comp)
* ----------------------------------------------------------------------------
*
* HTML Usage:
* ----------------------------------------------------------------------------
* data-b='<JSON>' | Binds the data from the JSON to the element
* | e.g.
* | <input type="text" data-b='{"value": "var"}' />
* | Now when `var` in the scope changes, so will this
* | element's value.
* | The keys of the JSON correspond to the JS
* | keys/attributes of the element, and the values
* | are the names of the variables in the scope.
* | Nested JSON is supported.
* |
* | For two-way bindings, the key needs "<->" in it.
* | e.g.
* | <input type="text" data-b='{"value<->onchange": "v"}' />
* | The name on the left side is the getter/setter, and
* | the name on the right side is the hook to know
* | when to update the variable.
*-----------------------------------------------------------------------------
*/
$min = {};
$min.__get_dep = function(args, stack_) {
var stack = stack_ || [];
var c = args.callee.caller;
if(stack.indexOf(c) !== -1) { return null; }
if(!c || c.__c) { return c; }
return $min.__get_dep(c.arguments, stack.concat([c]));
};
$min.__gen_base = function(val) {
var me = {val: val, subs: [], depped: []};
me.sub = function(fn) { me.subs.push({__update: fn}); };
me._unsub = function(o) {
var i = me.subs.indexOf(o);
if(i !== -1) { me.subs.splice(i, 1) };
};
me._ping = function(newVal, stack) {
for(var i = 0; i < me.subs.length; i++) {
me.subs[i].__update(newVal, stack);
}
};
me.__add_dep = function(args) {
var dep = $min.__get_dep(args);
if(dep && dep.__c) {
if(me.subs.indexOf(dep.__c) === -1) { me.subs.push(dep.__c); }
if(dep.__c.depped.indexOf(me) === -1) { dep.__c.depped.push(me); }
}
};
return me;
};
$min.o = function(val) {
var me = $min.__gen_base(val);
var out = function() {
me.__add_dep(arguments);
if(arguments.length > 0 && arguments[0] != me.val) {
me.val = arguments[0];
me._ping(me.val, [me]);
}
return me.val;
};
out.__o = me;
out.sub = me.sub;
out.mutated = function() { me._ping(me.val, [me]); };
return out;
};
$min.c = function(fn) {
var me = $min.__gen_base(fn);
me.__call = function() {
var old = me.depped; me.depped = [];
var v = me.val();
for(var i = 0; i < old.length; i++) {
if(me.depped.indexOf(old[i]) === -1) { old[i]._unsub(me); }
}
return v;
};
me.__update = function(v, _stack) {
var stack = _stack || [];
if(stack.indexOf(me) !== -1) { return; }
var res = me.__call();
me._ping(res, stack.concat([me]));
};
me.__update.__c = me; // So the dependency tracker knows this is a dep
var out = function() {
me.__add_dep(arguments);
return me.__call();
}
out.__c = me;
out.sub = me.sub;
out();
return out;
};
$min.scope = function(scope, element) {
scope = (typeof scope === "function") ? scope() : scope;
var b = element.querySelectorAll('[data-b]');
for(var i = 0; i < b.length; i++) {
var e = b[i];
var info = JSON.parse(e.dataset.b);
function apply(k, v, e) {
if(typeof v === "object") {
var keys = Object.keys(v);
for(var i = 0; i < keys.length; i++) {
apply(keys[i], v[keys[i]], (k) ? e[k] : e);
}
} else {
var obj = scope[v] || v, val = obj;
if(obj.__c || obj.__o) {
if(k.indexOf("<->") !== -1) {
var fn = function() { obj(e.value); };
e[k.split('<->')[1]] = fn;
k = k.split('<->')[0];
}
obj.sub(function(newVal) { e[k] = newVal; });
val = obj();
}
e[k] = val;
}
}
apply(null, info, e);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment