Skip to content

Commit

Permalink
feat(dataSource): Added pluggable dataSource ability and created Loca…
Browse files Browse the repository at this point in the history
…lStorageDataSource

Closes #90
  • Loading branch information
christopherthielen committed Jan 16, 2019
1 parent 7f8578a commit 1a934b4
Show file tree
Hide file tree
Showing 6 changed files with 252 additions and 73 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
With Deep State Redirect, a parent state remembers whatever child state was last activated.
When the user directly reactivates the parent state, they are redirected to the nested state (which was previously activated).


## Overview and Use Case

Deep State Redirect (DSR) is a marker you can add to a state definition.
Expand All @@ -31,3 +30,26 @@ If used with a Sticky State, the states will be reactivated, and the DOM will be
See: http://christopherthielen.github.io/ui-router-extras/#/dsr

TODO: Move docs here

### Using a custom DataStore

By default DSR stores the most recent redirects in memory.
Alternatively, you can store the redirects in Local Storage using
[LocalStorageDataStore](https:/ui-router/dsr/blob/master/src/DSRDataStore.ts)
or create your own DataStore.

When registering the DSRPlugin, pass an options object with a `dataStore` property, i.e.:

```js
router.plugin(DSRPlugin, { dataStore: new LocalStorageDataStore() });
```

## Example Builds

The [`/examples` directory](https:/ui-router/dsr/tree/master/examples) contains example setups for:

- Angular-CLI
- AngularJS + bower + script tags
- AngularJS + npm + script tags
- AngularJS + webpack
- Create-React-App
84 changes: 84 additions & 0 deletions src/DSRDataStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { StateOrName, UIRouter } from '@uirouter/core';
import { RecordedDSR } from './interface';

export interface DSRDataStore {
init(router: UIRouter): void;
// Gets the remembered DSR target state for a given state and params
get(state: StateOrName): RecordedDSR[];
// Sets the remembered DSR target state for a given state and params
set(state: StateOrName, recordedDSR: RecordedDSR[] | undefined): void;
}

export class StateObjectDataStore implements DSRDataStore {
private router: UIRouter;

private getState(stateOrName: StateOrName) {
const state = this.router.stateService.get(stateOrName);
return state && state.$$state();
}

public init(router: UIRouter): void {
this.router = router;
}

public get(stateOrName: StateOrName): RecordedDSR[] {
return this.getState(stateOrName).$dsr || [];
}

public set(stateOrName: StateOrName, recordedDsr: RecordedDSR[]): void {
const state = this.getState(stateOrName);
if (recordedDsr) {
state.$dsr = recordedDsr;
} else {
delete state.$dsr;
}
}
}

export class LocalStorageDataStore implements DSRDataStore {
private router: UIRouter;
private key = 'uiRouterDeepStateRedirect';

private getStore() {
const item = localStorage.getItem(this.key);
return JSON.parse(item || '{}');
}

private setStore(contents: any) {
if (contents) {
try {
localStorage.setItem(this.key, JSON.stringify(contents));
} catch (err) {
console.error(
'UI-Router Deep State Redirect: cannot store object in LocalStorage. Is there a circular reference?',
contents
);
console.error(err);
}
} else {
localStorage.removeItem(this.key);
}
}

private getStateName(stateOrName: StateOrName) {
const state = this.router.stateService.get(stateOrName);
return state && state.name;
}

public init(router: UIRouter): void {
this.router = router;
}

public get(stateOrName: StateOrName): RecordedDSR[] {
const stateName = this.getStateName(stateOrName);
const store = this.getStore();
return store[stateName] || [];
}

public set(stateOrName: StateOrName, recordedDsr: RecordedDSR[]): void {
const stateName = this.getStateName(stateOrName);
const store = this.getStore();
store[stateName] = recordedDsr;
this.setStore(store);
}
}
67 changes: 40 additions & 27 deletions src/dsr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,31 @@ import {
StateService,
} from '@uirouter/core';

import { DSRDataStore, StateObjectDataStore } from './DSRDataStore';
import { _DSRConfig, DSRConfigObj, DSRFunction, DSRProp, ParamPredicate, RecordedDSR } from './interface';

class DSRPlugin implements UIRouterPlugin {
name = 'deep-state-redirect';

dataStore: DSRDataStore;
$transitions: TransitionService;
$state: StateService;
hookDeregFns = [];

constructor($uiRouter: UIRouter) {
constructor($uiRouter: UIRouter, options: { dataStore: DSRDataStore }) {
this.$transitions = $uiRouter.transitionService;
this.$state = $uiRouter.stateService;
this.dataStore = (options && options.dataStore) || new StateObjectDataStore();
this.dataStore.init($uiRouter);

this.hookDeregFns.push(
this.$transitions.onRetain({ retained: state => !!this.getDsrProp(state.self) }, this.recordDeepState.bind(this)),
this.$transitions.onRetain({ retained: state => !!this.getDsrProp(state.self) }, this.recordDeepState.bind(this))
);
this.hookDeregFns.push(
this.$transitions.onEnter({ entering: state => !!this.getDsrProp(state.self) }, this.recordDeepState.bind(this)),
this.$transitions.onEnter({ entering: state => !!this.getDsrProp(state.self) }, this.recordDeepState.bind(this))
);
this.hookDeregFns.push(
this.$transitions.onBefore({ to: state => !!this.getDsrProp(state.self) }, this.deepStateRedirect.bind(this)),
this.$transitions.onBefore({ to: state => !!this.getDsrProp(state.self) }, this.deepStateRedirect.bind(this))
);
}

Expand All @@ -58,12 +62,13 @@ class DSRPlugin implements UIRouterPlugin {
reset(state?: StateOrName, params?: RawParams): void {
const { $state } = this;
if (!state) {
$state.get().forEach(_state => delete _state.$$state().$dsr);
$state.get().forEach(_state => this.dataStore.set(_state, undefined));
} else if (!params) {
delete $state.get(state).$$state().$dsr;
this.dataStore.set(state, undefined);
} else {
const currentDSRS = this.dataStore.get(state);
const $$state = $state.get(state).$$state();
$$state.$dsr = ($$state.$dsr as RecordedDSR[]).filter(this.paramsEqual($$state, params, undefined, true));
this.dataStore.set(state, currentDSRS.filter(this.paramsEqual($$state, params, undefined, true)));
}
}

Expand All @@ -85,7 +90,7 @@ class DSRPlugin implements UIRouterPlugin {
return state.deepStateRedirect || state.dsr;
}

private getConfig(state: StateDeclaration): _DSRConfig {
public getConfig(state: StateDeclaration): _DSRConfig {
const { $state } = this;
const dsrProp: DSRProp = this.getDsrProp(state);
if (typeof dsrProp === 'undefined') return;
Expand Down Expand Up @@ -116,11 +121,11 @@ class DSRPlugin implements UIRouterPlugin {
return { default: defaultTarget, params, fn };
}

private paramsEqual(
public paramsEqual(
state: StateObject,
transParams: RawParams,
paramPredicate: ParamPredicate = () => true,
negate = false,
negate = false
): (redirect: RecordedDSR) => boolean {
const schema = state.parameters({ inherit: true }).filter(paramPredicate);

Expand All @@ -132,26 +137,29 @@ class DSRPlugin implements UIRouterPlugin {

private recordDeepState(transition: Transition, state: StateDeclaration): void {
const { $state } = this;
const paramsConfig = this.getConfig(state).params;
const _state = state.$$state();
const hasParamsConfig: boolean = !!this.getConfig(state).params;
const _state: StateObject = state.$$state();

transition.promise.then(() => {
const transTo = transition.to();
const transParams = transition.params();
const recordedDsrTarget = $state.target(transTo, transParams);

if (paramsConfig) {
const recordedDSR = (_state.$dsr as RecordedDSR[]) || [];
const predicate = this.paramsEqual(transTo.$$state(), transParams, undefined, true);
_state.$dsr = recordedDSR.filter(predicate);
_state.$dsr.push({ triggerParams: transParams, target: recordedDsrTarget });
const triggerParams = transition.params();
const target: TargetState = $state.target(transTo, triggerParams);
const targetStateName = target.name();
const targetParams = target.params();
const recordedDSR: RecordedDSR = { triggerParams, targetStateName, targetParams };

if (hasParamsConfig) {
const currentDSRS: RecordedDSR[] = this.dataStore.get(_state);
const predicate = this.paramsEqual(transTo.$$state(), triggerParams, undefined, true);
const updatedDSRS = currentDSRS.filter(predicate).concat(recordedDSR);
this.dataStore.set(_state, updatedDSRS);
} else {
_state.$dsr = recordedDsrTarget;
this.dataStore.set(_state, [recordedDSR]);
}
});
}

private deepStateRedirect(transition: Transition) {
private deepStateRedirect(transition: Transition): TargetState | undefined {
const opts = transition.options();
if (opts['ignoreDsr'] || (opts.custom && opts.custom.ignoreDsr)) return;

Expand All @@ -165,22 +173,27 @@ class DSRPlugin implements UIRouterPlugin {
return redirect;
}

private getTargetState(dsr: RecordedDSR): TargetState {
return this.$state.target(dsr.targetStateName, dsr.targetParams);
}

private getDeepStateRedirect(stateOrName: StateOrName, params: RawParams): TargetState {
const { $state } = this;
const _state = $state.get(stateOrName);
const state = _state && _state.$$state();
const config: _DSRConfig = this.getConfig(_state);
let dsrTarget: TargetState;
const currentDSRS = this.dataStore.get(stateOrName);
let recordedDSR: RecordedDSR;

if (config.params) {
const predicate = this.paramsEqual(state, params, config.params, false);
const match = state.$dsr && (state.$dsr as RecordedDSR[]).filter(predicate)[0];
dsrTarget = match && match.target;
const match = currentDSRS.find(predicate);
recordedDSR = match && match;
} else {
dsrTarget = state.$dsr as TargetState;
recordedDSR = currentDSRS[0] && currentDSRS[0];
}

dsrTarget = dsrTarget || config.default;
let dsrTarget = recordedDSR ? this.getTargetState(recordedDSR) : config.default;

if (dsrTarget) {
// merge original params with deep state redirect params
Expand Down
5 changes: 3 additions & 2 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ declare module '@uirouter/core/lib/state/interface' {

declare module '@uirouter/core/lib/state/stateObject' {
interface StateObject {
$dsr: TargetState | RecordedDSR[];
$dsr: RecordedDSR[];
}
}

Expand All @@ -35,6 +35,7 @@ export interface _DSRConfig {
}

export interface RecordedDSR {
target: TargetState;
targetStateName: string;
targetParams: RawParams;
triggerParams: object;
}
42 changes: 25 additions & 17 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
{
"extends": [ "tslint-eslint-rules" ],
"extends": ["tslint-eslint-rules"],
"rules": {
"align": [true, "parameters", "statements"],
"ban": false,
"class-name": true,
"comment-format": [ true, "check-space" ],
"comment-format": [true, "check-space"],
"curly": false,
"eofline": true,
"forin": true,
"indent": [ true, "spaces" ],
"indent": [true, "spaces"],
"label-position": true,
"max-line-length": [true, 180],
"member-access": false,
"member-ordering": [ true, "static-before-instance", "variables-before-functions" ],
"member-ordering": [true, "static-before-instance", "variables-before-functions"],
"no-arg": true,
"no-bitwise": true,
"no-conditional-assignment": true,
"no-console": [ true, "log", "warn", "debug", "info", "time", "timeEnd", "trace" ],
"no-console": [true, "log", "warn", "debug", "info", "time", "timeEnd", "trace"],
"no-construct": true,
"no-debugger": true,
"no-duplicate-variable": true,
Expand All @@ -27,25 +27,33 @@
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": true,
"no-unused-expression": [ true, "allow-fast-null-checks" ],
"no-unused-expression": [true, "allow-fast-null-checks"],
"no-unused-variable": true,
"no-use-before-declare": true,
"no-var-keyword": true,
"object-curly-spacing": "always",
"object-literal-sort-keys": false,
"one-line": [ true, "check-catch", "check-else", "check-open-brace", "check-whitespace" ],
"prefer-const": [ true, { "destructuring": "all" } ],
"quotemark": [ true, "single", "avoid-escape", "jsx-double" ],
"one-line": [true, "check-catch", "check-else", "check-open-brace", "check-whitespace"],
"prefer-const": [true, { "destructuring": "all" }],
"quotemark": [true, "single", "avoid-escape", "jsx-double"],
"radix": true,
"semicolon": [ true, "always", "ignore-bound-class-methods", "ignore-interfaces" ],
"trailing-comma": [true, {"multiline": "always", "singleline": "never"}],
"triple-equals": [ true, "allow-null-check" ],
"typedef": [ "call-signature", "property-declaration" ],
"typedef-whitespace": [ true, { "call-signature": "nospace", "index-signature": "nospace", "parameter": "nospace", "property-declaration": "nospace", "variable-declaration": "nospace" } ],
"variable-name": [ true, "ban-keywords", "allow-leading-underscore" ],
"whitespace": [ true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type" ],
"semicolon": [true, "always", "ignore-bound-class-methods", "ignore-interfaces"],
"trailing-comma": [true],
"triple-equals": [true, "allow-null-check"],
"typedef": ["call-signature", "property-declaration"],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
}
],
"variable-name": [true, "ban-keywords", "allow-leading-underscore"],
"whitespace": [true, "check-branch", "check-decl", "check-operator", "check-separator", "check-type"],
"jsx-no-multiline-js": false,
"jsx-no-lambda": false
}
}

Loading

0 comments on commit 1a934b4

Please sign in to comment.