Add unobserve method
This commit is contained in:
parent
a0c4cf78e9
commit
ae6b90bee3
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user