Skip to content

Commit

Permalink
feat($urlRouter): abstract $location handling
Browse files Browse the repository at this point in the history
 - Wrap all handling of $location and UrlMatchers and abstract away from $state
 - Expose URL-syncing interfaces
 - Refactor and simplify URL generation
  • Loading branch information
nateabele committed Mar 24, 2014
1 parent 3d3e182 commit 08b4636
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 105 deletions.
77 changes: 24 additions & 53 deletions src/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*
* @requires ui.router.router.$urlRouterProvider
* @requires ui.router.util.$urlMatcherFactoryProvider
* @requires $locationProvider
*
* @description
* The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely
Expand All @@ -20,8 +19,8 @@
*
* The `$stateProvider` provides interfaces to declare these states for your app.
*/
$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider', '$locationProvider'];
function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $locationProvider) {
$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider'];
function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {

var root, states = {}, $state, queue = {}, abstractKey = 'abstract';

Expand Down Expand Up @@ -521,6 +520,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
* @requires $injector
* @requires ui.router.util.$resolve
* @requires ui.router.state.$stateParams
* @requires ui.router.router.$urlRouter
*
* @property {object} params A param object, e.g. {sectionId: section.id)}, that
* you'd like to test against the current active state.
Expand All @@ -534,24 +534,14 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
* between them. It also provides interfaces to ask for current state or even states
* you're coming from.
*/
// $urlRouter is injected just to ensure it gets instantiated
this.$get = $get;
$get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$location', '$urlRouter', '$browser'];
function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $location, $urlRouter, $browser) {
$get.$inject = ['$rootScope', '$q', '$view', '$injector', '$resolve', '$stateParams', '$urlRouter'];
function $get( $rootScope, $q, $view, $injector, $resolve, $stateParams, $urlRouter) {

var TransitionSuperseded = $q.reject(new Error('transition superseded'));
var TransitionPrevented = $q.reject(new Error('transition prevented'));
var TransitionAborted = $q.reject(new Error('transition aborted'));
var TransitionFailed = $q.reject(new Error('transition failed'));
var currentLocation = $location.url();
var baseHref = $browser.baseHref();

function syncUrl() {
if ($location.url() !== currentLocation) {
$location.url(currentLocation);
$location.replace();
}
}

// Handles the case where a state which is the target of a transition is not found, and the user
// can optionally retry or defer the transition
Expand Down Expand Up @@ -591,7 +581,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
var evt = $rootScope.$broadcast('$stateNotFound', redirect, state, params);

if (evt.defaultPrevented) {
syncUrl();
$urlRouter.update();
return TransitionAborted;
}

Expand All @@ -601,7 +591,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $

// Allow the handler to return a promise to defer state lookup retry
if (options.$retry) {
syncUrl();
$urlRouter.update();
return TransitionFailed;
}
var retryTransition = $state.transition = $q.when(evt.retry);
Expand All @@ -613,7 +603,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
}, function() {
return TransitionAborted;
});
syncUrl();
$urlRouter.update();

return retryTransition;
}
Expand Down Expand Up @@ -813,11 +803,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $

// If we're going to the same state and all locals are kept, we've got nothing to do.
// But clear 'transition', as we still want to cancel any other pending transitions.
// TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves,
// because we might accidentally abort a legitimate transition initiated from code?
// TODO: We may not want to bump 'transition' if we're called from a location change
// that we've initiated ourselves, because we might accidentally abort a legitimate
// transition initiated from code?
if (shouldTriggerReload(to, from, locals, options) ) {
if (to.self.reloadOnSearch !== false)
syncUrl();
if (to.self.reloadOnSearch !== false) $urlRouter.update();
$state.transition = null;
return $q.when($state.current);
}
Expand Down Expand Up @@ -855,7 +845,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
* </pre>
*/
if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) {
syncUrl();
$urlRouter.update();
return TransitionPrevented;
}
}
Expand Down Expand Up @@ -910,14 +900,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
copy($state.params, $stateParams);
$state.transition = null;

// Update $location
var toNav = to.navigable;
if (options.location && toNav) {
$location.url(toNav.url.format(toNav.locals.globals.$stateParams));

if (options.location === 'replace') {
$location.replace();
}
if (options.location && to.navigable) {
$urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, {
replace: options.location === 'replace'
});
}

if (options.notify) {
Expand All @@ -937,7 +923,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
*/
$rootScope.$broadcast('$stateChangeSuccess', to.self, toParams, from.self, fromParams);
}
currentLocation = $location.url();
$urlRouter.update(true);

return $state.current;
}, function (error) {
Expand Down Expand Up @@ -965,7 +951,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
evt = $rootScope.$broadcast('$stateChangeError', to.self, toParams, from.self, fromParams, error);

if (!evt.defaultPrevented) {
syncUrl();
$urlRouter.update();
}

return $q.reject(error);
Expand Down Expand Up @@ -1112,33 +1098,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
*/
$state.href = function href(stateOrName, params, options) {
options = extend({ lossy: true, inherit: false, absolute: false, relative: $state.$current }, options || {});

var state = findState(stateOrName, options.relative);
if (!isDefined(state)) return null;

if (!isDefined(state)) return null;
if (options.inherit) params = inheritParams($stateParams, params || {}, $state.$current, state);

var nav = (state && options.lossy) ? state.navigable : state;
var url = (nav && nav.url) ? nav.url.format(normalize(state.params, params || {})) : null;
if (!$locationProvider.html5Mode() && url) {
url = "#" + $locationProvider.hashPrefix() + url;
}

if (baseHref !== '/') {
if ($locationProvider.html5Mode()) {
url = baseHref.slice(0, -1) + url;
} else if (options.absolute){
url = baseHref.slice(1) + url;
}
}

if (options.absolute && url) {
url = $location.protocol() + '://' +
$location.host() +
($location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port()) +
(!$locationProvider.html5Mode() && url ? '/' : '') +
url;
if (!nav || !nav.url) {
return null;
}
return url;
return $urlRouter.href(nav.url, normalize(state.params, params || {}), { absolute: options.absolute });
};

/**
Expand Down
159 changes: 107 additions & 52 deletions src/urlRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @name ui.router.router.$urlRouterProvider
*
* @requires ui.router.util.$urlMatcherFactoryProvider
* @requires $locationProvider
*
* @description
* `$urlRouterProvider` has the responsibility of watching `$location`.
Expand All @@ -13,8 +14,8 @@
* There are several methods on `$urlRouterProvider` that make it useful to use directly
* in your module config.
*/
$UrlRouterProvider.$inject = ['$urlMatcherFactoryProvider'];
function $UrlRouterProvider( $urlMatcherFactory) {
$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider'];
function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
var rules = [],
otherwise = null;

Expand Down Expand Up @@ -208,66 +209,120 @@ function $UrlRouterProvider( $urlMatcherFactory) {
* @requires $location
* @requires $rootScope
* @requires $injector
* @requires $browser
*
* @description
*
*/
this.$get =
[ '$location', '$rootScope', '$injector',
function ($location, $rootScope, $injector) {
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
function update(evt) {
if (evt && evt.defaultPrevented) return;
function check(rule) {
var handled = rule($injector, $location);
if (handled) {
if (isString(handled)) $location.replace().url(handled);
return true;
}
this.$get = $get;
$get.$inject = ['$location', '$rootScope', '$injector', '$browser'];
function $get( $location, $rootScope, $injector, $browser) {

var baseHref = $browser.baseHref(), location = $location.url();

function appendBasePath(url, isHtml5, absolute) {
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(evt) {
if (evt && evt.defaultPrevented) return;

function check(rule) {
var handled = rule($injector, $location);

if (!handled) {
return false;
}
var 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 (otherwise) check(otherwise);
if (isString(handled)) $location.replace().url(handled);
return true;
}
var n = rules.length, i;

$rootScope.$on('$locationChangeSuccess', update);
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 (otherwise) check(otherwise);
}

$rootScope.$on('$locationChangeSuccess', update);

return {
/**
* @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: function() {
update();
},

return {
/**
* @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: function () {
update();
update: function(read) {
if (read) {
location = $location.url();
return;
}
};
}];
if ($location.url() === location) {
return;
}
$location.url(location);
$location.replace();
},

push: function(urlMatcher, params, options) {
$location.url(urlMatcher.format(params));
options = options || {};

if (options.replace) {
$location.replace();
}
},

href: function(urlMatcher, params, options) {
var isHtml5 = $locationProvider.html5Mode();
var url = urlMatcher.format(params);

if (!isHtml5 && url) {
url = "#" + $locationProvider.hashPrefix() + url;
}
url = appendBasePath(url, isHtml5, options.absolute);

if (!options.absolute || !url) {
return url;
}

var slash = (!isHtml5 && url ? '/' : ''),
port = $location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port();

return [$location.protocol(), '://', $location.host(), port, slash, url].join('');
}
};
}
}

angular.module('ui.router.router').provider('$urlRouter', $UrlRouterProvider);

1 comment on commit 08b4636

@timkindberg
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job Nate!

Please sign in to comment.