Skip to content
This repository has been archived by the owner on Jul 30, 2018. It is now read-only.

Commit

Permalink
Add a basic onChange mechanism (#148)
Browse files Browse the repository at this point in the history
* 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 b6165e6
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 6 deletions.
21 changes: 20 additions & 1 deletion README.md
Expand Up @@ -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', () => {
Expand Down
64 changes: 64 additions & 0 deletions src/Store.ts
@@ -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
Expand Down Expand Up @@ -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';
}
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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' });
}

Expand Down
72 changes: 67 additions & 5 deletions tests/unit/Store.ts
Expand Up @@ -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', () => {
Expand Down

0 comments on commit b6165e6

Please sign in to comment.