This repository has been archived by the owner. It is now read-only.
Permalink
Browse files

Add a basic onChange mechanism (#148)

* add on change for a path, or group of paths

* update test to show value not changing

* add remove handle

* add readme update
  • Loading branch information...
matt-gadd committed Dec 19, 2017
1 parent 3e6c420 commit b6165e6fd670874726e67ab166cb888967785d19
Showing with 151 additions and 6 deletions.
  1. +20 −1 README.md
  2. +64 −0 src/Store.ts
  3. +67 −5 tests/unit/Store.ts
@@ -195,7 +195,26 @@ Additionally, this means that there is no need to coordinate `actions` and `redu
### Subscribing to store changes
In order to be notified when changes occur within the store's state, simply register to the stores `.on()` for a type of `invalidate` passing the function to be called.
To be notified of changes in the store, use the `onChange()` function, which takes a `path` or an array of `path`'s and a callback for when that portion of state changes, for example:
```ts
store.onChange(store.path('foo', 'bar'), () => {
// do something when the state at foo/bar has been updated.
});
```
or
```ts
store.onChange([
store.path('foo', 'bar'),
store.path('baz')
], () => {
// do something when the state at /foo/bar or /baz has been updated.
});
```
In order to be notified when any changes occur within the store's state, simply register to the stores `.on()` for a type of `invalidate` passing the function to be called.
```ts
store.on('invalidate', () => {
@@ -1,6 +1,7 @@
import { Evented } from '@dojo/core/Evented';
import { Patch, PatchOperation } from './state/Patch';
import { Pointer } from './state/Pointer';
import Map from '@dojo/shim/Map';
/**
* The "path" to a value of type T on and object of type M. The path string is a JSON Pointer to the location of
@@ -62,6 +63,16 @@ export interface State<M> {
at<S extends Path<M, Array<any>>>(path: S, index: number): Path<M, S['value'][0]>;
}
interface OnChangeCallback {
callbackId: number;
callback: () => void;
}
interface OnChangeValue {
callbacks: OnChangeCallback[];
previousValue: any;
}
function isString(segment?: string): segment is string {
return typeof segment === 'string';
}
@@ -76,6 +87,10 @@ export class Store<T = any> extends Evented implements State<T> {
*/
private _state = {} as T;
private _changePaths = new Map<string, OnChangeValue>();
private _callbackId = 0;
/**
* Returns the state at a specific pointer path location.
*/
@@ -107,10 +122,59 @@ export class Store<T = any> extends Evented implements State<T> {
};
}
public onChange = <U = any>(paths: Path<T, U> | Path<T, U>[], callback: () => void) => {
const callbackId = this._callbackId;
if (!Array.isArray(paths)) {
paths = [ paths ];
}
paths.forEach((path) => this._addOnChange(path, callback, callbackId));
this._callbackId += 1;
return {
remove: () => {
(paths as Path<T, U>[]).forEach((path) => {
const onChange = this._changePaths.get(path.path);
if (onChange) {
onChange.callbacks = onChange.callbacks.filter((callback) => {
return callback.callbackId !== callbackId;
});
}
});
}
};
}
private _addOnChange = <U = any>(path: Path<T, U>, callback: () => void, callbackId: number): void => {
let changePaths = this._changePaths.get(path.path);
if (!changePaths) {
changePaths = { callbacks: [], previousValue: this.get(path) };
}
changePaths.callbacks.push({ callbackId, callback });
this._changePaths.set(path.path, changePaths);
}
private _runOnChanges() {
const callbackIdsCalled: number[] = [];
this._changePaths.forEach((value: OnChangeValue, path: string) => {
const { previousValue, callbacks } = value;
const newValue = new Pointer(path).get(this._state);
if (previousValue !== newValue) {
this._changePaths.set(path, { callbacks, previousValue: newValue });
callbacks.forEach((callbackItem) => {
const { callback, callbackId } = callbackItem;
if (callbackIdsCalled.indexOf(callbackId) === -1) {
callbackIdsCalled.push(callbackId);
callback();
}
});
}
});
}
/**
* Emits an invalidation event
*/
public invalidate(): any {
this._runOnChanges();
this.emit({ type: 'invalidate' });
}
@@ -23,11 +23,73 @@ describe('store', () => {
it('apply/get', () => {
const undo = store.apply(testPatchOperations);
assert.strictEqual(store.get(store.path('test')), 'test');
assert.deepEqual(undo, [
{ op: OperationType.TEST, path: new Pointer('/test'), value: 'test' },
{ op: OperationType.REMOVE, path: new Pointer('/test') }
]);
assert.strictEqual(store.get(store.path('test')), 'test');
assert.deepEqual(undo, [
{ op: OperationType.TEST, path: new Pointer('/test'), value: 'test' },
{ op: OperationType.REMOVE, path: new Pointer('/test') }
]);
});
it('should allow paths to be registered to an onChange', () => {
let first = 0;
let second = 0;
const { onChange, path, apply } = store;
onChange(path('foo', 'bar'), () => first += 1);
onChange([
path('foo', 'bar'),
path('baz')
], () => second += 1);
apply([
{ op: OperationType.ADD, path: new Pointer('/foo/bar'), value: 'test' },
{ op: OperationType.ADD, path: new Pointer('/baz'), value: 'hello' }
], true);
assert.strictEqual(first, 1);
assert.strictEqual(second, 1);
apply([
{ op: OperationType.ADD, path: new Pointer('/foo/bar'), value: 'test' },
{ op: OperationType.ADD, path: new Pointer('/baz'), value: 'world' }
], true);
assert.strictEqual(first, 1);
assert.strictEqual(second, 2);
});
it('can remove a registered onChange', () => {
let first = 0;
let second = 0;
const { onChange, path, apply } = store;
const { remove } = onChange(path('foo', 'bar'), () => first += 1);
onChange([
path('foo', 'bar'),
path('baz')
], () => second += 1);
apply([
{ op: OperationType.ADD, path: new Pointer('/foo/bar'), value: 'test' },
{ op: OperationType.ADD, path: new Pointer('/baz'), value: 'hello' }
], true);
assert.strictEqual(first, 1);
assert.strictEqual(second, 1);
remove();
apply([
{ op: OperationType.ADD, path: new Pointer('/foo/bar'), value: 'test2' },
{ op: OperationType.ADD, path: new Pointer('/baz'), value: 'hello2' }
], true);
assert.strictEqual(first, 1);
assert.strictEqual(second, 2);
});
it('invalidate', () => {

0 comments on commit b6165e6

Please sign in to comment.