Add unobserve method

This commit is contained in:
Bartek Fabiszewski 2019-11-30 12:46:45 +01:00
parent a0c4cf78e9
commit ae6b90bee3
2 changed files with 298 additions and 14 deletions

View File

@ -44,7 +44,26 @@ export default class uObserve {
} }
/** /**
* Observe object's proporty. On change call observer * Notify callback
* @callback ObserveCallback
* @param {*} value
*/
/**
* Notify observers
* @param {Set<ObserveCallback>} observers
* @param {*} value
*/
static notify(observers, value) {
for (const observer of observers) {
(async () => {
await observer(value);
})();
}
}
/**
* Observe object's property. On change call observer
* @param {Object} obj * @param {Object} obj
* @param {?string} property * @param {?string} property
* @param {ObserveCallback} observer * @param {ObserveCallback} observer
@ -52,7 +71,7 @@ export default class uObserve {
static observeProperty(obj, property, observer) { static observeProperty(obj, property, observer) {
this.addObserver(obj, observer, property); this.addObserver(obj, observer, property);
if (!obj.hasOwnProperty('_values')) { if (!obj.hasOwnProperty('_values')) {
Object.defineProperty(obj, '_values', { enumerable: false, configurable: false, value: [] }); Object.defineProperty(obj, '_values', { enumerable: false, configurable: false, value: {} });
} }
obj._values[property] = obj[property]; obj._values[property] = obj[property];
Object.defineProperty(obj, property, { Object.defineProperty(obj, property, {
@ -131,21 +150,100 @@ export default class uObserve {
} }
/** /**
* Notify observers * Remove observer from object's property or all it's properties
* @param {Set<ObserveCallback>} observers * unobserve(obj, prop, observer) observes given property prop;
* @param {*} value * unobserve(obj, observer) observes all properties of object obj.
* @param {Object} obj
* @param {(string|ObserveCallback)} p1
* @param {ObserveCallback=} p2
*/ */
static notify(observers, value) { static unobserve(obj, p1, p2) {
for (const observer of observers) { if (typeof p2 === 'function') {
(async () => { this.unobserveProperty(obj, p1, p2);
await observer(value); } else if (typeof p1 === 'function') {
})(); if (Array.isArray(obj)) {
this.unobserveArray(obj, p1);
} else {
this.unobserveRecursive(obj, p1);
}
} else {
throw new Error('Invalid arguments');
} }
} }
/** /**
* Notify callback * Remove observer from object's property
* @callback ObserveCallback * @param {Object} obj
* @param {*} value * @param {?string} property
* @param {ObserveCallback} observer
*/ */
static unobserveProperty(obj, property, observer) {
if (Array.isArray(obj[property])) {
this.unobserveArray(obj[property], observer);
}
this.removeObserver(obj, observer, property);
if (!obj._observers[property].size) {
delete obj[property];
obj[property] = obj._values[property];
delete obj._values[property];
}
}
/**
* Recursively remove observers from all properties
* @param {Object} obj
* @param {ObserveCallback} observer
*/
static unobserveRecursive(obj, observer) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
uObserve.unobserveProperty(obj, prop, observer);
}
}
}
/**
* Remove observer from array
* @param {Object} arr
* @param {ObserveCallback} observer
*/
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);
});
}
}
/**
* Remove observer from object's property
* @param {Object} obj Object
* @param {string} property Optional property
* @param {ObserveCallback} observer Observer
*/
static removeObserver(obj, observer, property) {
if (!obj.hasOwnProperty('_observers')) {
return;
}
let observers;
if (arguments.length === 3) {
if (!obj._observers[property]) {
return;
}
observers = obj._observers[property];
console.log(`Removing observer for ${property}`)
} else {
observers = obj._observers;
console.log('Removing observer for object…')
}
observers.forEach((obs) => {
if (obs === observer) {
console.log('Removed');
observers.delete(obs);
}
});
}
} }

View File

@ -27,6 +27,8 @@ describe('Observe tests', () => {
beforeEach(() => { beforeEach(() => {
object = { observed: 1, nonObserved: 1 }; object = { observed: 1, nonObserved: 1 };
result = false; result = false;
// eslint-disable-next-line no-undefined
resultValue = undefined;
}); });
describe('when object is observed', () => { describe('when object is observed', () => {
@ -49,6 +51,30 @@ describe('Observe tests', () => {
expect(resultValue).toBe(2); expect(resultValue).toBe(2);
}); });
it('should notify multiple observers when observed property is modified', () => {
// given
let result2 = false;
let resultValue2;
uObserve.observe(object, 'observed', (value) => {
result = true;
resultValue = value;
});
uObserve.observe(object, 'observed', (value) => {
result2 = true;
resultValue2 = value;
});
// when
expect(result).toBe(false);
expect(result2).toBe(false);
object.observed = 2;
// then
expect(result).toBe(true);
expect(resultValue).toBe(2);
expect(result2).toBe(true);
// noinspection JSUnusedAssignment
expect(resultValue2).toBe(2);
});
it('should not notify observers when non-observed property is modified', () => { it('should not notify observers when non-observed property is modified', () => {
// given // given
uObserve.observe(object, 'observed', () => { uObserve.observe(object, 'observed', () => {
@ -114,7 +140,7 @@ describe('Observe tests', () => {
expect(resultValue).toEqual(array); expect(resultValue).toEqual(array);
}); });
it('should notify observers when observed array object is modified', () => { it('should notify observers when observed array is modified', () => {
// given // given
const array = [ 1, 2 ]; const array = [ 1, 2 ];
uObserve.observe(array, (value) => { uObserve.observe(array, (value) => {
@ -150,6 +176,166 @@ describe('Observe tests', () => {
}); });
}); });
describe('when object is unobserved', () => {
it('should throw error if removed observer is missing', () => {
expect(() => {
uObserve.unobserve(object, 'unobserved');
}).toThrow(new Error('Invalid arguments'));
});
it('should not notify observers when unobserved property is modified', () => {
// given
const observer = (value) => {
result = true;
resultValue = value;
};
uObserve.observe(object, 'observed', observer);
// when
uObserve.unobserve(object, 'observed', observer);
expect(result).toBe(false);
object.observed = 2;
// then
expect(result).toBe(false);
// eslint-disable-next-line no-undefined
expect(resultValue).toBe(undefined);
expect(object.observed).toBe(2);
});
it('should not notify observers when any unobserved object property is modified', () => {
// given
const observer = (value) => {
result = true;
resultValue = value;
};
uObserve.observe(object, observer);
// when
uObserve.unobserve(object, observer);
expect(result).toBe(false);
object.observed = 2;
// then
expect(result).toBe(false);
// eslint-disable-next-line no-undefined
expect(resultValue).toBe(undefined);
expect(object.observed).toBe(2);
// given
result = false;
// eslint-disable-next-line no-undefined
resultValue = undefined;
// when
expect(result).toBe(false);
object.nonObserved = 2;
// then
expect(result).toBe(false);
// eslint-disable-next-line no-undefined
expect(resultValue).toBe(undefined);
expect(object.nonObserved).toBe(2);
});
it('should not notify observers when unobserved array property is modified', () => {
// given
const observer = (value) => {
result = true;
resultValue = value;
};
const array = [ 1, 2 ];
object = { array: array };
uObserve.observe(object, 'array', observer);
// when
uObserve.unobserve(object, 'array', observer);
expect(result).toBe(false);
array.push(3);
// then
expect(result).toBe(false);
// eslint-disable-next-line no-undefined
expect(resultValue).toEqual(undefined);
expect(array).toEqual([ 1, 2, 3 ]);
});
it('should not notify observers when unobserved array is modified', () => {
// given
const observer = (value) => {
result = true;
resultValue = value;
};
const array = [ 1, 2 ];
uObserve.observe(array, observer);
// when
uObserve.unobserve(array, observer);
expect(result).toBe(false);
array.push(3);
// then
expect(result).toBe(false);
// eslint-disable-next-line no-undefined
expect(resultValue).toEqual(undefined);
expect(array).toEqual([ 1, 2, 3 ]);
});
it('should remove one of two 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.unobserve(object, 'observed', observer2);
expect(result).toBe(false);
expect(result2).toBe(false);
object.observed = 2;
// then
expect(result).toBe(true);
expect(resultValue).toBe(2);
expect(result2).toBe(false);
// noinspection JSUnusedAssignment
expect(resultValue2).toBe(undefined);// eslint-disable-line no-undefined
});
it('should remove one of two observers from array', () => {
// given
let result2 = false;
let resultValue2;
const observer = (value) => {
result = true;
resultValue = value;
};
const observer2 = (value) => {
result2 = true;
resultValue2 = value;
};
const array = [ 1, 2 ];
uObserve.observe(array, observer);
uObserve.observe(array, observer2);
// when
uObserve.unobserve(array, observer2);
expect(result).toBe(false);
expect(result2).toBe(false);
array.push(3);
// then
expect(result).toBe(true);
expect(result2).toBe(false);
expect(resultValue).toEqual(array);
// noinspection JSUnusedAssignment
expect(resultValue2).toEqual(undefined);// eslint-disable-line no-undefined
expect(array).toEqual([ 1, 2, 3 ]);
});
});
describe('when notify is called directly', () => { describe('when notify is called directly', () => {
it('should call observers with given value', () => { it('should call observers with given value', () => {
// given // given