ui-router
Version:
State-based routing for Javascript
413 lines (374 loc) • 13.9 kB
text/typescript
/** @module url */ /** for typedoc */
import {extend, bindFunctions} from "../common/common";
import {isFunction, isString, isDefined, isArray} from "../common/predicates";
import {UrlMatcher} from "./module";
import {services} from "../common/coreservices";
import {UrlMatcherFactory} from "./urlMatcherFactory";
import {StateParams} from "../params/stateParams";
let $location = services.location;
// Returns a string that is a prefix of all strings matching the RegExp
function regExpPrefix(re) {
let prefix = /^\^((?:\\[^a-zA-Z0-9]|[^\\\[\]\^$*+?.()|{}]+)*)/.exec(re.source);
return (prefix != null) ? prefix[1].replace(/\\(.)/g, "$1") : '';
}
// Interpolates matched values into a String.replace()-style pattern
function interpolate(pattern, match) {
return pattern.replace(/\$(\$|\d{1,2})/, function (m, what) {
return match[what === '$' ? 0 : Number(what)];
});
}
function handleIfMatch($injector, $stateParams, handler, match) {
if (!match) return false;
let result = $injector.invoke(handler, handler, { $match: match, $stateParams: $stateParams });
return isDefined(result) ? result : true;
}
function appendBasePath(url, isHtml5, absolute) {
let baseHref = services.locationConfig.baseHref();
if (baseHref === '/') return url;
if (isHtml5) return baseHref.slice(0, -1) + url;
if (absolute) return baseHref.slice(1) + url;
return url;
}
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
function update(rules: Function[], otherwiseFn: Function, evt?: any) {
if (evt && evt.defaultPrevented) return;
function check(rule) {
let handled = rule(services.$injector, $location);
if (!handled) return false;
if (isString(handled)) {
$location.replace();
$location.url(handled);
}
return true;
}
let n = rules.length, i;
for (i = 0; i < n; i++) {
if (check(rules[i])) return;
}
// always check otherwise last to allow dynamic updates to the set of rules
if (otherwiseFn) check(otherwiseFn);
}
/**
* @ngdoc object
* @name ui.router.router.$urlRouterProvider
*
* @requires ui.router.util.$urlMatcherFactoryProvider
* @requires $locationProvider
*
* @description
* `$urlRouterProvider` has the responsibility of watching `$location`.
* When `$location` changes it runs through a list of rules one by one until a
* match is found. `$urlRouterProvider` is used behind the scenes anytime you specify
* a url in a state configuration. All urls are compiled into a UrlMatcher object.
*
* There are several methods on `$urlRouterProvider` that make it useful to use directly
* in your module config.
*/
export class UrlRouterProvider {
/** @hidden */
rules = [];
/** @hidden */
otherwiseFn: Function = null;
/** @hidden */
interceptDeferred = false;
constructor(private $urlMatcherFactory: UrlMatcherFactory, private $stateParams: StateParams) {
}
/**
* @ngdoc function
* @name ui.router.router.$urlRouterProvider#rule
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Defines rules that are used by `$urlRouterProvider` to find matches for
* specific URLs.
*
* @example
* <pre>
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
* // Here's an example of how you might allow case insensitive urls
* $urlRouterProvider.rule(function ($injector, $location) {
* var path = $location.path(),
* normalized = path.toLowerCase();
*
* if (path !== normalized) {
* return normalized;
* }
* });
* });
* </pre>
*
* @param {function} rule Handler function that takes `$injector` and `$location`
* services as arguments. You can use them to return a valid path as a string.
*
* @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
*/
rule(rule) {
if (!isFunction(rule)) throw new Error("'rule' must be a function");
this.rules.push(rule);
return this;
};
/**
* @ngdoc object
* @name ui.router.router.$urlRouterProvider#otherwise
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Defines a path that is used when an invalid route is requested.
*
* @example
* <pre>
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
* // if the path doesn't match any of the urls you configured
* // otherwise will take care of routing the user to the
* // specified url
* $urlRouterProvider.otherwise('/index');
*
* // Example of using function rule as param
* $urlRouterProvider.otherwise(function ($injector, $location) {
* return '/a/valid/url';
* });
* });
* </pre>
*
* @param {string|function} rule The url path you want to redirect to or a function
* rule that returns the url path. The function version is passed two params:
* `$injector` and `$location` services, and must return a url string.
*
* @return {object} `$urlRouterProvider` - `$urlRouterProvider` instance
*/
otherwise(rule) {
if (!isFunction(rule) && !isString(rule)) throw new Error("'rule' must be a string or function");
this.otherwiseFn = isString(rule) ? () => rule : rule;
return this;
};
/**
* @ngdoc function
* @name ui.router.router.$urlRouterProvider#when
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Registers a handler for a given url matching.
*
* If the handler is a string, it is
* treated as a redirect, and is interpolated according to the syntax of match
* (i.e. like `String.replace()` for `RegExp`, or like a `UrlMatcher` pattern otherwise).
*
* If the handler is a function, it is injectable. It gets invoked if `$location`
* matches. You have the option of inject the match object as `$match`.
*
* The handler can return
*
* - **falsy** to indicate that the rule didn't match after all, then `$urlRouter`
* will continue trying to find another one that matches.
* - **string** which is treated as a redirect and passed to `$location.url()`
* - **void** or any **truthy** value tells `$urlRouter` that the url was handled.
*
* @example
* <pre>
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
* $urlRouterProvider.when($state.url, function ($match, $stateParams) {
* if ($state.$current.navigable !== state ||
* !equalForKeys($match, $stateParams) {
* $state.transitionTo(state, $match, false);
* }
* });
* });
* </pre>
*
* @param {string|object} what The incoming path that you want to redirect.
* @param {string|function} handler The path you want to redirect your user to.
*/
when(what, handler) {
let {$urlMatcherFactory, $stateParams} = this;
let redirect, handlerIsString = isString(handler);
// @todo Queue this
if (isString(what)) what = $urlMatcherFactory.compile(what);
if (!handlerIsString && !isFunction(handler) && !isArray(handler))
throw new Error("invalid 'handler' in when()");
let strategies = {
matcher: function (_what, _handler) {
if (handlerIsString) {
redirect = $urlMatcherFactory.compile(_handler);
_handler = ['$match', redirect.format.bind(redirect)];
}
return extend(function () {
return handleIfMatch(services.$injector, $stateParams, _handler, _what.exec($location.path(), $location.search(), $location.hash()));
}, {
prefix: isString(_what.prefix) ? _what.prefix : ''
});
},
regex: function (_what, _handler) {
if (_what.global || _what.sticky) throw new Error("when() RegExp must not be global or sticky");
if (handlerIsString) {
redirect = _handler;
_handler = ['$match', ($match) => interpolate(redirect, $match)];
}
return extend(function () {
return handleIfMatch(services.$injector, $stateParams, _handler, _what.exec($location.path()));
}, {
prefix: regExpPrefix(_what)
});
}
};
let check = {
matcher: $urlMatcherFactory.isMatcher(what),
regex: what instanceof RegExp
};
for (var n in check) {
if (check[n]) return this.rule(strategies[n](what, handler));
}
throw new Error("invalid 'what' in when()");
};
/**
* @ngdoc function
* @name ui.router.router.$urlRouterProvider#deferIntercept
* @methodOf ui.router.router.$urlRouterProvider
*
* @description
* Disables (or enables) deferring location change interception.
*
* If you wish to customize the behavior of syncing the URL (for example, if you wish to
* defer a transition but maintain the current URL), call this method at configuration time.
* Then, at run time, call `$urlRouter.listen()` after you have configured your own
* `$locationChangeSuccess` event handler.
*
* @example
* <pre>
* var app = angular.module('app', ['ui.router.router']);
*
* app.config(function ($urlRouterProvider) {
*
* // Prevent $urlRouter from automatically intercepting URL changes;
* // this allows you to configure custom behavior in between
* // location changes and route synchronization:
* $urlRouterProvider.deferIntercept();
*
* }).run(function ($rootScope, $urlRouter, UserService) {
*
* $rootScope.$on('$locationChangeSuccess', function(e) {
* // UserService is an example service for managing user state
* if (UserService.isLoggedIn()) return;
*
* // Prevent $urlRouter's default handler from firing
* e.preventDefault();
*
* UserService.handleLogin().then(function() {
* // Once the user has logged in, sync the current URL
* // to the router:
* $urlRouter.sync();
* });
* });
*
* // Configures $urlRouter's listener *after* your custom listener
* $urlRouter.listen();
* });
* </pre>
*
* @param {boolean} defer Indicates whether to defer location change interception. Passing
* no parameter is equivalent to `true`.
*/
deferIntercept(defer) {
if (defer === undefined) defer = true;
this.interceptDeferred = defer;
};
}
export class UrlRouter {
private location: string;
private listener: Function;
constructor(private urlRouterProvider: UrlRouterProvider) {
bindFunctions(UrlRouter.prototype, this, this);
}
/**
* @ngdoc function
* @name ui.router.router.$urlRouter#sync
* @methodOf ui.router.router.$urlRouter
*
* @description
* Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`.
* This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event,
* perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed
* with the transition by calling `$urlRouter.sync()`.
*
* @example
* <pre>
* angular.module('app', ['ui.router'])
* .run(function($rootScope, $urlRouter) {
* $rootScope.$on('$locationChangeSuccess', function(evt) {
* // Halt state change from even starting
* evt.preventDefault();
* // Perform custom logic
* var meetsRequirement = ...
* // Continue with the update and state transition if logic allows
* if (meetsRequirement) $urlRouter.sync();
* });
* });
* </pre>
*/
sync() {
update(this.urlRouterProvider.rules, this.urlRouterProvider.otherwiseFn);
}
listen() {
return this.listener = this.listener || $location.onChange(evt => update(this.urlRouterProvider.rules, this.urlRouterProvider.otherwiseFn, evt));
}
update(read?) {
if (read) {
this.location = $location.url();
return;
}
if ($location.url() === this.location) return;
$location.url(this.location);
$location.replace();
}
push(urlMatcher, params, options) {
$location.url(urlMatcher.format(params || {}));
if (options && options.replace) $location.replace();
}
/**
* @ngdoc function
* @name ui.router.router.$urlRouter#href
* @methodOf ui.router.router.$urlRouter
*
* @description
* A URL generation method that returns the compiled URL for a given
* {@link ui.router.util.type:UrlMatcher `UrlMatcher`}, populated with the provided parameters.
*
* @example
* <pre>
* $bob = $urlRouter.href(new UrlMatcher("/about/:person"), {
* person: "bob"
* });
* // $bob == "/about/bob";
* </pre>
*
* @param {UrlMatcher} urlMatcher The `UrlMatcher` object which is used as the template of the URL to generate.
* @param {object=} params An object of parameter values to fill the matcher's required parameters.
* @param {object=} options Options object. The options are:
*
* - **`absolute`** - {boolean=false}, If true will generate an absolute url, e.g. "http://www.example.com/fullurl".
*
* @returns {string} Returns the fully compiled URL, or `null` if `params` fail validation against `urlMatcher`
*/
href(urlMatcher: UrlMatcher, params: any, options: any): string {
if (!urlMatcher.validates(params)) return null;
let url = urlMatcher.format(params);
options = options || {};
let cfg = services.locationConfig;
let isHtml5 = cfg.html5Mode();
if (!isHtml5 && url !== null) {
url = "#" + cfg.hashPrefix() + url;
}
url = appendBasePath(url, isHtml5, options.absolute);
if (!options.absolute || !url) {
return url;
}
let slash = (!isHtml5 && url ? '/' : ''), port = cfg.port();
port = <any> (port === 80 || port === 443 ? '' : ':' + port);
return [cfg.protocol(), '://', cfg.host(), port, slash, url].join('');
}
}