Skip to content

Commit 08b4636

Browse files
committed
feat($urlRouter): abstract $location handling
- Wrap all handling of $location and UrlMatchers and abstract away from $state - Expose URL-syncing interfaces - Refactor and simplify URL generation
1 parent 3d3e182 commit 08b4636

File tree

2 files changed

+131
-105
lines changed

2 files changed

+131
-105
lines changed

src/state.js

+24-53
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*
55
* @requires ui.router.router.$urlRouterProvider
66
* @requires ui.router.util.$urlMatcherFactoryProvider
7-
* @requires $locationProvider
87
*
98
* @description
109
* The new `$stateProvider` works similar to Angular's v1 router, but it focuses purely
@@ -20,8 +19,8 @@
2019
*
2120
* The `$stateProvider` provides interfaces to declare these states for your app.
2221
*/
23-
$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider', '$locationProvider'];
24-
function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $locationProvider) {
22+
$StateProvider.$inject = ['$urlRouterProvider', '$urlMatcherFactoryProvider'];
23+
function $StateProvider( $urlRouterProvider, $urlMatcherFactory) {
2524

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

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

542541
var TransitionSuperseded = $q.reject(new Error('transition superseded'));
543542
var TransitionPrevented = $q.reject(new Error('transition prevented'));
544543
var TransitionAborted = $q.reject(new Error('transition aborted'));
545544
var TransitionFailed = $q.reject(new Error('transition failed'));
546-
var currentLocation = $location.url();
547-
var baseHref = $browser.baseHref();
548-
549-
function syncUrl() {
550-
if ($location.url() !== currentLocation) {
551-
$location.url(currentLocation);
552-
$location.replace();
553-
}
554-
}
555545

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

593583
if (evt.defaultPrevented) {
594-
syncUrl();
584+
$urlRouter.update();
595585
return TransitionAborted;
596586
}
597587

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

602592
// Allow the handler to return a promise to defer state lookup retry
603593
if (options.$retry) {
604-
syncUrl();
594+
$urlRouter.update();
605595
return TransitionFailed;
606596
}
607597
var retryTransition = $state.transition = $q.when(evt.retry);
@@ -613,7 +603,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
613603
}, function() {
614604
return TransitionAborted;
615605
});
616-
syncUrl();
606+
$urlRouter.update();
617607

618608
return retryTransition;
619609
}
@@ -813,11 +803,11 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
813803

814804
// If we're going to the same state and all locals are kept, we've got nothing to do.
815805
// But clear 'transition', as we still want to cancel any other pending transitions.
816-
// TODO: We may not want to bump 'transition' if we're called from a location change that we've initiated ourselves,
817-
// because we might accidentally abort a legitimate transition initiated from code?
806+
// TODO: We may not want to bump 'transition' if we're called from a location change
807+
// that we've initiated ourselves, because we might accidentally abort a legitimate
808+
// transition initiated from code?
818809
if (shouldTriggerReload(to, from, locals, options) ) {
819-
if (to.self.reloadOnSearch !== false)
820-
syncUrl();
810+
if (to.self.reloadOnSearch !== false) $urlRouter.update();
821811
$state.transition = null;
822812
return $q.when($state.current);
823813
}
@@ -855,7 +845,7 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
855845
* </pre>
856846
*/
857847
if ($rootScope.$broadcast('$stateChangeStart', to.self, toParams, from.self, fromParams).defaultPrevented) {
858-
syncUrl();
848+
$urlRouter.update();
859849
return TransitionPrevented;
860850
}
861851
}
@@ -910,14 +900,10 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
910900
copy($state.params, $stateParams);
911901
$state.transition = null;
912902

913-
// Update $location
914-
var toNav = to.navigable;
915-
if (options.location && toNav) {
916-
$location.url(toNav.url.format(toNav.locals.globals.$stateParams));
917-
918-
if (options.location === 'replace') {
919-
$location.replace();
920-
}
903+
if (options.location && to.navigable) {
904+
$urlRouter.push(to.navigable.url, to.navigable.locals.globals.$stateParams, {
905+
replace: options.location === 'replace'
906+
});
921907
}
922908

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

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

967953
if (!evt.defaultPrevented) {
968-
syncUrl();
954+
$urlRouter.update();
969955
}
970956

971957
return $q.reject(error);
@@ -1112,33 +1098,18 @@ function $StateProvider( $urlRouterProvider, $urlMatcherFactory, $
11121098
*/
11131099
$state.href = function href(stateOrName, params, options) {
11141100
options = extend({ lossy: true, inherit: false, absolute: false, relative: $state.$current }, options || {});
1101+
11151102
var state = findState(stateOrName, options.relative);
1116-
if (!isDefined(state)) return null;
11171103

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

11201107
var nav = (state && options.lossy) ? state.navigable : state;
1121-
var url = (nav && nav.url) ? nav.url.format(normalize(state.params, params || {})) : null;
1122-
if (!$locationProvider.html5Mode() && url) {
1123-
url = "#" + $locationProvider.hashPrefix() + url;
1124-
}
1125-
1126-
if (baseHref !== '/') {
1127-
if ($locationProvider.html5Mode()) {
1128-
url = baseHref.slice(0, -1) + url;
1129-
} else if (options.absolute){
1130-
url = baseHref.slice(1) + url;
1131-
}
1132-
}
11331108

1134-
if (options.absolute && url) {
1135-
url = $location.protocol() + '://' +
1136-
$location.host() +
1137-
($location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port()) +
1138-
(!$locationProvider.html5Mode() && url ? '/' : '') +
1139-
url;
1109+
if (!nav || !nav.url) {
1110+
return null;
11401111
}
1141-
return url;
1112+
return $urlRouter.href(nav.url, normalize(state.params, params || {}), { absolute: options.absolute });
11421113
};
11431114

11441115
/**

src/urlRouter.js

+107-52
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* @name ui.router.router.$urlRouterProvider
44
*
55
* @requires ui.router.util.$urlMatcherFactoryProvider
6+
* @requires $locationProvider
67
*
78
* @description
89
* `$urlRouterProvider` has the responsibility of watching `$location`.
@@ -13,8 +14,8 @@
1314
* There are several methods on `$urlRouterProvider` that make it useful to use directly
1415
* in your module config.
1516
*/
16-
$UrlRouterProvider.$inject = ['$urlMatcherFactoryProvider'];
17-
function $UrlRouterProvider( $urlMatcherFactory) {
17+
$UrlRouterProvider.$inject = ['$locationProvider', '$urlMatcherFactoryProvider'];
18+
function $UrlRouterProvider( $locationProvider, $urlMatcherFactory) {
1819
var rules = [],
1920
otherwise = null;
2021

@@ -208,66 +209,120 @@ function $UrlRouterProvider( $urlMatcherFactory) {
208209
* @requires $location
209210
* @requires $rootScope
210211
* @requires $injector
212+
* @requires $browser
211213
*
212214
* @description
213215
*
214216
*/
215-
this.$get =
216-
[ '$location', '$rootScope', '$injector',
217-
function ($location, $rootScope, $injector) {
218-
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
219-
function update(evt) {
220-
if (evt && evt.defaultPrevented) return;
221-
function check(rule) {
222-
var handled = rule($injector, $location);
223-
if (handled) {
224-
if (isString(handled)) $location.replace().url(handled);
225-
return true;
226-
}
217+
this.$get = $get;
218+
$get.$inject = ['$location', '$rootScope', '$injector', '$browser'];
219+
function $get( $location, $rootScope, $injector, $browser) {
220+
221+
var baseHref = $browser.baseHref(), location = $location.url();
222+
223+
function appendBasePath(url, isHtml5, absolute) {
224+
if (baseHref === '/') return url;
225+
if (isHtml5) return baseHref.slice(0, -1) + url;
226+
if (absolute) return baseHref.slice(1) + url;
227+
return url;
228+
}
229+
230+
// TODO: Optimize groups of rules with non-empty prefix into some sort of decision tree
231+
function update(evt) {
232+
if (evt && evt.defaultPrevented) return;
233+
234+
function check(rule) {
235+
var handled = rule($injector, $location);
236+
237+
if (!handled) {
227238
return false;
228239
}
229-
var n=rules.length, i;
230-
for (i=0; i<n; i++) {
231-
if (check(rules[i])) return;
232-
}
233-
// always check otherwise last to allow dynamic updates to the set of rules
234-
if (otherwise) check(otherwise);
240+
if (isString(handled)) $location.replace().url(handled);
241+
return true;
235242
}
243+
var n = rules.length, i;
236244

237-
$rootScope.$on('$locationChangeSuccess', update);
245+
for (i = 0; i < n; i++) {
246+
if (check(rules[i])) return;
247+
}
248+
// always check otherwise last to allow dynamic updates to the set of rules
249+
if (otherwise) check(otherwise);
250+
}
251+
252+
$rootScope.$on('$locationChangeSuccess', update);
253+
254+
return {
255+
/**
256+
* @ngdoc function
257+
* @name ui.router.router.$urlRouter#sync
258+
* @methodOf ui.router.router.$urlRouter
259+
*
260+
* @description
261+
* Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`.
262+
* This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event,
263+
* perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed
264+
* with the transition by calling `$urlRouter.sync()`.
265+
*
266+
* @example
267+
* <pre>
268+
* angular.module('app', ['ui.router']);
269+
* .run(function($rootScope, $urlRouter) {
270+
* $rootScope.$on('$locationChangeSuccess', function(evt) {
271+
* // Halt state change from even starting
272+
* evt.preventDefault();
273+
* // Perform custom logic
274+
* var meetsRequirement = ...
275+
* // Continue with the update and state transition if logic allows
276+
* if (meetsRequirement) $urlRouter.sync();
277+
* });
278+
* });
279+
* </pre>
280+
*/
281+
sync: function() {
282+
update();
283+
},
238284

239-
return {
240-
/**
241-
* @ngdoc function
242-
* @name ui.router.router.$urlRouter#sync
243-
* @methodOf ui.router.router.$urlRouter
244-
*
245-
* @description
246-
* Triggers an update; the same update that happens when the address bar url changes, aka `$locationChangeSuccess`.
247-
* This method is useful when you need to use `preventDefault()` on the `$locationChangeSuccess` event,
248-
* perform some custom logic (route protection, auth, config, redirection, etc) and then finally proceed
249-
* with the transition by calling `$urlRouter.sync()`.
250-
*
251-
* @example
252-
* <pre>
253-
* angular.module('app', ['ui.router']);
254-
* .run(function($rootScope, $urlRouter) {
255-
* $rootScope.$on('$locationChangeSuccess', function(evt) {
256-
* // Halt state change from even starting
257-
* evt.preventDefault();
258-
* // Perform custom logic
259-
* var meetsRequirement = ...
260-
* // Continue with the update and state transition if logic allows
261-
* if (meetsRequirement) $urlRouter.sync();
262-
* });
263-
* });
264-
* </pre>
265-
*/
266-
sync: function () {
267-
update();
285+
update: function(read) {
286+
if (read) {
287+
location = $location.url();
288+
return;
268289
}
269-
};
270-
}];
290+
if ($location.url() === location) {
291+
return;
292+
}
293+
$location.url(location);
294+
$location.replace();
295+
},
296+
297+
push: function(urlMatcher, params, options) {
298+
$location.url(urlMatcher.format(params));
299+
options = options || {};
300+
301+
if (options.replace) {
302+
$location.replace();
303+
}
304+
},
305+
306+
href: function(urlMatcher, params, options) {
307+
var isHtml5 = $locationProvider.html5Mode();
308+
var url = urlMatcher.format(params);
309+
310+
if (!isHtml5 && url) {
311+
url = "#" + $locationProvider.hashPrefix() + url;
312+
}
313+
url = appendBasePath(url, isHtml5, options.absolute);
314+
315+
if (!options.absolute || !url) {
316+
return url;
317+
}
318+
319+
var slash = (!isHtml5 && url ? '/' : ''),
320+
port = $location.port() == 80 || $location.port() == 443 ? '' : ':' + $location.port();
321+
322+
return [$location.protocol(), '://', $location.host(), port, slash, url].join('');
323+
}
324+
};
325+
}
271326
}
272327

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

0 commit comments

Comments
 (0)