knockout-es5.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. /*!
  2. * Knockout ES5 plugin - https://github.com/SteveSanderson/knockout-es5
  3. * Copyright (c) Steve Sanderson
  4. * MIT license
  5. */
  6. (function(global, undefined) {
  7. 'use strict';
  8. var ko;
  9. // Model tracking
  10. // --------------
  11. //
  12. // This is the central feature of Knockout-ES5. We augment model objects by converting properties
  13. // into ES5 getter/setter pairs that read/write an underlying Knockout observable. This means you can
  14. // use plain JavaScript syntax to read/write the property while still getting the full benefits of
  15. // Knockout's automatic dependency detection and notification triggering.
  16. //
  17. // For comparison, here's Knockout ES3-compatible syntax:
  18. //
  19. // var firstNameLength = myModel.user().firstName().length; // Read
  20. // myModel.user().firstName('Bert'); // Write
  21. //
  22. // ... versus Knockout-ES5 syntax:
  23. //
  24. // var firstNameLength = myModel.user.firstName.length; // Read
  25. // myModel.user.firstName = 'Bert'; // Write
  26. // `ko.track(model)` converts each property on the given model object into a getter/setter pair that
  27. // wraps a Knockout observable. Optionally specify an array of property names to wrap; otherwise we
  28. // wrap all properties. If any of the properties are already observables, we replace them with
  29. // ES5 getter/setter pairs that wrap your original observable instances. In the case of readonly
  30. // ko.computed properties, we simply do not define a setter (so attempted writes will be ignored,
  31. // which is how ES5 readonly properties normally behave).
  32. //
  33. // By design, this does *not* recursively walk child object properties, because making literally
  34. // everything everywhere independently observable is usually unhelpful. When you do want to track
  35. // child object properties independently, define your own class for those child objects and put
  36. // a separate ko.track call into its constructor --- this gives you far more control.
  37. /**
  38. * @param {object} obj
  39. * @param {object|array.<string>} propertyNamesOrSettings
  40. * @param {boolean} propertyNamesOrSettings.deep Use deep track.
  41. * @param {array.<string>} propertyNamesOrSettings.fields Array of property names to wrap.
  42. * todo: @param {array.<string>} propertyNamesOrSettings.exclude Array of exclude property names to wrap.
  43. * todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property
  44. * names to wrap. A function that takes ... params
  45. * @return {object}
  46. */
  47. function track(obj, propertyNamesOrSettings) {
  48. if (!obj || typeof obj !== 'object') {
  49. throw new Error('When calling ko.track, you must pass an object as the first parameter.');
  50. }
  51. var propertyNames;
  52. if ( isPlainObject(propertyNamesOrSettings) ) {
  53. // defaults
  54. propertyNamesOrSettings.deep = propertyNamesOrSettings.deep || false;
  55. propertyNamesOrSettings.fields = propertyNamesOrSettings.fields || Object.getOwnPropertyNames(obj);
  56. propertyNamesOrSettings.lazy = propertyNamesOrSettings.lazy || false;
  57. wrap(obj, propertyNamesOrSettings.fields, propertyNamesOrSettings);
  58. } else {
  59. propertyNames = propertyNamesOrSettings || Object.getOwnPropertyNames(obj);
  60. wrap(obj, propertyNames, {});
  61. }
  62. return obj;
  63. }
  64. // fix for ie
  65. var rFunctionName = /^function\s*([^\s(]+)/;
  66. function getFunctionName( ctor ){
  67. if (ctor.name) {
  68. return ctor.name;
  69. }
  70. return (ctor.toString().trim().match( rFunctionName ) || [])[1];
  71. }
  72. function canTrack(obj) {
  73. return obj && typeof obj === 'object' && getFunctionName(obj.constructor) === 'Object';
  74. }
  75. function createPropertyDescriptor(originalValue, prop, map) {
  76. var isObservable = ko.isObservable(originalValue);
  77. var isArray = !isObservable && Array.isArray(originalValue);
  78. var observable = isObservable ? originalValue
  79. : isArray ? ko.observableArray(originalValue)
  80. : ko.observable(originalValue);
  81. map[prop] = function () { return observable; };
  82. // add check in case the object is already an observable array
  83. if (isArray || (isObservable && 'push' in observable)) {
  84. notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
  85. }
  86. return {
  87. configurable: true,
  88. enumerable: true,
  89. get: observable,
  90. set: ko.isWriteableObservable(observable) ? observable : undefined
  91. };
  92. }
  93. function createLazyPropertyDescriptor(originalValue, prop, map) {
  94. if (ko.isObservable(originalValue)) {
  95. // no need to be lazy if we already have an observable
  96. return createPropertyDescriptor(originalValue, prop, map);
  97. }
  98. var observable;
  99. function getOrCreateObservable(value, writing) {
  100. if (observable) {
  101. return writing ? observable(value) : observable;
  102. }
  103. if (Array.isArray(value)) {
  104. observable = ko.observableArray(value);
  105. notifyWhenPresentOrFutureArrayValuesMutate(ko, observable);
  106. return observable;
  107. }
  108. return (observable = ko.observable(value));
  109. }
  110. map[prop] = function () { return getOrCreateObservable(originalValue); };
  111. return {
  112. configurable: true,
  113. enumerable: true,
  114. get: function () { return getOrCreateObservable(originalValue)(); },
  115. set: function (value) { getOrCreateObservable(value, true); }
  116. };
  117. }
  118. function wrap(obj, props, options) {
  119. if (!props.length) {
  120. return;
  121. }
  122. var allObservablesForObject = getAllObservablesForObject(obj, true);
  123. var descriptors = {};
  124. props.forEach(function (prop) {
  125. // Skip properties that are already tracked
  126. if (prop in allObservablesForObject) {
  127. return;
  128. }
  129. // Skip properties where descriptor can't be redefined
  130. if (Object.getOwnPropertyDescriptor(obj, prop).configurable === false){
  131. return;
  132. }
  133. var originalValue = obj[prop];
  134. descriptors[prop] = (options.lazy ? createLazyPropertyDescriptor : createPropertyDescriptor)
  135. (originalValue, prop, allObservablesForObject);
  136. if (options.deep && canTrack(originalValue)) {
  137. wrap(originalValue, Object.keys(originalValue), options);
  138. }
  139. });
  140. Object.defineProperties(obj, descriptors);
  141. }
  142. function isPlainObject( obj ){
  143. return !!obj && typeof obj === 'object' && obj.constructor === Object;
  144. }
  145. // Lazily created by `getAllObservablesForObject` below. Has to be created lazily because the
  146. // WeakMap factory isn't available until the module has finished loading (may be async).
  147. var objectToObservableMap;
  148. // Gets or creates the hidden internal key-value collection of observables corresponding to
  149. // properties on the model object.
  150. function getAllObservablesForObject(obj, createIfNotDefined) {
  151. if (!objectToObservableMap) {
  152. objectToObservableMap = weakMapFactory();
  153. }
  154. var result = objectToObservableMap.get(obj);
  155. if (!result && createIfNotDefined) {
  156. result = {};
  157. objectToObservableMap.set(obj, result);
  158. }
  159. return result;
  160. }
  161. // Removes the internal references to observables mapped to the specified properties
  162. // or the entire object reference if no properties are passed in. This allows the
  163. // observables to be replaced and tracked again.
  164. function untrack(obj, propertyNames) {
  165. if (!objectToObservableMap) {
  166. return;
  167. }
  168. if (arguments.length === 1) {
  169. objectToObservableMap['delete'](obj);
  170. } else {
  171. var allObservablesForObject = getAllObservablesForObject(obj, false);
  172. if (allObservablesForObject) {
  173. propertyNames.forEach(function(propertyName) {
  174. delete allObservablesForObject[propertyName];
  175. });
  176. }
  177. }
  178. }
  179. // Computed properties
  180. // -------------------
  181. //
  182. // The preceding code is already sufficient to upgrade ko.computed model properties to ES5
  183. // getter/setter pairs (or in the case of readonly ko.computed properties, just a getter).
  184. // These then behave like a regular property with a getter function, except they are smarter:
  185. // your evaluator is only invoked when one of its dependencies changes. The result is cached
  186. // and used for all evaluations until the next time a dependency changes).
  187. //
  188. // However, instead of forcing developers to declare a ko.computed property explicitly, it's
  189. // nice to offer a utility function that declares a computed getter directly.
  190. // Implements `ko.defineProperty`
  191. function defineComputedProperty(obj, propertyName, evaluatorOrOptions) {
  192. var ko = this,
  193. computedOptions = { owner: obj, deferEvaluation: true };
  194. if (typeof evaluatorOrOptions === 'function') {
  195. computedOptions.read = evaluatorOrOptions;
  196. } else {
  197. if ('value' in evaluatorOrOptions) {
  198. throw new Error('For ko.defineProperty, you must not specify a "value" for the property. ' +
  199. 'You must provide a "get" function.');
  200. }
  201. if (typeof evaluatorOrOptions.get !== 'function') {
  202. throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, ' +
  203. 'or an options object containing a function called "get".');
  204. }
  205. computedOptions.read = evaluatorOrOptions.get;
  206. computedOptions.write = evaluatorOrOptions.set;
  207. }
  208. obj[propertyName] = ko.computed(computedOptions);
  209. track.call(ko, obj, [propertyName]);
  210. return obj;
  211. }
  212. // Array handling
  213. // --------------
  214. //
  215. // Arrays are special, because unlike other property types, they have standard mutator functions
  216. // (`push`/`pop`/`splice`/etc.) and it's desirable to trigger a change notification whenever one of
  217. // those mutator functions is invoked.
  218. //
  219. // Traditionally, Knockout handles this by putting special versions of `push`/`pop`/etc. on observable
  220. // arrays that mutate the underlying array and then trigger a notification. That approach doesn't
  221. // work for Knockout-ES5 because properties now return the underlying arrays, so the mutator runs
  222. // in the context of the underlying array, not any particular observable:
  223. //
  224. // // Operates on the underlying array value
  225. // myModel.someCollection.push('New value');
  226. //
  227. // To solve this, Knockout-ES5 detects array values, and modifies them as follows:
  228. // 1. Associates a hidden subscribable with each array instance that it encounters
  229. // 2. Intercepts standard mutators (`push`/`pop`/etc.) and makes them trigger the subscribable
  230. // Then, for model properties whose values are arrays, the property's underlying observable
  231. // subscribes to the array subscribable, so it can trigger a change notification after mutation.
  232. // Given an observable that underlies a model property, watch for any array value that might
  233. // be assigned as the property value, and hook into its change events
  234. function notifyWhenPresentOrFutureArrayValuesMutate(ko, observable) {
  235. var watchingArraySubscription = null;
  236. ko.computed(function () {
  237. // Unsubscribe to any earlier array instance
  238. if (watchingArraySubscription) {
  239. watchingArraySubscription.dispose();
  240. watchingArraySubscription = null;
  241. }
  242. // Subscribe to the new array instance
  243. var newArrayInstance = observable();
  244. if (newArrayInstance instanceof Array) {
  245. watchingArraySubscription = startWatchingArrayInstance(ko, observable, newArrayInstance);
  246. }
  247. });
  248. }
  249. // Listens for array mutations, and when they happen, cause the observable to fire notifications.
  250. // This is used to make model properties of type array fire notifications when the array changes.
  251. // Returns a subscribable that can later be disposed.
  252. function startWatchingArrayInstance(ko, observable, arrayInstance) {
  253. var subscribable = getSubscribableForArray(ko, arrayInstance);
  254. return subscribable.subscribe(observable);
  255. }
  256. // Lazily created by `getSubscribableForArray` below. Has to be created lazily because the
  257. // WeakMap factory isn't available until the module has finished loading (may be async).
  258. var arraySubscribablesMap;
  259. // Gets or creates a subscribable that fires after each array mutation
  260. function getSubscribableForArray(ko, arrayInstance) {
  261. if (!arraySubscribablesMap) {
  262. arraySubscribablesMap = weakMapFactory();
  263. }
  264. var subscribable = arraySubscribablesMap.get(arrayInstance);
  265. if (!subscribable) {
  266. subscribable = new ko.subscribable();
  267. arraySubscribablesMap.set(arrayInstance, subscribable);
  268. var notificationPauseSignal = {};
  269. wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal);
  270. addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal);
  271. }
  272. return subscribable;
  273. }
  274. // After each array mutation, fires a notification on the given subscribable
  275. function wrapStandardArrayMutators(arrayInstance, subscribable, notificationPauseSignal) {
  276. ['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'].forEach(function(fnName) {
  277. var origMutator = arrayInstance[fnName];
  278. arrayInstance[fnName] = function() {
  279. var result = origMutator.apply(this, arguments);
  280. if (notificationPauseSignal.pause !== true) {
  281. subscribable.notifySubscribers(this);
  282. }
  283. return result;
  284. };
  285. });
  286. }
  287. // Adds Knockout's additional array mutation functions to the array
  288. function addKnockoutArrayMutators(ko, arrayInstance, subscribable, notificationPauseSignal) {
  289. ['remove', 'removeAll', 'destroy', 'destroyAll', 'replace'].forEach(function(fnName) {
  290. // Make it a non-enumerable property for consistency with standard Array functions
  291. Object.defineProperty(arrayInstance, fnName, {
  292. enumerable: false,
  293. value: function() {
  294. var result;
  295. // These additional array mutators are built using the underlying push/pop/etc.
  296. // mutators, which are wrapped to trigger notifications. But we don't want to
  297. // trigger multiple notifications, so pause the push/pop/etc. wrappers and
  298. // delivery only one notification at the end of the process.
  299. notificationPauseSignal.pause = true;
  300. try {
  301. // Creates a temporary observableArray that can perform the operation.
  302. result = ko.observableArray.fn[fnName].apply(ko.observableArray(arrayInstance), arguments);
  303. }
  304. finally {
  305. notificationPauseSignal.pause = false;
  306. }
  307. subscribable.notifySubscribers(arrayInstance);
  308. return result;
  309. }
  310. });
  311. });
  312. }
  313. // Static utility functions
  314. // ------------------------
  315. //
  316. // Since Knockout-ES5 sets up properties that return values, not observables, you can't
  317. // trivially subscribe to the underlying observables (e.g., `someProperty.subscribe(...)`),
  318. // or tell them that object values have mutated, etc. To handle this, we set up some
  319. // extra utility functions that can return or work with the underlying observables.
  320. // Returns the underlying observable associated with a model property (or `null` if the
  321. // model or property doesn't exist, or isn't associated with an observable). This means
  322. // you can subscribe to the property, e.g.:
  323. //
  324. // ko.getObservable(model, 'propertyName')
  325. // .subscribe(function(newValue) { ... });
  326. function getObservable(obj, propertyName) {
  327. if (!obj || typeof obj !== 'object') {
  328. return null;
  329. }
  330. var allObservablesForObject = getAllObservablesForObject(obj, false);
  331. if (allObservablesForObject && propertyName in allObservablesForObject) {
  332. return allObservablesForObject[propertyName]();
  333. }
  334. return null;
  335. }
  336. // Returns a boolean indicating whether the property on the object has an underlying
  337. // observables. This does the check in a way not to create an observable if the
  338. // object was created with lazily created observables
  339. function isTracked(obj, propertyName) {
  340. if (!obj || typeof obj !== 'object') {
  341. return false;
  342. }
  343. var allObservablesForObject = getAllObservablesForObject(obj, false);
  344. return !!allObservablesForObject && propertyName in allObservablesForObject;
  345. }
  346. // Causes a property's associated observable to fire a change notification. Useful when
  347. // the property value is a complex object and you've modified a child property.
  348. function valueHasMutated(obj, propertyName) {
  349. var observable = getObservable(obj, propertyName);
  350. if (observable) {
  351. observable.valueHasMutated();
  352. }
  353. }
  354. // Module initialisation
  355. // ---------------------
  356. //
  357. // When this script is first evaluated, it works out what kind of module loading scenario
  358. // it is in (Node.js or a browser `<script>` tag), stashes a reference to its dependencies
  359. // (currently that's just the WeakMap shim), and then finally attaches itself to whichever
  360. // instance of Knockout.js it can find.
  361. // A function that returns a new ES6-compatible WeakMap instance (using ES5 shim if needed).
  362. // Instantiated by prepareExports, accounting for which module loader is being used.
  363. var weakMapFactory;
  364. // Extends a Knockout instance with Knockout-ES5 functionality
  365. function attachToKo(ko) {
  366. ko.track = track;
  367. ko.untrack = untrack;
  368. ko.getObservable = getObservable;
  369. ko.valueHasMutated = valueHasMutated;
  370. ko.defineProperty = defineComputedProperty;
  371. // todo: test it, maybe added it to ko. directly
  372. ko.es5 = {
  373. getAllObservablesForObject: getAllObservablesForObject,
  374. notifyWhenPresentOrFutureArrayValuesMutate: notifyWhenPresentOrFutureArrayValuesMutate,
  375. isTracked: isTracked
  376. };
  377. }
  378. // Determines which module loading scenario we're in, grabs dependencies, and attaches to KO
  379. function prepareExports() {
  380. if (typeof exports === 'object' && typeof module === 'object') {
  381. // Node.js case - load KO and WeakMap modules synchronously
  382. ko = require('knockout');
  383. var WM = require('../lib/weakmap');
  384. attachToKo(ko);
  385. weakMapFactory = function() { return new WM(); };
  386. module.exports = ko;
  387. } else if (typeof define === 'function' && define.amd) {
  388. define(['knockout'], function(koModule) {
  389. ko = koModule;
  390. attachToKo(koModule);
  391. weakMapFactory = function() { return new global.WeakMap(); };
  392. return koModule;
  393. });
  394. } else if ('ko' in global) {
  395. // Non-module case - attach to the global instance, and assume a global WeakMap constructor
  396. ko = global.ko;
  397. attachToKo(global.ko);
  398. weakMapFactory = function() { return new global.WeakMap(); };
  399. }
  400. }
  401. prepareExports();
  402. })(this);