Skip to content

Commit

Permalink
feat(UrlService): Add rules.initial("/home") to config initial stat…
Browse files Browse the repository at this point in the history
…e (like otherwise)

docs(otherwise): new words

Closes angular-ui/ui-router#3336
  • Loading branch information
christopherthielen committed Mar 16, 2017
1 parent de5c5d0 commit bbe4209
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 34 deletions.
7 changes: 3 additions & 4 deletions src/state/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,9 +435,7 @@ export interface StateDeclaration {
* - 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 [[UIInjector]] which can be used to get dependencies using [[UIInjector.get]] or resolves using [[UIInjector.getAsync]]
* - The function is called with the current [[Transition]]
* - 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.
*
Expand Down Expand Up @@ -489,7 +487,8 @@ export interface StateDeclaration {
* })
* ```
*/
redirectTo?: RedirectToResult | Promise<RedirectToResult>
redirectTo?: RedirectToResult |
((transition: Transition) => Promise<RedirectToResult>);

/**
* A Transition Hook called with the state is being entered. See: [[IHookRegistry.onEnter]]
Expand Down
69 changes: 59 additions & 10 deletions src/url/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,38 +308,87 @@ export interface UrlRulesApi {
when(matcher: (RegExp|UrlMatcher|string), handler: string|UrlRuleHandlerFn, options?: { priority: number }): UrlRule;

/**
* Defines the path or behavior to use when no url can be matched.
* Defines the state, url, or behavior to use when no other rule matches the URL.
*
* - If a string, it is treated as a url redirect
* This rule is matched when *no other rule* matches.
* It is generally used to handle unknown URLs (similar to "404" behavior, but on the client side).
*
* - If `handler` a string, it is treated as a url redirect
*
* #### Example:
* When no other url rule matches, redirect to `/index`
* ```js
* .otherwise('/index');
* ```
*
* - If a function, the function receives the current url ([[UrlParts]]) and the [[UIRouter]] object.
* If the function returns a string, the url is redirected to the return value.
* - If `handler` is an object with a `state` property, the state is activated.
*
* #### Example:
* When no other url rule matches, redirect to `/index`
* When no other url rule matches, redirect to `home` and provide a `dashboard` parameter value.
* ```js
* .otherwise(() => '/index');
* .otherwise({ state: 'home', params: { dashboard: 'default' } });
* ```
*
* - If `handler` is a function, the function receives the current url ([[UrlParts]]) and the [[UIRouter]] object.
* The function can perform actions, and/or return a value.
*
* #### Example:
* When no other url rule matches, go to `home` state
* When no other url rule matches, manually trigger a transition to the `home` state
* ```js
* .otherwise((url, router) => {
* .otherwise((urlParts, router) => {
* router.stateService.go('home');
* return;
* }
* });
* ```
*
* #### Example:
* When no other url rule matches, go to `home` state
* ```js
* .otherwise((urlParts, router) => {
* return { state: 'home' };
* });
* ```
*
* @param handler The url path to redirect to, or a function which returns the url path (or performs custom logic).
*/
otherwise(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef): void;

/**
* Defines the initial state, path, or behavior to use when the app starts.
*
* This rule defines the initial/starting state for the application.
*
* This rule is triggered the first time the URL is checked (when the app initially loads).
* The rule is triggered only when the url matches either `""` or `"/"`.
*
* Note: The rule is intended to be used when the root of the application is directly linked to.
* When the URL is *not* `""` or `"/"` and doesn't match other rules, the [[otherwise]] rule is triggered.
* This allows 404-like behavior when an unknown URL is deep-linked.
*
* #### Example:
* Start app at `home` state.
* ```js
* .initial({ state: 'home' });
* ```
*
* #### Example:
* Start app at `/home` (by url)
* ```js
* .initial('/home');
* ```
*
* #### Example:
* When no other url rule matches, go to `home` state
* ```js
* .initial((url, router) => {
* console.log('initial state');
* return { state: 'home' };
* })
* ```
*
* @param handler The initial state or url path, or a function which returns the state or url path (or performs custom logic).
*/
initial(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef, options?: { priority: number }): void;

/**
* Gets all registered rules
*
Expand Down
47 changes: 30 additions & 17 deletions src/url/urlRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@
* @module url
*/
/** for typedoc */
import { removeFrom, createProxyFunctions, inArray, composeSort, sortBy, extend } from "../common/common";
import { isFunction, isString, isDefined } from "../common/predicates";
import { UrlMatcher } from "./urlMatcher";
import { RawParams } from "../params/interface";
import { Disposable } from "../interface";
import { UIRouter } from "../router";
import { val, is, pattern, prop, pipe } from "../common/hof";
import { UrlRuleFactory } from "./urlRule";
import { TargetState } from "../state/targetState";
import { UrlRule, UrlRuleHandlerFn, UrlParts, UrlRulesApi, UrlSyncApi, MatchResult } from "./interface";
import { TargetStateDef } from "../state/interface";
import { composeSort, createProxyFunctions, extend, inArray, removeFrom, sortBy } from '../common/common';
import { isDefined, isFunction, isString } from '../common/predicates';
import { UrlMatcher } from './urlMatcher';
import { RawParams } from '../params/interface';
import { Disposable } from '../interface';
import { UIRouter } from '../router';
import { is, pattern, pipe, prop, val } from '../common/hof';
import { UrlRuleFactory } from './urlRule';
import { TargetState } from '../state/targetState';
import { MatchResult, UrlParts, UrlRule, UrlRuleHandlerFn, UrlRuleMatchFn, UrlRulesApi, UrlSyncApi } from './interface';
import { TargetStateDef } from '../state/interface';

/** @hidden */
function appendBasePath(url: string, isHtml5: boolean, absolute: boolean, baseHref: string): string {
Expand Down Expand Up @@ -42,7 +42,7 @@ let defaultRuleSortFn: (a: UrlRule, b: UrlRule) => number;
defaultRuleSortFn = composeSort(
sortBy(pipe(prop("priority"), x => -x)),
sortBy(pipe(prop("type"), type => ({ "STATE": 4, "URLMATCHER": 4, "REGEXP": 3, "RAW": 2, "OTHER": 1 })[type])),
(a,b) => (getMatcher(a) && getMatcher(b)) ? UrlMatcher.compare(getMatcher(a), getMatcher(b)) : 0,
(a, b) => (getMatcher(a) && getMatcher(b)) ? UrlMatcher.compare(getMatcher(a), getMatcher(b)) : 0,
sortBy(prop("$id"), inArray([ "REGEXP", "RAW", "OTHER" ])),
);

Expand Down Expand Up @@ -140,7 +140,7 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
$state = router.stateService;

let url: UrlParts = {
path: $url.path(), search: $url.search(), hash: $url.hash()
path: $url.path(), search: $url.search(), hash: $url.hash(),
};

let best = this.match(url);
Expand Down Expand Up @@ -273,15 +273,22 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {

/** @inheritdoc */
otherwise(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef) {
if (!isFunction(handler) && !isString(handler) && !is(TargetState)(handler) && !TargetState.isDef(handler)) {
throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");
}
let handlerFn: UrlRuleHandlerFn = getHandlerFn(handler);

let handlerFn: UrlRuleHandlerFn = isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler);
this._otherwiseFn = this.urlRuleFactory.create(val(true), handlerFn);
this._sorted = false;
};

/** @inheritdoc */
initial(handler: string | UrlRuleHandlerFn | TargetState | TargetStateDef) {
let handlerFn: UrlRuleHandlerFn = getHandlerFn(handler);

let matchFn: UrlRuleMatchFn = (urlParts, router) =>
router.globals.transitionHistory.size() === 0 && !!/^\/?$/.exec(urlParts.path);

this.rule(this.urlRuleFactory.create(matchFn, handlerFn));
};

/** @inheritdoc */
when(matcher: (RegExp|UrlMatcher|string), handler: string|UrlRuleHandlerFn, options?: { priority: number }): UrlRule {
let rule = this.urlRuleFactory.create(matcher, handler);
Expand All @@ -297,3 +304,9 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
};
}

function getHandlerFn(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef): UrlRuleHandlerFn {
if (!isFunction(handler) && !isString(handler) && !is(TargetState)(handler) && !TargetState.isDef(handler)) {
throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");
}
return isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler);
}
2 changes: 1 addition & 1 deletion src/url/urlService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const makeStub = (keys: string[]): any =>
/** @hidden */ const locationServicesFns = ["url", "path", "search", "hash", "onChange"];
/** @hidden */ const locationConfigFns = ["port", "protocol", "host", "baseHref", "html5Mode", "hashPrefix"];
/** @hidden */ const umfFns = ["type", "caseInsensitive", "strictMode", "defaultSquashPolicy"];
/** @hidden */ const rulesFns = ["sort", "when", "otherwise", "rules", "rule", "removeRule"];
/** @hidden */ const rulesFns = ["sort", "when", "initial", "otherwise", "rules", "rule", "removeRule"];
/** @hidden */ const syncFns = ["deferIntercept", "listen", "sync", "match"];

/**
Expand Down
62 changes: 62 additions & 0 deletions test/urlRouterSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,68 @@ describe("UrlRouter", function () {
expect(locationService.path()).toBe("/lastrule");
});

describe('.initial(string)', () => {
beforeEach(() => {
router.stateRegistry.register({ name: 'foo', url: '/foo' });
router.stateRegistry.register({ name: 'bar', url: '/bar' });
router.stateRegistry.register({ name: 'otherwise', url: '/otherwise' });

urlRouter.initial('/foo');
urlRouter.otherwise('/otherwise');
});

it("should activate the initial path when initial path matches ''" , function () {
locationService.url("");
expect(locationService.path()).toBe("/foo");
});

it("should activate the initial path when initial path matches '/'" , function () {
locationService.url("/");
expect(locationService.path()).toBe("/foo");
});

it("should not activate the initial path after the initial transition" , function (done) {
stateService.go('bar').then(() => {
locationService.url("/");
expect(locationService.path()).toBe("/otherwise");
done();
});
});
});

describe('.initial({ state: "state" })', () => {
let goSpy = null;
beforeEach(() => {
router.stateRegistry.register({ name: 'foo', url: '/foo' });
router.stateRegistry.register({ name: 'bar', url: '/bar' });
router.stateRegistry.register({ name: 'otherwise', url: '/otherwise' });

urlRouter.initial({ state: 'foo' });
urlRouter.otherwise({ state: 'otherwise' });

goSpy = spyOn(stateService, "transitionTo").and.callThrough();
});

it("should activate the initial path when initial path matches ''" , function () {
locationService.url("");
expect(goSpy).toHaveBeenCalledWith("foo", undefined, jasmine.anything());
});

it("should activate the initial path when initial path matches '/'" , function () {
locationService.url("/");
expect(goSpy).toHaveBeenCalledWith("foo", undefined, jasmine.anything());
});

it("should not activate the initial path after the initial transition" , function (done) {
stateService.go('bar').then(() => {
locationService.url("/");
expect(goSpy).toHaveBeenCalledWith("otherwise", undefined, jasmine.anything());
done();
});
});
});


it('`rule` should return a deregistration function', function() {
let count = 0;
let rule: UrlRule = {
Expand Down
6 changes: 5 additions & 1 deletion test/urlServiceSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TestingPlugin } from "./_testingPlugin";
import { UrlService } from "../src/url/urlService";

describe('UrlService facade', () => {
var router: UIRouter;
let router: UIRouter;

beforeEach(() => {
router = new UIRouter();
Expand Down Expand Up @@ -111,6 +111,10 @@ describe('UrlService facade', () => {
expectProxyCall(() => router.urlService.rules, router.urlRouter, "otherwise", ["foo"]);
});

it("should pass rules.initial() through to UrlRouter", () => {
expectProxyCall(() => router.urlService.rules, router.urlRouter, "initial", ["foo"]);
});

it("should pass rules.rules() through to UrlRouter", () => {
expectProxyCall(() => router.urlService.rules, router.urlRouter, "rules");
});
Expand Down
2 changes: 1 addition & 1 deletion tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"no-eval": true,
"no-internal-module": true,
"no-require-imports": true,
"no-string-literal": true,
"no-string-literal": false,
"no-switch-case-fall-through": true,
"no-trailing-whitespace": false,
"no-unused-expression": [true, "allow-fast-null-checks"],
Expand Down

0 comments on commit bbe4209

Please sign in to comment.