| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- var Model = Class.extend(EmitterMixin, ListenerMixin, {
- _props: null,
- _watchers: null,
- _globalWatchArgs: null,
- constructor: function() {
- this._watchers = {};
- this._props = {};
- this.applyGlobalWatchers();
- this.constructed();
- },
- // useful for monkeypatching. TODO: BaseClass?
- constructed: function() {
- },
- applyGlobalWatchers: function() {
- var argSets = this._globalWatchArgs || [];
- var i;
- for (i = 0; i < argSets.length; i++) {
- this.watch.apply(this, argSets[i]);
- }
- },
- has: function(name) {
- return name in this._props;
- },
- get: function(name) {
- if (name === undefined) {
- return this._props;
- }
- return this._props[name];
- },
- set: function(name, val) {
- var newProps;
- if (typeof name === 'string') {
- newProps = {};
- newProps[name] = val === undefined ? null : val;
- }
- else {
- newProps = name;
- }
- this.setProps(newProps);
- },
- reset: function(newProps) {
- var oldProps = this._props;
- var changeset = {}; // will have undefined's to signal unsets
- var name;
- for (name in oldProps) {
- changeset[name] = undefined;
- }
- for (name in newProps) {
- changeset[name] = newProps[name];
- }
- this.setProps(changeset);
- },
- unset: function(name) { // accepts a string or array of strings
- var newProps = {};
- var names;
- var i;
- if (typeof name === 'string') {
- names = [ name ];
- }
- else {
- names = name;
- }
- for (i = 0; i < names.length; i++) {
- newProps[names[i]] = undefined;
- }
- this.setProps(newProps);
- },
- setProps: function(newProps) {
- var changedProps = {};
- var changedCnt = 0;
- var name, val;
- for (name in newProps) {
- val = newProps[name];
- // a change in value?
- // if an object, don't check equality, because might have been mutated internally.
- // TODO: eventually enforce immutability.
- if (
- typeof val === 'object' ||
- val !== this._props[name]
- ) {
- changedProps[name] = val;
- changedCnt++;
- }
- }
- if (changedCnt) {
- this.trigger('before:batchChange', changedProps);
- for (name in changedProps) {
- val = changedProps[name];
- this.trigger('before:change', name, val);
- this.trigger('before:change:' + name, val);
- }
- for (name in changedProps) {
- val = changedProps[name];
- if (val === undefined) {
- delete this._props[name];
- }
- else {
- this._props[name] = val;
- }
- this.trigger('change:' + name, val);
- this.trigger('change', name, val);
- }
- this.trigger('batchChange', changedProps);
- }
- },
- watch: function(name, depList, startFunc, stopFunc) {
- var _this = this;
- this.unwatch(name);
- this._watchers[name] = this._watchDeps(depList, function(deps) {
- var res = startFunc.call(_this, deps);
- if (res && res.then) {
- _this.unset(name); // put in an unset state while resolving
- res.then(function(val) {
- _this.set(name, val);
- });
- }
- else {
- _this.set(name, res);
- }
- }, function(deps) {
- _this.unset(name);
- if (stopFunc) {
- stopFunc.call(_this, deps);
- }
- });
- },
- unwatch: function(name) {
- var watcher = this._watchers[name];
- if (watcher) {
- delete this._watchers[name];
- watcher.teardown();
- }
- },
- _watchDeps: function(depList, startFunc, stopFunc) {
- var _this = this;
- var queuedChangeCnt = 0;
- var depCnt = depList.length;
- var satisfyCnt = 0;
- var values = {}; // what's passed as the `deps` arguments
- var bindTuples = []; // array of [ eventName, handlerFunc ] arrays
- var isCallingStop = false;
- function onBeforeDepChange(depName, val, isOptional) {
- queuedChangeCnt++;
- if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
- if (satisfyCnt === depCnt) { // all deps previously satisfied?
- isCallingStop = true;
- stopFunc(values);
- isCallingStop = false;
- }
- }
- }
- function onDepChange(depName, val, isOptional) {
- if (val === undefined) { // unsetting a value?
- // required dependency that was previously set?
- if (!isOptional && values[depName] !== undefined) {
- satisfyCnt--;
- }
- delete values[depName];
- }
- else { // setting a value?
- // required dependency that was previously unset?
- if (!isOptional && values[depName] === undefined) {
- satisfyCnt++;
- }
- values[depName] = val;
- }
- queuedChangeCnt--;
- if (!queuedChangeCnt) { // last change to cause a "start"?
- // now finally satisfied or satisfied all along?
- if (satisfyCnt === depCnt) {
- // if the stopFunc initiated another value change, ignore it.
- // it will be processed by another change event anyway.
- if (!isCallingStop) {
- startFunc(values);
- }
- }
- }
- }
- // intercept for .on() that remembers handlers
- function bind(eventName, handler) {
- _this.on(eventName, handler);
- bindTuples.push([ eventName, handler ]);
- }
- // listen to dependency changes
- depList.forEach(function(depName) {
- var isOptional = false;
- if (depName.charAt(0) === '?') { // TODO: more DRY
- depName = depName.substring(1);
- isOptional = true;
- }
- bind('before:change:' + depName, function(val) {
- onBeforeDepChange(depName, val, isOptional);
- });
- bind('change:' + depName, function(val) {
- onDepChange(depName, val, isOptional);
- });
- });
- // process current dependency values
- depList.forEach(function(depName) {
- var isOptional = false;
- if (depName.charAt(0) === '?') { // TODO: more DRY
- depName = depName.substring(1);
- isOptional = true;
- }
- if (_this.has(depName)) {
- values[depName] = _this.get(depName);
- satisfyCnt++;
- }
- else if (isOptional) {
- satisfyCnt++;
- }
- });
- // initially satisfied
- if (satisfyCnt === depCnt) {
- startFunc(values);
- }
- return {
- teardown: function() {
- // remove all handlers
- for (var i = 0; i < bindTuples.length; i++) {
- _this.off(bindTuples[i][0], bindTuples[i][1]);
- }
- bindTuples = null;
- // was satisfied, so call stopFunc
- if (satisfyCnt === depCnt) {
- stopFunc();
- }
- },
- flash: function() {
- if (satisfyCnt === depCnt) {
- stopFunc();
- startFunc(values);
- }
- }
- };
- },
- flash: function(name) {
- var watcher = this._watchers[name];
- if (watcher) {
- watcher.flash();
- }
- }
- });
- Model.watch = function(/* same arguments as this.watch() */) {
- // creates new array every time, to not mess with subclass prototypes
- // TODO: make more efficient
- this.prototype._globalWatchArgs = (this.prototype._globalWatchArgs || []).concat(arguments);
- };
- FC.Model = Model;
|