diff --git a/js/src/observe.js b/js/src/observe.js index ff2634f..9e1dc11 100644 --- a/js/src/observe.js +++ b/js/src/observe.js @@ -36,11 +36,7 @@ export default class uObserve { if (typeof p2 === 'function') { this.observeProperty(obj, p1, p2); } else if (typeof p1 === 'function') { - if (Array.isArray(obj)) { - this.observeArray(obj, p1); - } else { - this.observeRecursive(obj, p1); - } + this.observeRecursive(obj, p1); } else { throw new Error('Invalid arguments'); } @@ -66,11 +62,11 @@ export default class uObserve { } static isObserved(obj, property) { - if (typeof obj !== 'object' || obj === null) { + if (typeof obj !== 'object' || obj === null || !obj.hasOwnProperty(property)) { return false; } - return obj.hasOwnProperty(property) && obj.hasOwnProperty('_values') && - obj._values.hasOwnProperty(property) && !!Object.getOwnPropertyDescriptor(obj, property)['set']; + return obj.hasOwnProperty('_values') && obj._values.hasOwnProperty(property) && + !!Object.getOwnPropertyDescriptor(obj, property)['set']; } /** @@ -129,9 +125,13 @@ export default class uObserve { * @param {ObserveCallback} observer */ static observeRecursive(obj, observer) { - for (const prop in obj) { - if (obj.hasOwnProperty(prop)) { - uObserve.observeProperty(obj, prop, observer); + if (Array.isArray(obj)) { + this.observeArray(obj, observer); + } else { + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + uObserve.observeProperty(obj, prop, observer); + } } } } @@ -148,7 +148,7 @@ export default class uObserve { const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, operation); descriptor.value = function () { const result = Array.prototype[operation].apply(arr, arguments); - console.log(`[${operation}] ` + arr.length ? `[${arr[0]}, …](${arr.length})` : arr); + console.log(`[${operation}] ` + (arr.length ? `[${arr[0]}, …](${arr.length})` : arr)); uObserve.notify(arr._observers, arr); return result; }; @@ -182,8 +182,8 @@ export default class uObserve { /** * Remove observer from object's property or all it's properties - * unobserve(obj, prop, observer) observes given property prop; - * unobserve(obj, observer) observes all properties of object obj. + * unobserve(obj, prop, observer) unobserves given property prop; + * unobserve(obj, observer) unobserves all properties of object obj. * @param {Object} obj * @param {(string|ObserveCallback)} p1 * @param {ObserveCallback=} p2 @@ -202,6 +202,32 @@ export default class uObserve { } } + /** + * Remove all observers from object's property or all it's properties + * unobserve(obj, prop) removes all observes from given property prop; + * unobserve(obj) removes all observers from all properties of object obj. + * @param {Object} obj + * @param {string} property + */ + static unobserveAll(obj, property) { + if (this.isObserved(obj, property)) { + console.log(`Removing all observers for ${property}…`); + if (Array.isArray(obj[property])) { + this.restoreArrayPrototypes(obj[property]); + } else if (typeof obj[property] === 'object' && obj[property] !== null) { + for (const prop in obj[property]) { + if (obj[property].hasOwnProperty(prop)) { + this.unobserveAll(obj[property], prop); + } + } + } + delete obj._observers[property]; + delete obj[property]; + obj[property] = obj._values[property]; + delete obj._values[property]; + } + } + /** * Remove observer from object's property * @param {Object} obj @@ -241,14 +267,18 @@ export default class uObserve { static unobserveArray(arr, observer) { this.removeObserver(arr, observer); if (!arr._observers.size) { - [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach( - (operation) => { - const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, operation); - Object.defineProperty(arr, operation, descriptor); - }); + this.restoreArrayPrototypes(arr); } } + static restoreArrayPrototypes(arr) { + [ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach( + (operation) => { + const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, operation); + Object.defineProperty(arr, operation, descriptor); + }); + } + /** * Remove observer from object's property * @param {Object} obj Object diff --git a/js/test/observe.test.js b/js/test/observe.test.js index 5223e7c..ec36e32 100644 --- a/js/test/observe.test.js +++ b/js/test/observe.test.js @@ -335,6 +335,66 @@ describe('Observe tests', () => { expect(array).toEqual([ 1, 2, 3 ]); }); + it('should remove all observers of object property', () => { + // given + let result2 = false; + let resultValue2; + const observer = (value) => { + result = true; + resultValue = value; + }; + const observer2 = (value) => { + result2 = true; + resultValue2 = value; + }; + uObserve.observe(object, 'observed', observer); + uObserve.observe(object, 'observed', observer2); + // when + uObserve.unobserveAll(object, 'observed'); + + expect(result).toBe(false); + expect(result2).toBe(false); + object.observed = 2; + // then + expect(result).toBe(false); + expect(resultValue).toBe(undefined);// eslint-disable-line no-undefined + expect(result2).toBe(false); + // noinspection JSUnusedAssignment + expect(resultValue2).toBe(undefined);// eslint-disable-line no-undefined + expect(object.observed).toBe(2); + }); + + it('should remove all observers from array property', () => { + // given + let result2 = false; + let resultValue2; + const observer = (value) => { + result = true; + resultValue = value; + }; + const observer2 = (value) => { + result2 = true; + resultValue2 = value; + }; + const array = [ 1, 2 ]; + object.arr = array; + uObserve.observe(object, 'arr', observer); + uObserve.observe(object, 'arr', observer2); + // when + uObserve.unobserveAll(object, 'arr'); + + expect(result).toBe(false); + expect(result2).toBe(false); + array.push(3); + // then + expect(result).toBe(false); + expect(result2).toBe(false); + expect(resultValue).toEqual(undefined);// eslint-disable-line no-undefined + // noinspection JSUnusedAssignment + expect(resultValue2).toEqual(undefined);// eslint-disable-line no-undefined + expect(object.arr).toEqual([ 1, 2, 3 ]); + }); + it('should throw error when observing non-existing property', () => { // given const nonExisting = '___non-existing___';