Last active
January 12, 2016 22:20
-
-
Save mrjjwright/7e0ea57ea2ab5bf936db to your computer and use it in GitHub Desktop.
Make vidom components reactive using mobservable
This file contains hidden or 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
(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