ractive-ractive
Version:
Ractive adaptor for Ractive objects
232 lines (187 loc) • 5.5 kB
JavaScript
/* global define, Ractive */
void (function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
Ractive.adaptors.Ractive = factory(root.Ractive);
}
}(this, function () {
var Adaptor = {
filter: filter,
wrap: wrap
};
/*
* Advanced options:
* You can adjust these settings via `Ractive.adaptors.Ractive.maxKeyLength`
* and so on. There's usually no need to do that, but it may be good for
* optimizing tests.
*/
Adaptor.fireWrapEvents = true;
Adaptor.maxKeyLength = 2048;
/*
* Check if the child is an Ractive instance.
*
* Also, if this key has been wrapped before, don't rewrap it. (Happens on
* deeply-nested values, and .reset() for some reason.)
*/
function filter (child, keypath, parent) {
if (!isRactiveInstance(child)) return false;
if (parent &&
parent._ractiveWraps &&
parent._ractiveWraps[keypath]) {
return false;
}
return true;
}
/*
* Global write lock.
* This prevents infinite loops from happening where a parent will set a
* value on the child, and the child will attempt to write back to the
* parent, and so on.
*/
var locked = Adaptor.locked = {};
function lock (key, fn) {
if (locked[key]) return;
try {
locked[key] = true;
return fn();
} finally {
delete locked[key];
}
}
/*
* Returns a wrapped Adaptor for Ractive.
* See: http://docs.ractivejs.org/latest/writing-adaptor-plugins
*/
function wrap (parent, child, keypath, prefixer) {
setup();
return {
get: get,
set: set,
reset: reset,
teardown: teardown
};
/*
* Initializes the adaptor.
*/
function setup () {
checkForRecursion();
markAsWrapped();
propagateSubinstances();
child.on('change', onChange);
if (Adaptor.fireWrapEvents) {
child.fire('wrap', parent, keypath);
parent.fire('wrapchild', child, keypath);
}
}
/*
* If the child has its own Ractive instances, recurse upwards. This
* will do `parent.set('child.grandchild', instance)` so that the
* `parent` can listen to the grandchild.
*/
function propagateSubinstances () {
var re = {};
each(child.get(), function (val, key) {
if (isRactiveInstance(val)) re[key] = val;
});
parent.set(prefixer(re));
}
function teardown () {
delete parent._ractiveWraps[keypath];
child.off('change', onChange);
if (Adaptor.fireWrapEvents) {
child.fire('unwrap', parent, keypath);
parent.fire('unwrapchild', child, keypath);
}
}
/*
* Propagate changes from child to parent.
* We well break it apart into key/vals and set those individually because
* some values may be locked.
*/
function onChange (updates) {
each(updates, function (value, key) {
lock(child._guid + key, function () {
parent.set(keypath + '.' + key, value);
});
});
}
/*
* Returns all attributes of the child, including computed properties.
* See: https://github.com/ractivejs/ractive/issues/1250
*/
function get () {
// Optimization: if there are no computed properties, returning all
// non-computed data should suffice.
if (!child.computed) return child.get();
var re = {};
each(child.get(), function (val, key) {
re[key] = val;
});
each(child.computed, function (_, key) {
if (typeof re[key] === 'undefined') {
re[key] = child.get(key);
}
});
return re;
}
function set (key, value) {
lock(child._guid + key, function () {
child.set(key, value);
});
}
/*
* Allow setting values by passing a POJO to .set(), for instance,
* `.set('child', { ... })`. If anything else is passed onto .set()
* (like another Ractive instance, or another adaptor'able), destroy
* this wrapper.
*/
function reset (object) {
if (object && object.constructor === Object) {
child.set(object);
} else {
return false;
}
}
/*
* Die on recursion.
* Keypath will look like 'child.sub.parent.child.sub.parent' ad nauseum.
*/
function checkForRecursion () {
if (keypath && keypath.length > Adaptor.maxKeyLength) {
throw new Error('Keypath too long (possible circular dependency)');
}
}
/*
* Let future wrappers know what we have wrapped Ractive instances.
* This value is used on `filter()`.
*/
function markAsWrapped () {
if (!parent._ractiveWraps) parent._ractiveWraps = {};
parent._ractiveWraps[keypath] = child;
}
}
/*
* Cross-browser forEach helper.
*/
function each (obj, fn) {
for (var key in obj) {
if (obj.hasOwnProperty(key)) fn(obj[key], key);
}
}
/*
* Check if an `obj instanceof Ractive`. This check will not require a
* reference to the root Ractive instance.
*/
function isRactiveInstance (obj) {
return obj && obj.constructor &&
typeof obj._guid === 'string' &&
typeof obj.set === 'function' &&
typeof obj.off === 'function' &&
typeof obj.on === 'function' &&
typeof obj.constructor.defaults === 'object';
}
return Adaptor;
}));