Explorar el Código

new model base class

Adam Shaw hace 8 años
padre
commit
0481b9c5dd
Se han modificado 3 ficheros con 492 adiciones y 0 borrados
  1. 1 0
      src.json
  2. 187 0
      src/common/Model.js
  3. 304 0
      tests/automated-better/Model.js

+ 1 - 0
src.json

@@ -6,6 +6,7 @@
     "moment-ext.js",
     "date-formatting.js",
     "common/Class.js",
+    "common/Model.js",
     "common/Promise.js",
     "common/TaskQueue.js",
     "common/EmitterMixin.js",

+ 187 - 0
src/common/Model.js

@@ -0,0 +1,187 @@
+
+var Model = Class.extend(EmitterMixin, ListenerMixin, {
+
+	_props: null,
+	_watchTeardowns: null,
+	_globalWatchArgs: null,
+
+	constructor: function() {
+		this._watchTeardowns = {};
+		this._props = {};
+		this.applyGlobalWatchers();
+	},
+
+	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;
+	},
+
+	set: function(props, val) {
+		var name;
+
+		if (typeof props === 'object') {
+			for (name in props) {
+				this._setProp(name, props[name]);
+			}
+		}
+		else {
+			this._setProp(props, val);
+		}
+	},
+
+	_setProp: function(name, val) {
+		if (val === undefined) {
+			val = null;
+		}
+
+		if (val !== this._props[name]) { // if not set, will be undefined, and will be truthy
+			this._props[name] = val;
+			this.trigger('change:' + name, val);
+		}
+	},
+
+	unset: function(name) {
+		if (this.has(name)) {
+			delete this._props[name];
+			this.trigger('change:' + name, undefined);
+		}
+	},
+
+	get: function(name) {
+		return this._props[name];
+	},
+
+	watch: function(name, depList, startFunc, stopFunc) {
+		var _this = this;
+
+		this.unwatch(name);
+
+		this._watchTeardowns[name] = this._watchDeps(depList, function(deps) {
+			var res = startFunc.call(_this, deps);
+
+			if (res && res.then) {
+				res.then(function(val) {
+					_this.set(name, val);
+				});
+			}
+			else {
+				_this.set(name, res);
+			}
+		}, function() {
+			_this.unset(name);
+
+			if (stopFunc) {
+				stopFunc.call(_this);
+			}
+		});
+	},
+
+	unwatch: function(name) {
+		var teardown = this._watchTeardowns[name];
+
+		if (teardown) {
+			teardown();
+		}
+	},
+
+	_watchDeps: function(depList, startFunc, stopFunc) {
+		var _this = this;
+		var len = depList.length;
+		var satisfyCnt = 0;
+		var values = {}; // what's passed as the `deps` arguments
+		var watchMap = {};
+
+		function reportUpdate(depName, val) {
+
+			if (val !== undefined) { // set
+
+				if (!(depName in values)) { // not previously set
+					values[depName] = val;
+					satisfyCnt++;
+
+					if (satisfyCnt === len) { // finally now satisfied
+						startFunc(values);
+					}
+				}
+				else { // was previously set
+					if (satisfyCnt === len) { // was satisfied
+						stopFunc();
+						values[depName] = val;
+						startFunc(values);
+					}
+					else {
+						values[depName] = val;
+					}
+				}
+			}
+			else { // unset
+
+				if (depName in values) { // previously set
+					delete values[depName];
+					satisfyCnt--;
+
+					if (satisfyCnt === len - 1) { // was previously satisfied
+						stopFunc();
+					}
+				}
+				// else, not previously set. who cares.
+			}
+		}
+
+		depList.forEach(function(depName) {
+			var onChange = function(val) {
+				reportUpdate(depName, val);
+			};
+
+			_this.on('change:' + depName, onChange);
+			watchMap[depName] = onChange;
+		});
+
+		if (!len) { // no deps, so resolve immediately
+			startFunc(values);
+		}
+		else {
+			depList.forEach(function(depName) {
+				if (_this.has(depName)) {
+					reportUpdate(depName, _this.get(depName));
+				}
+			});
+		}
+
+		return function() { // teardown
+			var depName;
+
+			for (depName in watchMap) {
+				_this.off('change:' + name, watchMap[name]);
+			}
+
+			if (satisfyCnt === len) { // was satisfied
+				stopFunc();
+			}
+		};
+	}
+
+});
+
+
+Model.watch = function(/* same arguments as this.watch() */) {
+	var proto = this.prototype;
+
+	if (!proto._globalWatchArgs) {
+		proto._globalWatchArgs = [];
+	}
+
+	proto._globalWatchArgs.push(arguments);
+};
+
+
+FC.Model = Model;
+

+ 304 - 0
tests/automated-better/Model.js

@@ -0,0 +1,304 @@
+
+describe('Model', function() {
+	var Model = $.fullCalendar.Model;
+
+	describe('set/get', function() {
+
+		it('retrieves a previously set value', function() {
+			var funcs = {
+				change: function(val) {
+					expect(val).toBe(5);
+				}
+			};
+			var spy = spyOn(funcs, 'change').and.callThrough();
+
+			var m = new Model();
+			m.on('change:myvar', funcs.change);
+			m.set('myvar', 5);
+
+			expect(m.get('myvar')).toBe(5);
+			expect(spy).toHaveBeenCalled();
+		});
+
+		it('retrieves values previously set in bulk', function() {
+			var m = new Model();
+			m.set({
+				myvar: 5,
+				myothervar: 6
+			});
+			expect(m.get('myvar')).toBe(5);
+			expect(m.get('myothervar')).toBe(6);
+		});
+
+		it('retrieves undefined when not previously set', function() {
+			var m = new Model();
+			expect(m.get('myvar')).toBeUndefined();
+		});
+	});
+
+	describe('has', function() {
+
+		it('reports false when not previously set', function() {
+			var m = new Model();
+			expect(m.has('myvar')).toBe(false);
+		});
+
+		it('reports true when previously set', function() {
+			var m = new Model();
+			m.set('myvar', 5);
+			expect(m.has('myvar')).toBe(true);
+		});
+	});
+
+	describe('watch', function() {
+
+		describe('when called as a task', function() {
+
+			describe('when no deps', function() {
+				it('resolves immediately', function() {
+					var funcs = {
+						start: function() { }
+					};
+					var spy = spyOn(funcs, 'start');
+					var m = new Model();
+					m.watch('myid', [], funcs.start);
+					expect(spy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+				});
+			});
+
+			describe('when all deps already satisfied', function() {
+				it('resolves immediately', function() {
+					var funcs = {
+						start: function() { }
+					};
+					var spy = spyOn(funcs, 'start');
+
+					var m = new Model();
+					m.set({
+						myvar: 5,
+						myothervar: 6
+					});
+
+					m.watch('myid', [ 'myvar', 'myothervar' ], funcs.start);
+					expect(spy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+				});
+			});
+
+			describe('when not all deps satisfied', function() {
+				it('resolves only after all deps are set', function() {
+					var funcs = {
+						start: function() { }
+					};
+					var spy = spyOn(funcs, 'start');
+
+					var m = new Model();
+					m.set('myvar', 5);
+
+					m.watch('myid', [ 'myvar', 'myothervar' ], funcs.start);
+					expect(spy).not.toHaveBeenCalled();
+					expect(m.has('myid')).toBe(false);
+
+					m.set('myothervar', 6);
+					expect(spy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+				});
+			});
+
+			describe('when previously resolved dep is unset', function() {
+				it('calls the stop function', function() {
+					var funcs = {
+						start: function() { },
+						stop: function() { }
+					};
+					var startSpy = spyOn(funcs, 'start');
+					var stopSpy = spyOn(funcs, 'stop');
+
+					var m = new Model();
+					m.set('myvar', 5);
+
+					m.watch('myid', [ 'myvar', 'myothervar' ], funcs.start, funcs.stop);
+
+					m.set('myothervar', 6);
+					expect(startSpy).toHaveBeenCalled();
+					expect(stopSpy).not.toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+
+					m.unset('myothervar');
+					expect(stopSpy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(false);
+				});
+			});
+		});
+
+		describe('when called as a computed value', function() {
+
+			describe('when it has no deps', function() {
+				it('resolves immediately', function() {
+					var funcs = {
+						generator: function() {
+							return 9;
+						},
+						change: function(val) {
+							expect(val).toBe(9);
+						}
+					};
+					var generatorSpy = spyOn(funcs, 'generator').and.callThrough();
+					var changeSpy = spyOn(funcs, 'change').and.callThrough();
+
+					var m = new Model();
+					m.on('change:myid', funcs.change);
+					m.watch('myid', [], funcs.generator);
+
+					expect(generatorSpy).toHaveBeenCalled();
+					expect(changeSpy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+					expect(m.get('myid')).toBe(9);
+				});
+			});
+
+			describe('when all deps already satisfied', function() {
+				it('resolves immediately', function() {
+					var funcs = {
+						generator: function(deps) {
+							return deps.myvar + deps.myothervar;
+						}
+					};
+					var spy = spyOn(funcs, 'generator').and.callThrough();
+
+					var m = new Model();
+					m.set({
+						myvar: 5,
+						myothervar: 6
+					});
+
+					m.watch('myid', [ 'myvar', 'myothervar' ], funcs.generator);
+					expect(spy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+					expect(m.get('myid')).toBe(11);
+				});
+			});
+
+			describe('when not all deps satisfied', function() {
+				it('resolves only after all deps are set', function() {
+					var funcs = {
+						generator: function(deps) {
+							return deps.myvar + deps.myothervar;
+						}
+					};
+					var spy = spyOn(funcs, 'generator').and.callThrough();
+
+					var m = new Model();
+					m.set('myvar', 5);
+
+					m.watch('myid', [ 'myvar', 'myothervar' ], funcs.generator);
+					expect(spy).not.toHaveBeenCalled();
+					expect(m.has('myid')).toBe(false);
+
+					m.set('myothervar', 6);
+					expect(spy).toHaveBeenCalled();
+					expect(m.has('myid')).toBe(true);
+					expect(m.get('myid')).toBe(11);
+				});
+
+				describe('when watching an instance', function() {
+
+					it('resolves only after final async dep resolves', function(done) {
+						var funcs = {
+							generator: function(deps) {
+								return deps.myvar + deps.myothervar;
+							}
+						};
+						var spy = spyOn(funcs, 'generator').and.callThrough();
+
+						var m = new Model();
+						m.set('myvar', 5);
+						m.watch('myothervar', [ 'myvar' ], function(deps) {
+							var deferred = $.Deferred();
+							setTimeout(function() {
+								deferred.resolve(deps.myvar * 2);
+							}, 100);
+							return deferred.promise();
+						});
+
+						m.watch('myid', [ 'myvar', 'myothervar' ], funcs.generator);
+						expect(spy).not.toHaveBeenCalled();
+						expect(m.has('myid')).toBe(false);
+
+						setTimeout(function() {
+							expect(spy).toHaveBeenCalled();
+							expect(m.has('myid')).toBe(true);
+							expect(m.get('myid')).toBe(15);
+							done();
+						}, 200);
+					});
+				});
+
+				describe('when using class-methods', function() {
+
+					it('resolves only after final async dep resolves', function(done) {
+						var funcs = {
+							generator: function(deps) {
+								return deps.myvar + deps.myothervar;
+							}
+						};
+						var spy = spyOn(funcs, 'generator').and.callThrough();
+
+						var MyClass = Model.extend();
+						MyClass.watch('myothervar', [ 'myvar' ], function(deps) {
+							var deferred = $.Deferred();
+							setTimeout(function() {
+								deferred.resolve(deps.myvar * 2);
+							}, 100);
+							return deferred.promise();
+						});
+						MyClass.watch('myid', [ 'myvar', 'myothervar' ], funcs.generator);
+
+						var m = new MyClass();
+						m.set({
+							myvar: 5
+						});
+
+						expect(spy).not.toHaveBeenCalled();
+						expect(m.has('myid')).toBe(false);
+
+						setTimeout(function() {
+							expect(spy).toHaveBeenCalled();
+							expect(m.has('myid')).toBe(true);
+							expect(m.get('myid')).toBe(15);
+							done();
+						}, 200);
+					});
+				});
+			});
+		});
+	});
+
+	describe('unwatch', function() {
+		it('calls the stop function and won\'t call the start function again', function() {
+			var funcs = {
+				start: function() { },
+				stop: function() { }
+			};
+			var startSpy = spyOn(funcs, 'start');
+			var stopSpy = spyOn(funcs, 'stop');
+
+			var m = new Model();
+			m.set('myvar', 5);
+
+			m.watch('myid', [ 'myvar', 'myothervar' ], funcs.start, funcs.stop);
+			expect(startSpy).not.toHaveBeenCalled();
+
+			m.set('myothervar', 6);
+			expect(startSpy).toHaveBeenCalledTimes(1);
+
+			m.unwatch('myid');
+			expect(stopSpy).toHaveBeenCalledTimes(1);
+
+			// doesn't call it again
+			m.set('myvar', 5);
+			expect(startSpy).toHaveBeenCalledTimes(1);
+		});
+	});
+});