Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

StoreProvider for injecting store state into a widget tree #77

Merged
merged 3 commits into from
Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 57 additions & 52 deletions src/stores/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ An application store designed to complement @dojo/widgets and @dojo/widget-core
- [Initial State](#initial-state)
- [How Does This Differ From Redux](#how-does-this-differ-from-Redux)
- [Advanced](#advanced)
- [Connecting Stores To Widgets](#connecting-stores-to-widgets)
- [Subscribing To Store Changes](#subscribing-to-store-changes)
- [Connecting Store Updates To Widgets](#connecting-store-updates-to-widgets)
- [Transforming Executor Arguments](#transforming-executor-arguments)
- [Optimistic Update Pattern](#optimistic-update-pattern)
- [Executing Concurrent Commands](#executing-concurrent-commands)
Expand Down Expand Up @@ -328,46 +328,25 @@ Additionally, this means that there is no need to coordinate `actions` and `redu

## Advanced

### Subscribing to store changes

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.
});
```
### Connecting Store To Widgets

In order to be notified when any changes occur within the store's state, simply register to the stores `.on()` method for a type of `invalidate` passing the function to be called.
Store data can be connected to widgets within your application using the `StoreProvider` widget provided by `@dojo/framework/stores`.

```ts
store.on('invalidate', () => {
// do something when the store's state has been updated.
});
```
Container Property API:

### Connecting Store Updates To Widgets
* `renderer`: A render function that has the store injected in order to access state and pass processes to child widgets.
* `stateKey`: The key of the state in the registry.
* `paths` (optional): A function to connect the `Container` to sections of the state.

Store data can be connected to widgets within your application using the [Containers & Injectors Pattern](../widget-core#containers--injectors) supported by `@dojo/widget-core`. The `@dojo/framework/stores` package provides a specialized injector that invalidates store containers on two conditions:
There are two mechanisms to connect the `StoreProvider` to the `Store`:

1. The recommended approach is to register `paths` on container creation to ensure invalidation will only occur when state you are interested in changes.
2. A catch-all when no `paths` are defined for the container, it will invalidate when any data changes in the store.

```ts
import { WidgetBase } from '@dojo/widget-core/WidgetBase';
import { Store } from '@dojo/framework/stores/Stores';
import { StoreContainer } from '@dojo/framework/stores/StoreInjector';
import StoreProvider from '@dojo/framework/stores/StoreProvider';

interface State {
foo: string;
Expand All @@ -376,37 +355,63 @@ interface State {
}
}

// Will only invalidate when the `foo` or `bar/baz` property is changed
const Container = StoreContainer<State>(WidgetBase, 'state', {
paths(path) {
return [
path('foo'),
path('bar', 'baz')
];
},
getProperties(store: Store<State>) {
return {
foo: store.get(store.path('foo'))
};
}
});

// Catch all, will invalidate if _any_ state changes in the store even if the container is not interested in the changes
const Container = StoreContainer<State>(WidgetBase, 'state', { getProperties(store: Store<State>) {
return {
foo: store.get(store.path('foo'))
class MyApp extends WidgetBase {
protected render() {
return w(StoreProvider, { stateKey: 'state', renderer: (store: Store<State>) => {
return v('div', [ store.get(store.path('foo')) ]);
}});
}
}});
}
```

Instead of typing `StoreContainer` for every usage, it is possible to create a pre-typed StoreContainer that can be used across your application. To do this use `createStoreContainer` from `@dojo/framework/stores/StoreInjector` passing the stores state interface as the generic argument. The result of this can be exported and used across your application.
A pre-typed container can be created by extending the standard `StoreProvider` and passing the `State` type as a generic.

```ts
interface State {
foo: string;
}

export const StoreContainer = createStoreContainer<State>();
export class MyTypeStoreProvider extends StoreProvider<State> {}
```

**However** in order for TypeScript to infer this correctly when using `w()`, the generic will need to be explicitly passed.

```ts
w<MyTypeStoreProvider>(MyTypeStoreProvider, {
stateKey: 'state',
renderer(store) {
return v('div', [ store.get(store.path('foo')) ]);
}
});
```

### Subscribing to store changes

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()` method for a type of `invalidate` passing the function to be called.

```ts
store.on('invalidate', () => {
// do something when the store's state has been updated.
});
```

### Transforming Executor Arguments
Expand Down
8 changes: 3 additions & 5 deletions src/stores/StoreInjector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import { handleDecorator } from '../widget-core/decorators/handleDecorator';
import { beforeProperties } from '../widget-core/decorators/beforeProperties';
import { alwaysRender } from '../widget-core/decorators/alwaysRender';
import { InjectorItem, RegistryLabel, Constructor, DNode } from '../widget-core/interfaces';
import { Store, StatePaths, Path } from './Store';
import { Store, Path } from './Store';
import { Registry } from '../widget-core/Registry';
import { GetPaths } from './StoreProvider';
export { GetPaths } from './StoreProvider';
const registeredInjectorsMap: WeakMap<WidgetBase, InjectorItem<Store>[]> = new WeakMap();

export interface GetProperties<S extends Store, W extends WidgetBase<any, any> = WidgetBase<any, any>> {
Expand All @@ -27,10 +29,6 @@ export interface StoreContainerOptions<S, W extends WidgetBase> {
getProperties: GetProperties<Store<S>, W>;
}

export interface GetPaths<S = any> {
(path: StatePaths<S>): Path<S, any>[];
}

export interface StoreInjectConfig<S = any> {
name: RegistryLabel;
getProperties: GetProperties<Store<S>, any>;
Expand Down
77 changes: 77 additions & 0 deletions src/stores/StoreProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import WidgetBase from '../widget-core/WidgetBase';
import { DNode } from '../widget-core/interfaces';
import { Store, StatePaths, Path } from './Store';
import { diffProperty } from '../widget-core/decorators/diffProperty';
import { Handle } from '../core/Destroyable';
import { shallow } from '../widget-core/diff';

export interface GetPaths<S = any> {
(path: StatePaths<S>): Path<S, any>[];
}

export interface StoreProviderProperties<S = any> {
renderer: (store: Store<S>) => DNode | DNode[];
stateKey: string;
paths?: GetPaths<S>;
}

function mockPath(...paths: string[]): string {
return paths.join(',');
}

function pathDiff(previousProperty: Function, newProperty: Function) {
const previousPaths = previousProperty ? previousProperty(mockPath) : [];
const currentPaths = newProperty ? newProperty(mockPath) : [];
const result = shallow(previousPaths, currentPaths);
return {
changed: result.changed,
value: newProperty
};
}

export class StoreProvider<S = any> extends WidgetBase<StoreProviderProperties<S>, never> {
private _handle: Handle | undefined;

private _getStore(key: string): Store<S> | undefined {
const item = this.registry.getInjector<Store<S>>(key);
if (item) {
return item.injector();
}
}

@diffProperty('stateKey')
@diffProperty('paths', pathDiff)
protected onChange(previousProperties: StoreProviderProperties, currentProperties: StoreProviderProperties) {
const { stateKey, paths } = currentProperties;
if (this._handle) {
this._handle.destroy();
this._handle = undefined;
}
const store = this._getStore(stateKey);
if (store) {
if (paths) {
const handle = store.onChange(paths(store.path), () => this.invalidate());
this._handle = {
destroy: () => {
handle.remove();
}
};
} else {
this._handle = store.on('invalidate', () => {
this.invalidate();
});
}
this.own(this._handle);
}
}

protected render(): DNode | DNode[] {
const { stateKey, renderer } = this.properties;
const store = this._getStore(stateKey);
if (store) {
return renderer(store);
}
}
}

export default StoreProvider;
2 changes: 1 addition & 1 deletion src/stores/middleware/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import global from '../../shim/global';
import { ProcessError, ProcessResult, ProcessCallback, processExecutor } from '../process';
import { Store } from '../Store';
import { GetPaths } from '../StoreInjector';
import { GetPaths } from '../StoreProvider';
import { add } from '../state/operations';

export function collector<T = any>(id: string, getPaths: GetPaths<T>, callback?: ProcessCallback): ProcessCallback {
Expand Down
Loading