Skip to content

Commit 6becb12

Browse files
feat(redirectTo): Process redirectTo property of a state as a redirect string/object/hook function
Closes #27 Closes #948
1 parent 771c4ab commit 6becb12

File tree

4 files changed

+224
-2
lines changed

4 files changed

+224
-2
lines changed

src/hooks/redirectTo.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {isString, isFunction} from "../common/predicates"
2+
import {UIRInjector} from "../common/interface";
3+
import {Transition} from "../transition/transition";
4+
import {UiRouter} from "../router";
5+
import {services} from "../common/coreservices";
6+
import {TargetState} from "../state/targetState";
7+
8+
/**
9+
* A hook that redirects to a different state or params
10+
*
11+
* See [[StateDeclaration.redirectTo]]
12+
*/
13+
export const redirectToHook = (transition: Transition, $injector: UIRInjector) => {
14+
let redirect = transition.to().redirectTo;
15+
if (!redirect) return;
16+
17+
let router: UiRouter = $injector.get(UiRouter);
18+
let $state = router.stateService;
19+
20+
if (isFunction(redirect))
21+
return services.$q.when(redirect(transition, $injector)).then(handleResult);
22+
23+
return handleResult(redirect);
24+
25+
function handleResult(result) {
26+
if (result instanceof TargetState) return result;
27+
if (isString(result)) return $state.target(<any> result, transition.params(), transition.options());
28+
if (result['state'] || result['params'])
29+
return $state.target(result['state'] || transition.to(), result['params'] || transition.params(), transition.options());
30+
}
31+
};

src/state/interface.ts

+65-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {Transition} from "../transition/transition";
88
import {TransitionStateHookFn} from "../transition/interface";
99
import {ResolvePolicy, ResolvableLiteral} from "../resolve/interface";
1010
import {Resolvable} from "../resolve/resolvable";
11+
import {UIRInjector} from "../common/interface";
12+
import {TargetState} from "./targetState";
1113

1214
export type StateOrName = (string|StateDeclaration|State);
1315

@@ -388,12 +390,73 @@ export interface StateDeclaration {
388390
/**
389391
* An inherited property to store state data
390392
*
391-
* This is a spot for you to store inherited state metadata. Child states' `data` object will
392-
* prototypically inherit from the parent state .
393+
* This is a spot for you to store inherited state metadata.
394+
* Child states' `data` object will prototypally inherit from their parent state.
393395
*
394396
* This is a good spot to put metadata such as `requiresAuth`.
397+
*
398+
* Note: because prototypal inheritance is used, changes to parent `data` objects reflect in the child `data` objects.
399+
* Care should be taken if you are using `hasOwnProperty` on the `data` object.
400+
* Properties from parent objects will return false for `hasOwnProperty`.
395401
*/
396402
data?: any;
403+
404+
/**
405+
* Synchronously or asynchronously redirects Transitions to a different state/params
406+
*
407+
* If this property is defined, a Transition directly to this state will be redirected based on the property's value.
408+
*
409+
* - If the value is a `string`, the Transition is redirected to the state named by the string.
410+
*
411+
* - If the property is an object with a `state` and/or `params` property,
412+
* the Transition is redirected to the named `state` and/or `params`.
413+
*
414+
* - If the value is a [[TargetState]] the Transition is redirected to the `TargetState`
415+
*
416+
* - If the property is a function:
417+
* - The function is called with two parameters:
418+
* - The current [[Transition]]
419+
* - An injector which can be used to get dependencies using [[UIRInjector.get]]
420+
* - The return value is processed using the previously mentioned rules.
421+
* - If the return value is a promise, the promise is waited for, then the resolved async value is processed using the same rules.
422+
*
423+
* @example
424+
* ```js
425+
*
426+
* // a string
427+
* .state('A', {
428+
* redirectTo: 'A.B'
429+
* })
430+
* // a {state, params} object
431+
* .state('C', {
432+
* redirectTo: { state: 'C.D', params: { foo: 'index' } }
433+
* })
434+
* // a fn
435+
* .state('E', {
436+
* redirectTo: () => "A"
437+
* })
438+
* // a fn conditionally returning a {state, params}
439+
* .state('F', {
440+
* redirectTo: (trans) => {
441+
* if (trans.params().foo < 10)
442+
* return { state: 'F', params: { foo: 10 } };
443+
* }
444+
* })
445+
* // a fn returning a promise for a redirect
446+
* .state('G', {
447+
* redirectTo: (trans, injector) => {
448+
* let svc = injector.get('SomeService')
449+
* let promise = svc.getAsyncRedirect(trans.params.foo);
450+
* return promise;
451+
* }
452+
* })
453+
*/
454+
redirectTo?: (
455+
($transition$: Transition, $injector: UIRInjector) => TargetState |
456+
{ state: (string|StateDeclaration), params: { [key: string]: any }} |
457+
string
458+
)
459+
397460
/**
398461
* A Transition Hook called with the state is being entered. See: [[IHookRegistry.onEnter]]
399462
*

src/transition/transitionService.ts

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {loadEnteringViews, activateViews} from "../hooks/views";
1414
import {UiRouter} from "../router";
1515
import {val} from "../common/hof";
1616
import {updateUrl} from "../hooks/url";
17+
import {redirectToHook} from "../hooks/redirectTo";
1718

1819
/**
1920
* The default [[Transition]] options.
@@ -54,6 +55,7 @@ export class TransitionService implements IHookRegistry {
5455
loadViews: Function;
5556
activateViews: Function;
5657
updateUrl: Function;
58+
redirectTo: Function;
5759
};
5860

5961
constructor(private _router: UiRouter) {
@@ -65,6 +67,9 @@ export class TransitionService implements IHookRegistry {
6567

6668
private registerTransitionHooks() {
6769
let fns = this._deregisterHookFns;
70+
71+
// Wire up redirectTo hook
72+
fns.redirectTo = this.onStart({to: (state) => !!state.redirectTo}, redirectToHook);
6873

6974
// Wire up onExit/Retain/Enter state hooks
7075
fns.onExit = this.onExit({exiting: state => !!state.onExit}, makeEnterExitRetainHook('onExit'));
@@ -75,6 +80,7 @@ export class TransitionService implements IHookRegistry {
7580
fns.eagerResolve = this.onStart({}, $eagerResolvePath, {priority: 1000});
7681
fns.lazyResolve = this.onEnter({ entering: val(true) }, $lazyResolveState, {priority: 1000});
7782

83+
// Wire up the View management hooks
7884
fns.loadViews = this.onStart({}, loadEnteringViews);
7985
fns.activateViews = this.onSuccess({}, activateViews);
8086

test/core/hooksSpec.ts

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
2+
import {UiRouter} from "../../src/router";
3+
import {tree2Array} from "../stateHelper.ts";
4+
import {find} from "../../src/common/common";
5+
6+
7+
let statetree = {
8+
A: {
9+
AA: {
10+
AAA: {
11+
url: "/:fooId", params: { fooId: "" }
12+
}
13+
}
14+
}
15+
};
16+
17+
describe("hooks", () => {
18+
let router, $state, states, init;
19+
beforeEach(() => {
20+
router = new UiRouter();
21+
$state = router.stateService;
22+
states = tree2Array(statetree, false);
23+
init = () => {
24+
states.forEach(state => router.stateRegistry.register(state));
25+
router.stateRegistry.stateQueue.autoFlush($state);
26+
}
27+
})
28+
29+
describe('redirectTo:', () => {
30+
it("should redirect to a state by name from the redirectTo: string", (done) => {
31+
find(states, s => s.name === 'A').redirectTo = "AAA";
32+
init();
33+
34+
$state.go('A').then(() => {
35+
expect(router.globals.current.name).toBe('AAA')
36+
done()
37+
})
38+
})
39+
40+
it("should redirect to a state by name from the redirectTo: object", (done) => {
41+
find(states, s => s.name === 'A').redirectTo = { state: "AAA" }
42+
init();
43+
44+
$state.go('A').then(() => {
45+
expect(router.globals.current.name).toBe('AAA')
46+
done()
47+
})
48+
})
49+
50+
it("should redirect to a state and params by name from the redirectTo: object", (done) => {
51+
find(states, s => s.name === 'A').redirectTo = { state: "AAA", params: { fooId: 'abc'} };
52+
init();
53+
54+
$state.go('A').then(() => {
55+
expect(router.globals.current.name).toBe('AAA')
56+
expect(router.globals.params.fooId).toBe('abc')
57+
done()
58+
})
59+
})
60+
61+
it("should redirect to a TargetState returned from the redirectTo: function", (done) => {
62+
find(states, s => s.name === 'A').redirectTo =
63+
() => $state.target("AAA");
64+
init();
65+
66+
$state.go('A').then(() => {
67+
expect(router.globals.current.name).toBe('AAA')
68+
done()
69+
})
70+
})
71+
72+
it("should redirect after waiting for a promise for a state name returned from the redirectTo: function", (done) => {
73+
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
74+
setTimeout(() => resolve("AAA"), 50)
75+
});
76+
init();
77+
78+
$state.go('A').then(() => {
79+
expect(router.globals.current.name).toBe('AAA');
80+
done()
81+
})
82+
})
83+
84+
it("should redirect after waiting for a promise for a {state, params} returned from the redirectTo: function", (done) => {
85+
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
86+
setTimeout(() => resolve({ state: "AAA", params: { fooId: "FOO" } }), 50)
87+
});
88+
init();
89+
90+
$state.go('A').then(() => {
91+
expect(router.globals.current.name).toBe('AAA');
92+
expect(router.globals.params.fooId).toBe('FOO');
93+
done()
94+
})
95+
})
96+
97+
it("should redirect after waiting for a promise for a TargetState returned from the redirectTo: function", (done) => {
98+
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
99+
setTimeout(() => resolve($state.target("AAA")), 50)
100+
});
101+
init();
102+
103+
$state.go('A').then(() => {
104+
expect(router.globals.current.name).toBe('AAA');
105+
done()
106+
})
107+
})
108+
109+
it("should not redirect if the redirectTo: function returns something other than a string, { state, params}, TargetState (or promise for)", (done) => {
110+
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
111+
setTimeout(() => resolve(12345), 50)
112+
});
113+
init();
114+
115+
$state.go('A').then(() => {
116+
expect(router.globals.current.name).toBe('A');
117+
done()
118+
})
119+
})
120+
})
121+
122+
});

0 commit comments

Comments
 (0)