Add observe class
This commit is contained in:
parent
f40e042268
commit
9515e5e278
143
js/src/observe.js
Normal file
143
js/src/observe.js
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* μlogger
|
||||
*
|
||||
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||
*
|
||||
* This is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
export default class uObserve {
|
||||
|
||||
/**
|
||||
* Observe object's property or all properties if not specified.
|
||||
* On change call observer function.
|
||||
* observe(obj, prop, observer) observes given property prop;
|
||||
* observe(obj, observer) observes all properties of object obj.
|
||||
* @param {Object} obj
|
||||
* @param {(string|function)} p1
|
||||
* @param {function=} p2
|
||||
*/
|
||||
static observe(obj, p1, p2) {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid arguments');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe object's proporty. On change call observer
|
||||
* @param {Object} obj
|
||||
* @param {?string} property
|
||||
* @param {function} observer
|
||||
*/
|
||||
static observeProperty(obj, property, observer) {
|
||||
this.addObserver(obj, observer, property);
|
||||
if (!obj.hasOwnProperty('_values')) {
|
||||
Object.defineProperty(obj, '_values', { enumerable: false, configurable: false, value: [] });
|
||||
}
|
||||
obj._values[property] = obj[property];
|
||||
Object.defineProperty(obj, property, {
|
||||
get: () => obj._values[property],
|
||||
set: (newValue) => {
|
||||
if (obj._values[property] !== newValue) {
|
||||
obj._values[property] = newValue;
|
||||
uObserve.notify(obj._observers[property], newValue);
|
||||
}
|
||||
if (Array.isArray(obj[property])) {
|
||||
this.observeArray(obj[property], observer);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (Array.isArray(obj[property])) {
|
||||
this.observeArray(obj[property], observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively add observer to all properties
|
||||
* @param {Object} obj
|
||||
* @param {function} observer
|
||||
*/
|
||||
static observeRecursive(obj, observer) {
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
uObserve.observeProperty(obj, prop, observer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observe array
|
||||
* @param {Object} arr
|
||||
* @param {function} observer
|
||||
*/
|
||||
static observeArray(arr, observer) {
|
||||
this.addObserver(arr, observer);
|
||||
[ 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift' ].forEach(
|
||||
(operation) => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, operation);
|
||||
descriptor.value = function () {
|
||||
const result = Array.prototype[operation].apply(arr, arguments);
|
||||
uObserve.notify(arr._observers, arr);
|
||||
return result;
|
||||
};
|
||||
Object.defineProperty(arr, operation, descriptor);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store observer in object
|
||||
* @param {Object} obj Object
|
||||
* @param {function} observer Observer
|
||||
* @param {string=} property Optional property
|
||||
*/
|
||||
static addObserver(obj, observer, property) {
|
||||
if (!obj.hasOwnProperty('_observers')) {
|
||||
Object.defineProperty(obj, '_observers', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
value: (arguments.length === 3) ? [] : new Set()
|
||||
});
|
||||
}
|
||||
if (arguments.length === 3) {
|
||||
if (!obj._observers[property]) {
|
||||
obj._observers[property] = new Set();
|
||||
}
|
||||
obj._observers[property].add(observer);
|
||||
} else {
|
||||
obj._observers.add(observer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify observers
|
||||
* @param {Set<function>} observers
|
||||
* @param {*} value
|
||||
*/
|
||||
static notify(observers, value) {
|
||||
for (const observer of observers) {
|
||||
(async () => {
|
||||
await observer(value);
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
169
js/test/observe.test.js
Normal file
169
js/test/observe.test.js
Normal file
@ -0,0 +1,169 @@
|
||||
/*
|
||||
* μlogger
|
||||
*
|
||||
* Copyright(C) 2019 Bartek Fabiszewski (www.fabiszewski.net)
|
||||
*
|
||||
* This is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import uObserve from '../src/observe.js';
|
||||
|
||||
describe('Observe tests', () => {
|
||||
let object;
|
||||
let result = false;
|
||||
let resultValue;
|
||||
|
||||
beforeEach(() => {
|
||||
object = { observed: 1, nonObserved: 1 };
|
||||
result = false;
|
||||
});
|
||||
|
||||
describe('when object is observed', () => {
|
||||
|
||||
it('should throw error if observer is missing', () => {
|
||||
expect(() => { uObserve.observe(object, 'observed'); }).toThrow(new Error('Invalid arguments'));
|
||||
});
|
||||
|
||||
it('should notify observers when observed property is modified', () => {
|
||||
// given
|
||||
uObserve.observe(object, 'observed', (value) => {
|
||||
result = true;
|
||||
resultValue = value;
|
||||
});
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
object.observed = 2;
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(resultValue).toBe(2);
|
||||
});
|
||||
|
||||
it('should not notify observers when non-observed property is modified', () => {
|
||||
// given
|
||||
uObserve.observe(object, 'observed', () => {
|
||||
result = true;
|
||||
});
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
object.nonObserved = 2;
|
||||
// then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should not notify observers when modified value is same', () => {
|
||||
// given
|
||||
uObserve.observe(object, 'observed', () => {
|
||||
result = true;
|
||||
});
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
object.observed = 1;
|
||||
// then
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should notify observers when any property is modified', () => {
|
||||
// given
|
||||
uObserve.observe(object, (value) => {
|
||||
result = true;
|
||||
resultValue = value;
|
||||
});
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
object.observed = 2;
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(resultValue).toBe(2);
|
||||
|
||||
// given
|
||||
result = false;
|
||||
resultValue = null;
|
||||
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
object.nonObserved = 2;
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(resultValue).toBe(2);
|
||||
});
|
||||
|
||||
it('should notify observers when observed array property is modified', () => {
|
||||
// given
|
||||
const array = [ 1, 2 ];
|
||||
object = { array: array };
|
||||
uObserve.observe(object, 'array', (value) => {
|
||||
result = true;
|
||||
resultValue = value;
|
||||
});
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
array.push(3);
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(resultValue).toEqual(array);
|
||||
});
|
||||
|
||||
it('should notify observers when observed array object is modified', () => {
|
||||
// given
|
||||
const array = [ 1, 2 ];
|
||||
uObserve.observe(array, (value) => {
|
||||
result = true;
|
||||
resultValue = value;
|
||||
});
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
array.push(3);
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(resultValue).toEqual(array);
|
||||
});
|
||||
|
||||
it('should retain observers after array is reassigned', () => {
|
||||
// given
|
||||
const array = [ 1, 2 ];
|
||||
const newArray = [ 3, 4 ];
|
||||
object = { array: array };
|
||||
uObserve.observe(object, 'array', (value) => {
|
||||
result = true;
|
||||
resultValue = value;
|
||||
});
|
||||
// when
|
||||
object.array = newArray;
|
||||
result = false;
|
||||
|
||||
expect(result).toBe(false);
|
||||
object.array.push(5);
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(resultValue).toEqual(newArray);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when notify is called directly', () => {
|
||||
it('should call observers with given value', () => {
|
||||
// given
|
||||
const observers = new Set();
|
||||
let result2 = false;
|
||||
observers.add((value) => { result = value; });
|
||||
observers.add((value) => { result2 = value; });
|
||||
// when
|
||||
expect(result).toBe(false);
|
||||
expect(result2).toBe(false);
|
||||
uObserve.notify(observers, true);
|
||||
// then
|
||||
expect(result).toBe(true);
|
||||
expect(result2).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user