knockout-es5.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. /**
  2. * @license
  3. * Knockout ES5 plugin - https://github.com/SteveSanderson/knockout-es5
  4. * Copyright (c) Steve Sanderson
  5. * MIT license
  6. */
  7. define(function() {
  8. 'use strict';
  9. var OBSERVABLES_PROPERTY = '__knockoutObservables';
  10. var SUBSCRIBABLE_PROPERTY = '__knockoutSubscribable';
  11. // Model tracking
  12. // --------------
  13. //
  14. // This is the central feature of Knockout-ES5. We augment model objects by converting properties
  15. // into ES5 getter/setter pairs that read/write an underlying Knockout observable. This means you can
  16. // use plain JavaScript syntax to read/write the property while still getting the full benefits of
  17. // Knockout's automatic dependency detection and notification triggering.
  18. //
  19. // For comparison, here's Knockout ES3-compatible syntax:
  20. //
  21. // var firstNameLength = myModel.user().firstName().length; // Read
  22. // myModel.user().firstName('Bert'); // Write
  23. //
  24. // ... versus Knockout-ES5 syntax:
  25. //
  26. // var firstNameLength = myModel.user.firstName.length; // Read
  27. // myModel.user.firstName = 'Bert'; // Write
  28. // `ko.track(model)` converts each property on the given model object into a getter/setter pair that
  29. // wraps a Knockout observable. Optionally specify an array of property names to wrap; otherwise we
  30. // wrap all properties. If any of the properties are already observables, we replace them with
  31. // ES5 getter/setter pairs that wrap your original observable instances. In the case of readonly
  32. // ko.computed properties, we simply do not define a setter (so attempted writes will be ignored,
  33. // which is how ES5 readonly properties normally behave).
  34. //
  35. // By design, this does *not* recursively walk child object properties, because making literally
  36. // everything everywhere independently observable is usually unhelpful. When you do want to track
  37. // child object properties independently, define your own class for those child objects and put
  38. // a separate ko.track call into its constructor --- this gives you far more control.
  39. function track(obj, propertyNames) {
  40. if (!obj /*|| typeof obj !== 'object'*/) {
  41. throw new Error('When calling ko.track, you must pass an object as the first parameter.');
  42. }
  43. var ko = this,
  44. allObservablesForObject = getAllObservablesForObject(obj, true);
  45. propertyNames = propertyNames || Object.getOwnPropertyNames(obj);
  46. propertyNames.forEach(function(propertyName) {
  47. // Skip storage properties
  48. if (propertyName === OBSERVABLES_PROPERTY || propertyName === SUBSCRIBABLE_PROPERTY) {
  49. return;
  50. }
  51. // Skip properties that are already tracked
  52. if (propertyName in allObservablesForObject) {
  53. return;
  54. }
  55. var origValue = obj[propertyName],
  56. isArray = origValue instanceof Array,
  57. observable = ko.isObservable(origValue) ? origValue
  58. : isArray ? ko.observableArray(origValue)
  59. : ko.observable(origValue);
  60. Object.defineProperty(obj, propertyName, {
  61. configurable: true,
  62. enumerable: true,
  63. get: observable,
  64. set: ko.isWriteableObservable(observable) ? observable : undefined
  65. });
  66. allObservablesForObject[propertyName] = observable;
  67. if (isArray) {
  68. notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
  69. }
  70. });
  71. return obj;
  72. }
  73. // Gets or creates the hidden internal key-value collection of observables corresponding to
  74. // properties on the model object.
  75. function getAllObservablesForObject(obj, createIfNotDefined) {
  76. var result = obj[OBSERVABLES_PROPERTY];
  77. if (!result && createIfNotDefined) {
  78. result = {};
  79. Object.defineProperty(obj, OBSERVABLES_PROPERTY, {
  80. value : result
  81. });
  82. }
  83. return result;
  84. }
  85. // Computed properties
  86. // -------------------
  87. //
  88. // The preceding code is already sufficient to upgrade ko.computed model properties to ES5
  89. // getter/setter pairs (or in the case of readonly ko.computed properties, just a getter).
  90. // These then behave like a regular property with a getter function, except they are smarter:
  91. // your evaluator is only invoked when one of its dependencies changes. The result is cached
  92. // and used for all evaluations until the next time a dependency changes).
  93. //
  94. // However, instead of forcing developers to declare a ko.computed property explicitly, it's
  95. // nice to offer a utility function that declares a computed getter directly.
  96. // Implements `ko.defineProperty`
  97. function defineComputedProperty(obj, propertyName, evaluatorOrOptions) {
  98. var ko = this,
  99. computedOptions = { owner: obj, deferEvaluation: true };
  100. if (typeof evaluatorOrOptions === 'function') {
  101. computedOptions.read = evaluatorOrOptions;
  102. } else {
  103. if ('value' in evaluatorOrOptions) {
  104. throw new Error('For ko.defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');
  105. }
  106. if (typeof evaluatorOrOptions.get !== 'function') {
  107. throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');
  108. }
  109. computedOptions.read = evaluatorOrOptions.get;
  110. computedOptions.write = evaluatorOrOptions.set;
  111. }
  112. obj[propertyName] = ko.computed(computedOptions);
  113. track.call(ko, obj, [propertyName]);
  114. return obj;
  115. }
  116. // Array handling
  117. // --------------
  118. //
  119. // Arrays are special, because unlike other property types, they have standard mutator functions
  120. // (`push`/`pop`/`splice`/etc.) and it's desirable to trigger a change notification whenever one of
  121. // those mutator functions is invoked.
  122. //
  123. // Traditionally, Knockout handles this by putting special versions of `push`/`pop`/etc. on observable
  124. // arrays that mutate the underlying array and then trigger a notification. That approach doesn't
  125. // work for Knockout-ES5 because properties now return the underlying arrays, so the mutator runs
  126. // in the context of the underlying array, not any particular observable:
  127. //
  128. // // Operates on the underlying array value
  129. // myModel.someCollection.push('New value');
  130. //
  131. // To solve this, Knockout-ES5 detects array values, and modifies them as follows:
  132. // 1. Associates a hidden subscribable with each array instance that it encounters
  133. // 2. Intercepts standard mutators (`push`/`pop`/etc.) and makes them trigger the subscribable
  134. // Then, for model properties whose values are arrays, the property's underlying observable
  135. // subscribes to the array subscribable, so it can trigger a change notification after mutation.
  136. // Given an observable that underlies a model property, watch for any array value that might
  137. // be assigned as the property value, and hook into its change events
  138. function notifyWhenPresentOrFutureArrayValuesMutate(ko, observable) {
  139. var watchingArraySubscription = null;
  140. ko.computed(function () {
  141. // Unsubscribe to any earlier array instance
  142. if (watchingArraySubscription) {
  143. watchingArraySubscription.dispose();
  144. watchingArraySubscription = null;
  145. }
  146. // Subscribe to the new array instance
  147. var newArrayInstance = observable();
  148. if (newArrayInstance instanceof Array) {
  149. watchingArraySubscription = startWatchingArrayInstance(ko, observable, newArrayInstance);
  150. }
  151. });
  152. }
  153. // Listens for array mutations, and when they happen, cause the observable to fire notifications.
  154. // This is used to make model properties of type array fire notifications when the array changes.
  155. // Returns a subscribable that can later be disposed.
  156. function startWatchingArrayInstance(ko, observable, arrayInstance) {
  157. var subscribable = getSubscribableForArray(ko, arrayInstance);
  158. return subscribable.subscribe(observable);
  159. }
  160. // Gets or creates a subscribable that fires after each array mutation
  161. function getSubscribableForArray(ko, arrayInstance) {
  162. var subscribable = arrayInstance[SUBSCRIBABLE_PROPERTY];
  163. if (!subscribable) {
  164. subscribable = new ko.subscribable();
  165. Object.defineProperty(arrayInstance, SUBSCRIBABLE_PROPERTY, {
  166. value : subscribable
  167. });
  168. var notificationPauseSignal = {};
  169. wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal);
  170. addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal);
  171. }
  172. return subscribable;
  173. }
  174. // After each array mutation, fires a notification on the given subscribable
  175. function wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal) {
  176. ['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'].forEach(function(fnName) {
  177. var origMutator = arrayInstance[fnName];
  178. arrayInstance[fnName] = function() {
  179. var result = origMutator.apply(this, arguments);
  180. if (notificationPauseSignal.pause !== true) {
  181. subscribable.notifySubscribers(this);
  182. }
  183. return result;
  184. };
  185. });
  186. }
  187. // Adds Knockout's additional array mutation functions to the array
  188. function addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal) {
  189. ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'].forEach(function(fnName) {
  190. // Make it a non-enumerable property for consistency with standard Array functions
  191. Object.defineProperty(arrayInstance, fnName, {
  192. enumerable: false,
  193. value: function() {
  194. var result;
  195. // These additional array mutators are built using the underlying push/pop/etc.
  196. // mutators, which are wrapped to trigger notifications. But we don't want to
  197. // trigger multiple notifications, so pause the push/pop/etc. wrappers and
  198. // delivery only one notification at the end of the process.
  199. notificationPauseSignal.pause = true;
  200. try {
  201. // Creates a temporary observableArray that can perform the operation.
  202. result = ko.observableArray.fn[fnName].apply(ko.observableArray(arrayInstance), arguments);
  203. }
  204. finally {
  205. notificationPauseSignal.pause = false;
  206. }
  207. subscribable.notifySubscribers(arrayInstance);
  208. return result;
  209. }
  210. });
  211. });
  212. }
  213. // Static utility functions
  214. // ------------------------
  215. //
  216. // Since Knockout-ES5 sets up properties that return values, not observables, you can't
  217. // trivially subscribe to the underlying observables (e.g., `someProperty.subscribe(...)`),
  218. // or tell them that object values have mutated, etc. To handle this, we set up some
  219. // extra utility functions that can return or work with the underlying observables.
  220. // Returns the underlying observable associated with a model property (or `null` if the
  221. // model or property doesn't exist, or isn't associated with an observable). This means
  222. // you can subscribe to the property, e.g.:
  223. //
  224. // ko.getObservable(model, 'propertyName')
  225. // .subscribe(function(newValue) { ... });
  226. function getObservable(obj, propertyName) {
  227. if (!obj /*|| typeof obj !== 'object'*/) {
  228. return null;
  229. }
  230. var allObservablesForObject = getAllObservablesForObject(obj, false);
  231. return (allObservablesForObject && allObservablesForObject[propertyName]) || null;
  232. }
  233. // Causes a property's associated observable to fire a change notification. Useful when
  234. // the property value is a complex object and you've modified a child property.
  235. function valueHasMutated(obj, propertyName) {
  236. var observable = getObservable(obj, propertyName);
  237. if (observable) {
  238. observable.valueHasMutated();
  239. }
  240. }
  241. // Extends a Knockout instance with Knockout-ES5 functionality
  242. function attachToKo(ko) {
  243. ko.track = track;
  244. ko.getObservable = getObservable;
  245. ko.valueHasMutated = valueHasMutated;
  246. ko.defineProperty = defineComputedProperty;
  247. }
  248. return {
  249. attachToKo : attachToKo
  250. };
  251. });