Skip to content

Commit

Permalink
feat(redirectTo): Process redirectTo property of a state as a redir…
Browse files Browse the repository at this point in the history
…ect string/object/hook function

Closes #27
Closes #948
  • Loading branch information
christopherthielen committed Jun 26, 2016
1 parent 771c4ab commit 6becb12
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 2 deletions.
31 changes: 31 additions & 0 deletions src/hooks/redirectTo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {isString, isFunction} from "../common/predicates"
import {UIRInjector} from "../common/interface";
import {Transition} from "../transition/transition";
import {UiRouter} from "../router";
import {services} from "../common/coreservices";
import {TargetState} from "../state/targetState";

/**
* A hook that redirects to a different state or params
*
* See [[StateDeclaration.redirectTo]]
*/
export const redirectToHook = (transition: Transition, $injector: UIRInjector) => {
let redirect = transition.to().redirectTo;
if (!redirect) return;

let router: UiRouter = $injector.get(UiRouter);
let $state = router.stateService;

if (isFunction(redirect))
return services.$q.when(redirect(transition, $injector)).then(handleResult);

return handleResult(redirect);

function handleResult(result) {
if (result instanceof TargetState) return result;
if (isString(result)) return $state.target(<any> result, transition.params(), transition.options());
if (result['state'] || result['params'])
return $state.target(result['state'] || transition.to(), result['params'] || transition.params(), transition.options());
}
};
67 changes: 65 additions & 2 deletions src/state/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {Transition} from "../transition/transition";
import {TransitionStateHookFn} from "../transition/interface";
import {ResolvePolicy, ResolvableLiteral} from "../resolve/interface";
import {Resolvable} from "../resolve/resolvable";
import {UIRInjector} from "../common/interface";
import {TargetState} from "./targetState";

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

Expand Down Expand Up @@ -388,12 +390,73 @@ export interface StateDeclaration {
/**
* An inherited property to store state data
*
* This is a spot for you to store inherited state metadata. Child states' `data` object will
* prototypically inherit from the parent state .
* This is a spot for you to store inherited state metadata.
* Child states' `data` object will prototypally inherit from their parent state.
*
* This is a good spot to put metadata such as `requiresAuth`.
*
* Note: because prototypal inheritance is used, changes to parent `data` objects reflect in the child `data` objects.
* Care should be taken if you are using `hasOwnProperty` on the `data` object.
* Properties from parent objects will return false for `hasOwnProperty`.
*/
data?: any;

/**
* Synchronously or asynchronously redirects Transitions to a different state/params
*
* If this property is defined, a Transition directly to this state will be redirected based on the property's value.
*
* - If the value is a `string`, the Transition is redirected to the state named by the string.
*
* - If the property is an object with a `state` and/or `params` property,
* the Transition is redirected to the named `state` and/or `params`.
*
* - If the value is a [[TargetState]] the Transition is redirected to the `TargetState`
*
* - If the property is a function:
* - The function is called with two parameters:
* - The current [[Transition]]
* - An injector which can be used to get dependencies using [[UIRInjector.get]]
* - The return value is processed using the previously mentioned rules.
* - If the return value is a promise, the promise is waited for, then the resolved async value is processed using the same rules.
*
* @example
* ```js
*
* // a string
* .state('A', {
* redirectTo: 'A.B'
* })
* // a {state, params} object
* .state('C', {
* redirectTo: { state: 'C.D', params: { foo: 'index' } }
* })
* // a fn
* .state('E', {
* redirectTo: () => "A"
* })
* // a fn conditionally returning a {state, params}
* .state('F', {
* redirectTo: (trans) => {
* if (trans.params().foo < 10)
* return { state: 'F', params: { foo: 10 } };
* }
* })
* // a fn returning a promise for a redirect
* .state('G', {
* redirectTo: (trans, injector) => {
* let svc = injector.get('SomeService')
* let promise = svc.getAsyncRedirect(trans.params.foo);
* return promise;
* }
* })
*/
redirectTo?: (
($transition$: Transition, $injector: UIRInjector) => TargetState |
{ state: (string|StateDeclaration), params: { [key: string]: any }} |
string
)

/**
* A Transition Hook called with the state is being entered. See: [[IHookRegistry.onEnter]]
*
Expand Down
6 changes: 6 additions & 0 deletions src/transition/transitionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {loadEnteringViews, activateViews} from "../hooks/views";
import {UiRouter} from "../router";
import {val} from "../common/hof";
import {updateUrl} from "../hooks/url";
import {redirectToHook} from "../hooks/redirectTo";

/**
* The default [[Transition]] options.
Expand Down Expand Up @@ -54,6 +55,7 @@ export class TransitionService implements IHookRegistry {
loadViews: Function;
activateViews: Function;
updateUrl: Function;
redirectTo: Function;
};

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

private registerTransitionHooks() {
let fns = this._deregisterHookFns;

// Wire up redirectTo hook
fns.redirectTo = this.onStart({to: (state) => !!state.redirectTo}, redirectToHook);

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

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

Expand Down
122 changes: 122 additions & 0 deletions test/core/hooksSpec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

import {UiRouter} from "../../src/router";
import {tree2Array} from "../stateHelper.ts";
import {find} from "../../src/common/common";


let statetree = {
A: {
AA: {
AAA: {
url: "/:fooId", params: { fooId: "" }
}
}
}
};

describe("hooks", () => {
let router, $state, states, init;
beforeEach(() => {
router = new UiRouter();
$state = router.stateService;
states = tree2Array(statetree, false);
init = () => {
states.forEach(state => router.stateRegistry.register(state));
router.stateRegistry.stateQueue.autoFlush($state);
}
})

describe('redirectTo:', () => {
it("should redirect to a state by name from the redirectTo: string", (done) => {
find(states, s => s.name === 'A').redirectTo = "AAA";
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA')
done()
})
})

it("should redirect to a state by name from the redirectTo: object", (done) => {
find(states, s => s.name === 'A').redirectTo = { state: "AAA" }
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA')
done()
})
})

it("should redirect to a state and params by name from the redirectTo: object", (done) => {
find(states, s => s.name === 'A').redirectTo = { state: "AAA", params: { fooId: 'abc'} };
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA')
expect(router.globals.params.fooId).toBe('abc')
done()
})
})

it("should redirect to a TargetState returned from the redirectTo: function", (done) => {
find(states, s => s.name === 'A').redirectTo =
() => $state.target("AAA");
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA')
done()
})
})

it("should redirect after waiting for a promise for a state name returned from the redirectTo: function", (done) => {
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
setTimeout(() => resolve("AAA"), 50)
});
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA');
done()
})
})

it("should redirect after waiting for a promise for a {state, params} returned from the redirectTo: function", (done) => {
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
setTimeout(() => resolve({ state: "AAA", params: { fooId: "FOO" } }), 50)
});
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA');
expect(router.globals.params.fooId).toBe('FOO');
done()
})
})

it("should redirect after waiting for a promise for a TargetState returned from the redirectTo: function", (done) => {
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
setTimeout(() => resolve($state.target("AAA")), 50)
});
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('AAA');
done()
})
})

it("should not redirect if the redirectTo: function returns something other than a string, { state, params}, TargetState (or promise for)", (done) => {
find(states, s => s.name === 'A').redirectTo = () => new Promise((resolve) => {
setTimeout(() => resolve(12345), 50)
});
init();

$state.go('A').then(() => {
expect(router.globals.current.name).toBe('A');
done()
})
})
})

});

0 comments on commit 6becb12

Please sign in to comment.