Skip to content

Instantly share code, notes, and snippets.

@mrjjwright
Last active January 12, 2016 22:20
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrjjwright/7e0ea57ea2ab5bf936db to your computer and use it in GitHub Desktop.
Save mrjjwright/7e0ea57ea2ab5bf936db to your computer and use it in GitHub Desktop.
Make vidom components reactive using mobservable
(function() {
function mrFactory(mobservable, vidom) {
if (!mobservable)
throw new Error("mobservable-vidom requires the Mobservable package.")
if (!vidom)
throw new Error("mobservable-vidom requires vidom to be available");
var isTracking = false;
// WeakMap<Node, Object>;
var componentByNodeRegistery = typeof WeakMap !== "undefined" ? new WeakMap() : undefined;
var renderReporter = new mobservable.extras.SimpleEventEmitter();
function reportRendering(component) {
var node = component.getDomNode();
if (node)
componentByNodeRegistery.set(node, component);
renderReporter.emit({
event: 'render',
renderTime: component.__$mobRenderEnd - component.__$mobRenderStart,
totalTime: Date.now() - component.__$mobRenderStart,
component: component,
node: node
});
}
var reactiveMixin = {
onInit: function() {
var baseRender = this.onRender;
this.__$mobDependencies = [];
this.onRender = function(attrs, children) {
if (isTracking)
this.__$mobRenderStart = Date.now();
// invoke the old render function and in the mean time track all dependencies using
// 'autorun'.
// when the dependencies change, the function is triggered, but we don't want to
// rerender because that would ignore the normal Vidom lifecycle,
// so instead we dispose the current observer and trigger a update.
var hasRendered = false;
var self = this;
var rendering;
this.__$mobDisposer = mobservable.autorun(function reactiveRender() {
if (!hasRendered) {
hasRendered = true;
mobservable.extras.withStrict(true, function() {
rendering = baseRender.call(self, attrs, children);
});
} else {
self.__$mobDisposer();
vidom.Component.prototype.update.call(self);
}
}, this);
// make sure views are not disposed between the clean-up of the observer and the next render
// (invoked through vidom update)
this.$mobservable = this.__$mobDisposer.$mobservable;
var newDependencies = this.$mobservable.observing.map(function(dep) {
dep.setRefCount(+1);
return dep;
});
this.__$mobDependencies.forEach(function(dep) {
dep.setRefCount(-1);
});
this.__$mobDependencies = newDependencies;
if (isTracking)
this.__$mobRenderEnd = Date.now();
return rendering;
}
},
onUnmount: function() {
this.__$mobDisposer && this.__$mobDisposer();
this.__$mobDependencies.forEach(function(dep) {
dep.setRefCount(-1);
});
delete this.$mobservable;
if (isTracking) {
var node = this.getDomNode();
if (node) {
componentByNodeRegistery.delete(node);
renderReporter.emit({
event: 'destroy',
component: this,
node: node
});
}
}
},
onMount: function() {
if (isTracking)
reportRendering(this);
},
onUpdate: function() {
if (isTracking)
reportRendering(this);
},
shouldUpdate: function(attrs, prevAttrs) {
// update on any state changes (as is the default)
if (this.attrs !== prevAttrs)
return true;
// update if props are shallowly not equal, inspired by PureRenderMixin
var keys = Object.keys(prevAttrs);
var key;
if (keys.length !== Object.keys(attrs).length)
return true;
for(var i = keys.length -1; i >= 0, key = keys[i]; i--) {
var newValue = attrs[key];
if (newValue !== prevAttrs[key]) {
return true;
} else if (newValue && typeof newValue === "object" && !mobservable.isReactive(newValue)) {
/**
* If the newValue is still the same object, but that object is not reactive,
* fallback to the default React behavior: update, because the object *might* have changed.
* If you need the non default behavior, just use the React pure render mixin, as that one
* will work fine with mobservable as well, instead of the default implementation of
* observer.
*/
return true;
}
}
return false;
}
}
function patch(target, funcName) {
var base = target[funcName];
var mixinFunc = reactiveMixin[funcName];
target[funcName] = function() {
base && base.apply(this, arguments);
mixinFunc.apply(this, arguments);
}
}
function observer(componentClass) {
// If it is function but doesn't seem to be a vidom class constructor,
// wrap it to a vidom class automatically
if (typeof componentClass === "function" && !componentClass.prototype.onRender && !vidom.Component.isPrototypeOf(componentClass)) {
console.log("Wrapping class as observer");
return observer(vidom.createComponent({
onRender: function(attrs, children) {
var c = componentClass.call(this);
if (attrs != undefined) {
c.attrs(attrs);
}
if (children != undefined && children.length) {
c.children(children);
};
return c;
}
}));
}
if (!componentClass)
throw new Error("Please pass a valid component to 'observer'");
var target = componentClass.prototype || componentClass;
[
"onInit",
"onMount",
"onUnmount",
"onUpdate",
].forEach(function(funcName) {
patch(target, funcName)
});
if (!target.shouldComponentUpdate)
target.shouldUpdate = reactiveMixin.shouldUpdate;
return componentClass;
}
function trackComponents() {
if (typeof WeakMap === "undefined")
throw new Error("tracking components is not supported in this browser");
if (!isTracking)
isTracking = true;
}
return ({
observer: observer,
renderReporter: renderReporter,
componentByNodeRegistery: componentByNodeRegistery,
trackComponents: trackComponents
});
}
// UMD
if (typeof define === 'function' && define.amd) {
define('mobservable-vidom', ['mobservable', 'vidom'/* or native */], mrFactory);
} else if (typeof exports === 'object') {
module.exports = mrFactory(require('mobservable'), require('vidom'/* or native */));
} else {
window.mobservableVidom = mrFactory(this['mobservable'], this['vidom'] );
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment