Model.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. var Model = Class.extend(EmitterMixin, ListenerMixin, {
  2. _props: null,
  3. _watchers: null,
  4. _globalWatchArgs: null,
  5. constructor: function() {
  6. this._watchers = {};
  7. this._props = {};
  8. this.applyGlobalWatchers();
  9. this.constructed();
  10. },
  11. // useful for monkeypatching. TODO: BaseClass?
  12. constructed: function() {
  13. },
  14. applyGlobalWatchers: function() {
  15. var argSets = this._globalWatchArgs || [];
  16. var i;
  17. for (i = 0; i < argSets.length; i++) {
  18. this.watch.apply(this, argSets[i]);
  19. }
  20. },
  21. has: function(name) {
  22. return name in this._props;
  23. },
  24. get: function(name) {
  25. if (name === undefined) {
  26. return this._props;
  27. }
  28. return this._props[name];
  29. },
  30. set: function(name, val) {
  31. var newProps;
  32. if (typeof name === 'string') {
  33. newProps = {};
  34. newProps[name] = val === undefined ? null : val;
  35. }
  36. else {
  37. newProps = name;
  38. }
  39. this.setProps(newProps);
  40. },
  41. reset: function(newProps) {
  42. var oldProps = this._props;
  43. var changeset = {}; // will have undefined's to signal unsets
  44. var name;
  45. for (name in oldProps) {
  46. changeset[name] = undefined;
  47. }
  48. for (name in newProps) {
  49. changeset[name] = newProps[name];
  50. }
  51. this.setProps(changeset);
  52. },
  53. unset: function(name) { // accepts a string or array of strings
  54. var newProps = {};
  55. var names;
  56. var i;
  57. if (typeof name === 'string') {
  58. names = [ name ];
  59. }
  60. else {
  61. names = name;
  62. }
  63. for (i = 0; i < names.length; i++) {
  64. newProps[names[i]] = undefined;
  65. }
  66. this.setProps(newProps);
  67. },
  68. setProps: function(newProps) {
  69. var changedProps = {};
  70. var changedCnt = 0;
  71. var name, val;
  72. for (name in newProps) {
  73. val = newProps[name];
  74. // a change in value?
  75. // if an object, don't check equality, because might have been mutated internally.
  76. // TODO: eventually enforce immutability.
  77. if (
  78. typeof val === 'object' ||
  79. val !== this._props[name]
  80. ) {
  81. changedProps[name] = val;
  82. changedCnt++;
  83. }
  84. }
  85. if (changedCnt) {
  86. this.trigger('before:batchChange', changedProps);
  87. for (name in changedProps) {
  88. val = changedProps[name];
  89. this.trigger('before:change', name, val);
  90. this.trigger('before:change:' + name, val);
  91. }
  92. for (name in changedProps) {
  93. val = changedProps[name];
  94. if (val === undefined) {
  95. delete this._props[name];
  96. }
  97. else {
  98. this._props[name] = val;
  99. }
  100. this.trigger('change:' + name, val);
  101. this.trigger('change', name, val);
  102. }
  103. this.trigger('batchChange', changedProps);
  104. }
  105. },
  106. watch: function(name, depList, startFunc, stopFunc) {
  107. var _this = this;
  108. this.unwatch(name);
  109. this._watchers[name] = this._watchDeps(depList, function(deps) {
  110. var res = startFunc.call(_this, deps);
  111. if (res && res.then) {
  112. _this.unset(name); // put in an unset state while resolving
  113. res.then(function(val) {
  114. _this.set(name, val);
  115. });
  116. }
  117. else {
  118. _this.set(name, res);
  119. }
  120. }, function(deps) {
  121. _this.unset(name);
  122. if (stopFunc) {
  123. stopFunc.call(_this, deps);
  124. }
  125. });
  126. },
  127. unwatch: function(name) {
  128. var watcher = this._watchers[name];
  129. if (watcher) {
  130. delete this._watchers[name];
  131. watcher.teardown();
  132. }
  133. },
  134. _watchDeps: function(depList, startFunc, stopFunc) {
  135. var _this = this;
  136. var queuedChangeCnt = 0;
  137. var depCnt = depList.length;
  138. var satisfyCnt = 0;
  139. var values = {}; // what's passed as the `deps` arguments
  140. var bindTuples = []; // array of [ eventName, handlerFunc ] arrays
  141. var isCallingStop = false;
  142. function onBeforeDepChange(depName, val, isOptional) {
  143. queuedChangeCnt++;
  144. if (queuedChangeCnt === 1) { // first change to cause a "stop" ?
  145. if (satisfyCnt === depCnt) { // all deps previously satisfied?
  146. isCallingStop = true;
  147. stopFunc(values);
  148. isCallingStop = false;
  149. }
  150. }
  151. }
  152. function onDepChange(depName, val, isOptional) {
  153. if (val === undefined) { // unsetting a value?
  154. // required dependency that was previously set?
  155. if (!isOptional && values[depName] !== undefined) {
  156. satisfyCnt--;
  157. }
  158. delete values[depName];
  159. }
  160. else { // setting a value?
  161. // required dependency that was previously unset?
  162. if (!isOptional && values[depName] === undefined) {
  163. satisfyCnt++;
  164. }
  165. values[depName] = val;
  166. }
  167. queuedChangeCnt--;
  168. if (!queuedChangeCnt) { // last change to cause a "start"?
  169. // now finally satisfied or satisfied all along?
  170. if (satisfyCnt === depCnt) {
  171. // if the stopFunc initiated another value change, ignore it.
  172. // it will be processed by another change event anyway.
  173. if (!isCallingStop) {
  174. startFunc(values);
  175. }
  176. }
  177. }
  178. }
  179. // intercept for .on() that remembers handlers
  180. function bind(eventName, handler) {
  181. _this.on(eventName, handler);
  182. bindTuples.push([ eventName, handler ]);
  183. }
  184. // listen to dependency changes
  185. depList.forEach(function(depName) {
  186. var isOptional = false;
  187. if (depName.charAt(0) === '?') { // TODO: more DRY
  188. depName = depName.substring(1);
  189. isOptional = true;
  190. }
  191. bind('before:change:' + depName, function(val) {
  192. onBeforeDepChange(depName, val, isOptional);
  193. });
  194. bind('change:' + depName, function(val) {
  195. onDepChange(depName, val, isOptional);
  196. });
  197. });
  198. // process current dependency values
  199. depList.forEach(function(depName) {
  200. var isOptional = false;
  201. if (depName.charAt(0) === '?') { // TODO: more DRY
  202. depName = depName.substring(1);
  203. isOptional = true;
  204. }
  205. if (_this.has(depName)) {
  206. values[depName] = _this.get(depName);
  207. satisfyCnt++;
  208. }
  209. else if (isOptional) {
  210. satisfyCnt++;
  211. }
  212. });
  213. // initially satisfied
  214. if (satisfyCnt === depCnt) {
  215. startFunc(values);
  216. }
  217. return {
  218. teardown: function() {
  219. // remove all handlers
  220. for (var i = 0; i < bindTuples.length; i++) {
  221. _this.off(bindTuples[i][0], bindTuples[i][1]);
  222. }
  223. bindTuples = null;
  224. // was satisfied, so call stopFunc
  225. if (satisfyCnt === depCnt) {
  226. stopFunc();
  227. }
  228. },
  229. flash: function() {
  230. if (satisfyCnt === depCnt) {
  231. stopFunc();
  232. startFunc(values);
  233. }
  234. }
  235. };
  236. },
  237. flash: function(name) {
  238. var watcher = this._watchers[name];
  239. if (watcher) {
  240. watcher.flash();
  241. }
  242. }
  243. });
  244. Model.watch = function(/* same arguments as this.watch() */) {
  245. // creates new array every time, to not mess with subclass prototypes
  246. // TODO: make more efficient
  247. this.prototype._globalWatchArgs = (this.prototype._globalWatchArgs || []).concat(arguments);
  248. };
  249. FC.Model = Model;