Skip to content

Commit bbe4209

Browse files
feat(UrlService): Add rules.initial("/home") to config initial state (like otherwise)
docs(otherwise): new words Closes angular-ui/ui-router#3336
1 parent de5c5d0 commit bbe4209

File tree

7 files changed

+161
-34
lines changed

7 files changed

+161
-34
lines changed

src/state/interface.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -435,9 +435,7 @@ export interface StateDeclaration {
435435
* - If the value is a [[TargetState]] the Transition is redirected to the `TargetState`
436436
*
437437
* - If the property is a function:
438-
* - The function is called with two parameters:
439-
* - The current [[Transition]]
440-
* - An [[UIInjector]] which can be used to get dependencies using [[UIInjector.get]] or resolves using [[UIInjector.getAsync]]
438+
* - The function is called with the current [[Transition]]
441439
* - The return value is processed using the previously mentioned rules.
442440
* - If the return value is a promise, the promise is waited for, then the resolved async value is processed using the same rules.
443441
*
@@ -489,7 +487,8 @@ export interface StateDeclaration {
489487
* })
490488
* ```
491489
*/
492-
redirectTo?: RedirectToResult | Promise<RedirectToResult>
490+
redirectTo?: RedirectToResult |
491+
((transition: Transition) => Promise<RedirectToResult>);
493492

494493
/**
495494
* A Transition Hook called with the state is being entered. See: [[IHookRegistry.onEnter]]

src/url/interface.ts

+59-10
Original file line numberDiff line numberDiff line change
@@ -308,38 +308,87 @@ export interface UrlRulesApi {
308308
when(matcher: (RegExp|UrlMatcher|string), handler: string|UrlRuleHandlerFn, options?: { priority: number }): UrlRule;
309309

310310
/**
311-
* Defines the path or behavior to use when no url can be matched.
311+
* Defines the state, url, or behavior to use when no other rule matches the URL.
312312
*
313-
* - If a string, it is treated as a url redirect
313+
* This rule is matched when *no other rule* matches.
314+
* It is generally used to handle unknown URLs (similar to "404" behavior, but on the client side).
315+
*
316+
* - If `handler` a string, it is treated as a url redirect
314317
*
315318
* #### Example:
316319
* When no other url rule matches, redirect to `/index`
317320
* ```js
318321
* .otherwise('/index');
319322
* ```
320323
*
321-
* - If a function, the function receives the current url ([[UrlParts]]) and the [[UIRouter]] object.
322-
* If the function returns a string, the url is redirected to the return value.
324+
* - If `handler` is an object with a `state` property, the state is activated.
323325
*
324326
* #### Example:
325-
* When no other url rule matches, redirect to `/index`
327+
* When no other url rule matches, redirect to `home` and provide a `dashboard` parameter value.
326328
* ```js
327-
* .otherwise(() => '/index');
329+
* .otherwise({ state: 'home', params: { dashboard: 'default' } });
328330
* ```
329331
*
332+
* - If `handler` is a function, the function receives the current url ([[UrlParts]]) and the [[UIRouter]] object.
333+
* The function can perform actions, and/or return a value.
334+
*
330335
* #### Example:
331-
* When no other url rule matches, go to `home` state
336+
* When no other url rule matches, manually trigger a transition to the `home` state
332337
* ```js
333-
* .otherwise((url, router) => {
338+
* .otherwise((urlParts, router) => {
334339
* router.stateService.go('home');
335-
* return;
336-
* }
340+
* });
341+
* ```
342+
*
343+
* #### Example:
344+
* When no other url rule matches, go to `home` state
345+
* ```js
346+
* .otherwise((urlParts, router) => {
347+
* return { state: 'home' };
348+
* });
337349
* ```
338350
*
339351
* @param handler The url path to redirect to, or a function which returns the url path (or performs custom logic).
340352
*/
341353
otherwise(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef): void;
342354

355+
/**
356+
* Defines the initial state, path, or behavior to use when the app starts.
357+
*
358+
* This rule defines the initial/starting state for the application.
359+
*
360+
* This rule is triggered the first time the URL is checked (when the app initially loads).
361+
* The rule is triggered only when the url matches either `""` or `"/"`.
362+
*
363+
* Note: The rule is intended to be used when the root of the application is directly linked to.
364+
* When the URL is *not* `""` or `"/"` and doesn't match other rules, the [[otherwise]] rule is triggered.
365+
* This allows 404-like behavior when an unknown URL is deep-linked.
366+
*
367+
* #### Example:
368+
* Start app at `home` state.
369+
* ```js
370+
* .initial({ state: 'home' });
371+
* ```
372+
*
373+
* #### Example:
374+
* Start app at `/home` (by url)
375+
* ```js
376+
* .initial('/home');
377+
* ```
378+
*
379+
* #### Example:
380+
* When no other url rule matches, go to `home` state
381+
* ```js
382+
* .initial((url, router) => {
383+
* console.log('initial state');
384+
* return { state: 'home' };
385+
* })
386+
* ```
387+
*
388+
* @param handler The initial state or url path, or a function which returns the state or url path (or performs custom logic).
389+
*/
390+
initial(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef, options?: { priority: number }): void;
391+
343392
/**
344393
* Gets all registered rules
345394
*

src/url/urlRouter.ts

+30-17
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
* @module url
44
*/
55
/** for typedoc */
6-
import { removeFrom, createProxyFunctions, inArray, composeSort, sortBy, extend } from "../common/common";
7-
import { isFunction, isString, isDefined } from "../common/predicates";
8-
import { UrlMatcher } from "./urlMatcher";
9-
import { RawParams } from "../params/interface";
10-
import { Disposable } from "../interface";
11-
import { UIRouter } from "../router";
12-
import { val, is, pattern, prop, pipe } from "../common/hof";
13-
import { UrlRuleFactory } from "./urlRule";
14-
import { TargetState } from "../state/targetState";
15-
import { UrlRule, UrlRuleHandlerFn, UrlParts, UrlRulesApi, UrlSyncApi, MatchResult } from "./interface";
16-
import { TargetStateDef } from "../state/interface";
6+
import { composeSort, createProxyFunctions, extend, inArray, removeFrom, sortBy } from '../common/common';
7+
import { isDefined, isFunction, isString } from '../common/predicates';
8+
import { UrlMatcher } from './urlMatcher';
9+
import { RawParams } from '../params/interface';
10+
import { Disposable } from '../interface';
11+
import { UIRouter } from '../router';
12+
import { is, pattern, pipe, prop, val } from '../common/hof';
13+
import { UrlRuleFactory } from './urlRule';
14+
import { TargetState } from '../state/targetState';
15+
import { MatchResult, UrlParts, UrlRule, UrlRuleHandlerFn, UrlRuleMatchFn, UrlRulesApi, UrlSyncApi } from './interface';
16+
import { TargetStateDef } from '../state/interface';
1717

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

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

142142
let url: UrlParts = {
143-
path: $url.path(), search: $url.search(), hash: $url.hash()
143+
path: $url.path(), search: $url.search(), hash: $url.hash(),
144144
};
145145

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

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

280-
let handlerFn: UrlRuleHandlerFn = isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler);
281278
this._otherwiseFn = this.urlRuleFactory.create(val(true), handlerFn);
282279
this._sorted = false;
283280
};
284281

282+
/** @inheritdoc */
283+
initial(handler: string | UrlRuleHandlerFn | TargetState | TargetStateDef) {
284+
let handlerFn: UrlRuleHandlerFn = getHandlerFn(handler);
285+
286+
let matchFn: UrlRuleMatchFn = (urlParts, router) =>
287+
router.globals.transitionHistory.size() === 0 && !!/^\/?$/.exec(urlParts.path);
288+
289+
this.rule(this.urlRuleFactory.create(matchFn, handlerFn));
290+
};
291+
285292
/** @inheritdoc */
286293
when(matcher: (RegExp|UrlMatcher|string), handler: string|UrlRuleHandlerFn, options?: { priority: number }): UrlRule {
287294
let rule = this.urlRuleFactory.create(matcher, handler);
@@ -297,3 +304,9 @@ export class UrlRouter implements UrlRulesApi, UrlSyncApi, Disposable {
297304
};
298305
}
299306

307+
function getHandlerFn(handler: string|UrlRuleHandlerFn|TargetState|TargetStateDef): UrlRuleHandlerFn {
308+
if (!isFunction(handler) && !isString(handler) && !is(TargetState)(handler) && !TargetState.isDef(handler)) {
309+
throw new Error("'handler' must be a string, function, TargetState, or have a state: 'newtarget' property");
310+
}
311+
return isFunction(handler) ? handler as UrlRuleHandlerFn : val(handler);
312+
}

src/url/urlService.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const makeStub = (keys: string[]): any =>
1515
/** @hidden */ const locationServicesFns = ["url", "path", "search", "hash", "onChange"];
1616
/** @hidden */ const locationConfigFns = ["port", "protocol", "host", "baseHref", "html5Mode", "hashPrefix"];
1717
/** @hidden */ const umfFns = ["type", "caseInsensitive", "strictMode", "defaultSquashPolicy"];
18-
/** @hidden */ const rulesFns = ["sort", "when", "otherwise", "rules", "rule", "removeRule"];
18+
/** @hidden */ const rulesFns = ["sort", "when", "initial", "otherwise", "rules", "rule", "removeRule"];
1919
/** @hidden */ const syncFns = ["deferIntercept", "listen", "sync", "match"];
2020

2121
/**

test/urlRouterSpec.ts

+62
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,68 @@ describe("UrlRouter", function () {
6161
expect(locationService.path()).toBe("/lastrule");
6262
});
6363

64+
describe('.initial(string)', () => {
65+
beforeEach(() => {
66+
router.stateRegistry.register({ name: 'foo', url: '/foo' });
67+
router.stateRegistry.register({ name: 'bar', url: '/bar' });
68+
router.stateRegistry.register({ name: 'otherwise', url: '/otherwise' });
69+
70+
urlRouter.initial('/foo');
71+
urlRouter.otherwise('/otherwise');
72+
});
73+
74+
it("should activate the initial path when initial path matches ''" , function () {
75+
locationService.url("");
76+
expect(locationService.path()).toBe("/foo");
77+
});
78+
79+
it("should activate the initial path when initial path matches '/'" , function () {
80+
locationService.url("/");
81+
expect(locationService.path()).toBe("/foo");
82+
});
83+
84+
it("should not activate the initial path after the initial transition" , function (done) {
85+
stateService.go('bar').then(() => {
86+
locationService.url("/");
87+
expect(locationService.path()).toBe("/otherwise");
88+
done();
89+
});
90+
});
91+
});
92+
93+
describe('.initial({ state: "state" })', () => {
94+
let goSpy = null;
95+
beforeEach(() => {
96+
router.stateRegistry.register({ name: 'foo', url: '/foo' });
97+
router.stateRegistry.register({ name: 'bar', url: '/bar' });
98+
router.stateRegistry.register({ name: 'otherwise', url: '/otherwise' });
99+
100+
urlRouter.initial({ state: 'foo' });
101+
urlRouter.otherwise({ state: 'otherwise' });
102+
103+
goSpy = spyOn(stateService, "transitionTo").and.callThrough();
104+
});
105+
106+
it("should activate the initial path when initial path matches ''" , function () {
107+
locationService.url("");
108+
expect(goSpy).toHaveBeenCalledWith("foo", undefined, jasmine.anything());
109+
});
110+
111+
it("should activate the initial path when initial path matches '/'" , function () {
112+
locationService.url("/");
113+
expect(goSpy).toHaveBeenCalledWith("foo", undefined, jasmine.anything());
114+
});
115+
116+
it("should not activate the initial path after the initial transition" , function (done) {
117+
stateService.go('bar').then(() => {
118+
locationService.url("/");
119+
expect(goSpy).toHaveBeenCalledWith("otherwise", undefined, jasmine.anything());
120+
done();
121+
});
122+
});
123+
});
124+
125+
64126
it('`rule` should return a deregistration function', function() {
65127
let count = 0;
66128
let rule: UrlRule = {

test/urlServiceSpec.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { TestingPlugin } from "./_testingPlugin";
44
import { UrlService } from "../src/url/urlService";
55

66
describe('UrlService facade', () => {
7-
var router: UIRouter;
7+
let router: UIRouter;
88

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

114+
it("should pass rules.initial() through to UrlRouter", () => {
115+
expectProxyCall(() => router.urlService.rules, router.urlRouter, "initial", ["foo"]);
116+
});
117+
114118
it("should pass rules.rules() through to UrlRouter", () => {
115119
expectProxyCall(() => router.urlService.rules, router.urlRouter, "rules");
116120
});

tslint.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"no-eval": true,
2929
"no-internal-module": true,
3030
"no-require-imports": true,
31-
"no-string-literal": true,
31+
"no-string-literal": false,
3232
"no-switch-case-fall-through": true,
3333
"no-trailing-whitespace": false,
3434
"no-unused-expression": [true, "allow-fast-null-checks"],

0 commit comments

Comments
 (0)