diff --git a/dist/knockout-es5.js b/dist/knockout-es5.js index 128172b..fcbf964 100644 --- a/dist/knockout-es5.js +++ b/dist/knockout-es5.js @@ -44,7 +44,8 @@ * @param {boolean} propertyNamesOrSettings.deep Use deep track. * @param {array.} propertyNamesOrSettings.fields Array of property names to wrap. * todo: @param {array.} propertyNamesOrSettings.exclude Array of exclude property names to wrap. - * todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property names to wrap. A function that takes ... params + * todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property + * names to wrap. A function that takes ... params * @return {object} */ function track(obj, propertyNamesOrSettings) { @@ -59,17 +60,10 @@ propertyNamesOrSettings.deep = propertyNamesOrSettings.deep || false; propertyNamesOrSettings.fields = propertyNamesOrSettings.fields || Object.getOwnPropertyNames(obj); - if (propertyNamesOrSettings.deep === true) { - trackDeep(propertyNamesOrSettings.fields, obj); - } else { - return track(obj, propertyNamesOrSettings.fields); - } - + wrap(obj, propertyNamesOrSettings.fields, propertyNamesOrSettings.deep); } else { propertyNames = propertyNamesOrSettings || Object.getOwnPropertyNames(obj); - propertyNames.forEach(function(propertyName) { - wrap(propertyName, null, obj); - }); + wrap(obj, propertyNames); } return obj; @@ -84,43 +78,19 @@ return (ctor.toString().trim().match( rFunctionName ) || [])[1]; } - function trackDeep(tree, obj) { - var keys = Array.isArray(tree) ? tree : Object.keys(tree); - var limb; - - keys.forEach(function(key) { - limb = obj[key]; - - wrap(key, - (('Object' === getFunctionName(limb.constructor) - && Object.keys(limb).length) - ? limb - : null), - obj); - }); + function canTrack(obj) { + return obj && typeof obj === 'object' && getFunctionName(obj.constructor) === 'Object'; } - // Wrap property into observable - function wrap(prop, subprops, obj) { - var allObservablesForObject = getAllObservablesForObject(obj, true); - var origValue, observable, isArray; - - if (subprops) { - origValue = obj[prop]; - observable = ko.isObservable(origValue) ? origValue : ko.observable(origValue); - - Object.defineProperty(obj, prop, { - configurable: true, - enumerable: true, - get: observable, - set: ko.isWriteableObservable(observable) ? observable : undefined - }); - - allObservablesForObject[prop] = observable; + function wrap(obj, props, deep) { + if (!props.length) { + return; + } - trackDeep(subprops, obj[prop]); + var allObservablesForObject = getAllObservablesForObject(obj, true); + var descriptor = {}; - } else { + props.forEach(function (prop) { // Skip properties that are already tracked if (prop in allObservablesForObject) { return; @@ -131,25 +101,44 @@ return; } - origValue = obj[prop]; - isArray = Array.isArray(origValue); - observable = ko.isObservable(origValue) ? origValue + var origValue = obj[prop]; + var isObservable = ko.isObservable(origValue); + var isArray = Array.isArray(origValue); + var observable = isObservable ? origValue : isArray ? ko.observableArray(origValue) : ko.observable(origValue); - Object.defineProperty(obj, prop, { + // add check in case the object is already an observable array + if (isObservable && 'push' in observable) { + isArray = true; + origValue = observable.peek(); + } + + descriptor[prop] = { configurable: true, enumerable: true, get: observable, set: ko.isWriteableObservable(observable) ? observable : undefined - }); + }; allObservablesForObject[prop] = observable; if (isArray) { notifyWhenPresentOrFutureArrayValuesMutate(ko, observable); + + if (deep) { + origValue.forEach(function (child) { + if (canTrack(child)) { + wrap(child, Object.keys(child), true); + } + }); + } + } else if (deep && canTrack(origValue)) { + wrap(origValue, Object.keys(origValue), true); } - } + }); + + Object.defineProperties(obj, descriptor); } function isPlainObject( obj ){ diff --git a/dist/knockout-es5.min.js b/dist/knockout-es5.min.js index 58e3f85..df6aef9 100644 --- a/dist/knockout-es5.min.js +++ b/dist/knockout-es5.min.js @@ -3,7 +3,7 @@ * Copyright (c) Steve Sanderson * MIT license */ -!function(a,b){"use strict";function c(a,b){if(!a||"object"!=typeof a)throw new Error("When calling ko.track, you must pass an object as the first parameter.");var d;if(g(b)){if(b.deep=b.deep||!1,b.fields=b.fields||Object.getOwnPropertyNames(a),b.deep!==!0)return c(a,b.fields);e(b.fields,a)}else d=b||Object.getOwnPropertyNames(a),d.forEach(function(b){f(b,null,a)});return a}function d(a){return a.name?a.name:(a.toString().trim().match(x)||[])[1]}function e(a,b){var c,e=Array.isArray(a)?a:Object.keys(a);e.forEach(function(a){c=b[a],f(a,"Object"===d(c.constructor)&&Object.keys(c).length?c:null,b)})}function f(a,c,d){var f,g,i,j=h(d,!0);if(c)f=d[a],g=t.isObservable(f)?f:t.observable(f),Object.defineProperty(d,a,{configurable:!0,enumerable:!0,get:g,set:t.isWriteableObservable(g)?g:b}),j[a]=g,e(c,d[a]);else{if(a in j)return;if(Object.getOwnPropertyDescriptor(d,a).configurable===!1)return;f=d[a],i=Array.isArray(f),g=t.isObservable(f)?f:i?t.observableArray(f):t.observable(f),Object.defineProperty(d,a,{configurable:!0,enumerable:!0,get:g,set:t.isWriteableObservable(g)?g:b}),j[a]=g,i&&k(t,g)}}function g(a){return!!a&&"object"==typeof a&&a.constructor===Object}function h(a,b){u||(u=w());var c=u.get(a);return!c&&b&&(c={},u.set(a,c)),c}function i(a,b){if(u)if(1===arguments.length)u["delete"](a);else{var c=h(a,!1);c&&b.forEach(function(a){delete c[a]})}}function j(a,b,d){var e=this,f={owner:a,deferEvaluation:!0};if("function"==typeof d)f.read=d;else{if("value"in d)throw new Error('For ko.defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');if("function"!=typeof d.get)throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');f.read=d.get,f.write=d.set}return a[b]=e.computed(f),c.call(e,a,[b]),a}function k(a,b){var c=null;a.computed(function(){c&&(c.dispose(),c=null);var d=b();d instanceof Array&&(c=l(a,b,d))})}function l(a,b,c){var d=m(a,c);return d.subscribe(b)}function m(a,b){v||(v=w());var c=v.get(b);if(!c){c=new a.subscribable,v.set(b,c);var d={};n(b,c,d),o(a,b,c,d)}return c}function n(a,b,c){["pop","push","reverse","shift","sort","splice","unshift"].forEach(function(d){var e=a[d];a[d]=function(){var a=e.apply(this,arguments);return c.pause!==!0&&b.notifySubscribers(this),a}})}function o(a,b,c,d){["remove","removeAll","destroy","destroyAll","replace"].forEach(function(e){Object.defineProperty(b,e,{enumerable:!1,value:function(){var f;d.pause=!0;try{f=a.observableArray.fn[e].apply(a.observableArray(b),arguments)}finally{d.pause=!1}return c.notifySubscribers(b),f}})})}function p(a,b){if(!a||"object"!=typeof a)return null;var c=h(a,!1);return c&&c[b]||null}function q(a,b){var c=p(a,b);c&&c.valueHasMutated()}function r(a){a.track=c,a.untrack=i,a.getObservable=p,a.valueHasMutated=q,a.defineProperty=j,a.es5={getAllObservablesForObject:h,notifyWhenPresentOrFutureArrayValuesMutate:k}}function s(){if("object"==typeof exports&&"object"==typeof module){t=require("knockout");var b=require("../lib/weakmap");r(t),w=function(){return new b},module.exports=t}else"function"==typeof define&&define.amd?define(["knockout"],function(b){return t=b,r(b),w=function(){return new a.WeakMap},b}):"ko"in a&&(t=a.ko,r(a.ko),w=function(){return new a.WeakMap})}var t,u,v,w,x=/^function\s*([^\s(]+)/;s()}(this),/*! WeakMap shim +!function(a,b){"use strict";function c(a,b){if(!a||"object"!=typeof a)throw new Error("When calling ko.track, you must pass an object as the first parameter.");var c;return g(b)?(b.deep=b.deep||!1,b.fields=b.fields||Object.getOwnPropertyNames(a),f(a,b.fields,b.deep)):(c=b||Object.getOwnPropertyNames(a),f(a,c)),a}function d(a){return a.name?a.name:(a.toString().trim().match(x)||[])[1]}function e(a){return a&&"object"==typeof a&&"Object"===d(a.constructor)}function f(a,c,d){if(c.length){var g=h(a,!0),i={};c.forEach(function(c){if(!(c in g)&&Object.getOwnPropertyDescriptor(a,c).configurable!==!1){var h=a[c],j=t.isObservable(h),l=Array.isArray(h),m=j?h:l?t.observableArray(h):t.observable(h);j&&"push"in m&&(l=!0,h=m.peek()),i[c]={configurable:!0,enumerable:!0,get:m,set:t.isWriteableObservable(m)?m:b},g[c]=m,l?(k(t,m),d&&h.forEach(function(a){e(a)&&f(a,Object.keys(a),!0)})):d&&e(h)&&f(h,Object.keys(h),!0)}}),Object.defineProperties(a,i)}}function g(a){return!!a&&"object"==typeof a&&a.constructor===Object}function h(a,b){u||(u=w());var c=u.get(a);return!c&&b&&(c={},u.set(a,c)),c}function i(a,b){if(u)if(1===arguments.length)u["delete"](a);else{var c=h(a,!1);c&&b.forEach(function(a){delete c[a]})}}function j(a,b,d){var e=this,f={owner:a,deferEvaluation:!0};if("function"==typeof d)f.read=d;else{if("value"in d)throw new Error('For ko.defineProperty, you must not specify a "value" for the property. You must provide a "get" function.');if("function"!=typeof d.get)throw new Error('For ko.defineProperty, the third parameter must be either an evaluator function, or an options object containing a function called "get".');f.read=d.get,f.write=d.set}return a[b]=e.computed(f),c.call(e,a,[b]),a}function k(a,b){var c=null;a.computed(function(){c&&(c.dispose(),c=null);var d=b();d instanceof Array&&(c=l(a,b,d))})}function l(a,b,c){var d=m(a,c);return d.subscribe(b)}function m(a,b){v||(v=w());var c=v.get(b);if(!c){c=new a.subscribable,v.set(b,c);var d={};n(b,c,d),o(a,b,c,d)}return c}function n(a,b,c){["pop","push","reverse","shift","sort","splice","unshift"].forEach(function(d){var e=a[d];a[d]=function(){var a=e.apply(this,arguments);return c.pause!==!0&&b.notifySubscribers(this),a}})}function o(a,b,c,d){["remove","removeAll","destroy","destroyAll","replace"].forEach(function(e){Object.defineProperty(b,e,{enumerable:!1,value:function(){var f;d.pause=!0;try{f=a.observableArray.fn[e].apply(a.observableArray(b),arguments)}finally{d.pause=!1}return c.notifySubscribers(b),f}})})}function p(a,b){if(!a||"object"!=typeof a)return null;var c=h(a,!1);return c&&c[b]||null}function q(a,b){var c=p(a,b);c&&c.valueHasMutated()}function r(a){a.track=c,a.untrack=i,a.getObservable=p,a.valueHasMutated=q,a.defineProperty=j,a.es5={getAllObservablesForObject:h,notifyWhenPresentOrFutureArrayValuesMutate:k}}function s(){if("object"==typeof exports&&"object"==typeof module){t=require("knockout");var b=require("../lib/weakmap");r(t),w=function(){return new b},module.exports=t}else"function"==typeof define&&define.amd?define(["knockout"],function(b){return t=b,r(b),w=function(){return new a.WeakMap},b}):"ko"in a&&(t=a.ko,r(a.ko),w=function(){return new a.WeakMap})}var t,u,v,w,x=/^function\s*([^\s(]+)/;s()}(this),/*! WeakMap shim * (The MIT License) * * Copyright (c) 2012 Brandon Benvie diff --git a/src/knockout-es5.js b/src/knockout-es5.js index 690f342..2792a51 100644 --- a/src/knockout-es5.js +++ b/src/knockout-es5.js @@ -44,7 +44,8 @@ * @param {boolean} propertyNamesOrSettings.deep Use deep track. * @param {array.} propertyNamesOrSettings.fields Array of property names to wrap. * todo: @param {array.} propertyNamesOrSettings.exclude Array of exclude property names to wrap. - * todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property names to wrap. A function that takes ... params + * todo: @param {function(string, *):boolean} propertyNamesOrSettings.filter Function to filter property + * names to wrap. A function that takes ... params * @return {object} */ function track(obj, propertyNamesOrSettings) { @@ -59,17 +60,10 @@ propertyNamesOrSettings.deep = propertyNamesOrSettings.deep || false; propertyNamesOrSettings.fields = propertyNamesOrSettings.fields || Object.getOwnPropertyNames(obj); - if (propertyNamesOrSettings.deep === true) { - trackDeep(propertyNamesOrSettings.fields, obj); - } else { - return track(obj, propertyNamesOrSettings.fields); - } - + wrap(obj, propertyNamesOrSettings.fields, propertyNamesOrSettings.deep); } else { propertyNames = propertyNamesOrSettings || Object.getOwnPropertyNames(obj); - propertyNames.forEach(function(propertyName) { - wrap(propertyName, null, obj); - }); + wrap(obj, propertyNames); } return obj; @@ -84,43 +78,19 @@ return (ctor.toString().trim().match( rFunctionName ) || [])[1]; } - function trackDeep(tree, obj) { - var keys = Array.isArray(tree) ? tree : Object.keys(tree); - var limb; - - keys.forEach(function(key) { - limb = obj[key]; - - wrap(key, - (('Object' === getFunctionName(limb.constructor) - && Object.keys(limb).length) - ? limb - : null), - obj); - }); + function canTrack(obj) { + return obj && typeof obj === 'object' && getFunctionName(obj.constructor) === 'Object'; } - // Wrap property into observable - function wrap(prop, subprops, obj) { - var allObservablesForObject = getAllObservablesForObject(obj, true); - var origValue, observable, isArray; - - if (subprops) { - origValue = obj[prop]; - observable = ko.isObservable(origValue) ? origValue : ko.observable(origValue); - - Object.defineProperty(obj, prop, { - configurable: true, - enumerable: true, - get: observable, - set: ko.isWriteableObservable(observable) ? observable : undefined - }); - - allObservablesForObject[prop] = observable; + function wrap(obj, props, deep) { + if (!props.length) { + return; + } - trackDeep(subprops, obj[prop]); + var allObservablesForObject = getAllObservablesForObject(obj, true); + var descriptor = {}; - } else { + props.forEach(function (prop) { // Skip properties that are already tracked if (prop in allObservablesForObject) { return; @@ -131,25 +101,44 @@ return; } - origValue = obj[prop]; - isArray = Array.isArray(origValue); - observable = ko.isObservable(origValue) ? origValue + var origValue = obj[prop]; + var isObservable = ko.isObservable(origValue); + var isArray = Array.isArray(origValue); + var observable = isObservable ? origValue : isArray ? ko.observableArray(origValue) : ko.observable(origValue); - Object.defineProperty(obj, prop, { + // add check in case the object is already an observable array + if (isObservable && 'push' in observable) { + isArray = true; + origValue = observable.peek(); + } + + descriptor[prop] = { configurable: true, enumerable: true, get: observable, set: ko.isWriteableObservable(observable) ? observable : undefined - }); + }; allObservablesForObject[prop] = observable; if (isArray) { notifyWhenPresentOrFutureArrayValuesMutate(ko, observable); + + if (deep) { + origValue.forEach(function (child) { + if (canTrack(child)) { + wrap(child, Object.keys(child), true); + } + }); + } + } else if (deep && canTrack(origValue)) { + wrap(origValue, Object.keys(origValue), true); } - } + }); + + Object.defineProperties(obj, descriptor); } function isPlainObject( obj ){ diff --git a/test/arrays.test.js b/test/arrays.test.js index 979c195..379b233 100644 --- a/test/arrays.test.js +++ b/test/arrays.test.js @@ -137,4 +137,18 @@ describe('Array handling', function () { assert.equal(allNotifiedValues.length, 1); assert.equal(allNotifiedValues[ 0 ], '4,5'); }); + + it('if a property is already an observable array, it still gets wrapped with mutator functions', function () { + var plainArray = ['a', 'b', 'c'], + obj = ko.track({ myArray: ko.observableArray(plainArray) }), + lastNotifiedValue = ko.computed(function() { return obj.myArray.join(','); }); + + // Reading the property returns the underlying array value + assert.equal(obj.myArray instanceof Array, true); + assert.deepEqual(obj.myArray, plainArray); + assert.equal(lastNotifiedValue(), 'a,b,c'); + + obj.myArray.push('d'); + assert.equal(lastNotifiedValue(), 'a,b,c,d'); + }); }); \ No newline at end of file