feat($location): default hashPrefix to '!'

The $location service is designed to support hash prefixed URLs
for cases where the browser does not support HTML5 push-state navigation.

The Google Ajax Crawling Scheme expects that local paths within a SPA start
with a hash-bang (e.g. `somedomain.com/base/path/#!/client/side/path`).

The `$locationProvide` allows the application developer to configure the
hashPrefix, and it is normal to set this to a bang '!', but the default
has always been the empty string ''.

This has caused some confusion where a user is not aware of this feature
and wonders why adding a hash value to the location (e.g. `$location.hash('xxx')`)
results in a double hash: `##xxx`.

This commit changes the default value of the prefix to '!', which is more
natural and expected.

See https://developers.google.com/webmasters/ajax-crawling/docs/getting-started

Closes #13812
Closes #14202

BREAKING CHANGE

The hash-prefix for `$location` hash-bang URLs has changed from the empty
string "" to the bang "!". If your application does not use HTML5 mode
or is being run on browsers that do not support HTML5 mode, and you have
not specified your own hash-prefix then client side URLs will now contain
a "!" prefix. For example, rather than `mydomain.com/#/a/b/c` will become
`mydomain/#!/a/b/c`.

If you actually wanted to have no hash-prefix then you should configure
this by adding a configuration block to you application:

```
appModule.config(['$locationProvider', function($locationProvider) {
  $locationProvider.hashPrefix("");
}]);
```
This commit is contained in:
Peter Bacon Darwin
2016-03-09 10:16:19 +00:00
parent 6a56461135
commit aa077e8112
5 changed files with 124 additions and 101 deletions
+2 -2
View File
@@ -99,11 +99,11 @@ To configure the `$location` service, retrieve the
- **hashPrefix(prefix)**: {string}<br />
prefix used for Hashbang URLs (used in Hashbang mode or in legacy browser in Html5 mode)<br />
default: `""`
default: `"!"`
### Example configuration
```js
$locationProvider.html5Mode(true).hashPrefix('!');
$locationProvider.html5Mode(true).hashPrefix('*');
```
## Getter and setter methods
+2 -1
View File
@@ -701,7 +701,7 @@ function locationGetterSetter(property, preprocess) {
* Use the `$locationProvider` to configure how the application deep linking paths are stored.
*/
function $LocationProvider() {
var hashPrefix = '',
var hashPrefix = '!',
html5Mode = {
enabled: false,
requireBase: true,
@@ -712,6 +712,7 @@ function $LocationProvider() {
* @ngdoc method
* @name $locationProvider#hashPrefix
* @description
* The default value for the prefix is `'!'`.
* @param {string=} prefix Prefix for hash part (containing path and search)
* @returns {*} current value if used as getter or itself (chaining) if used as setter
*/
+7 -7
View File
@@ -755,9 +755,9 @@ describe('browser', function() {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/#/initialPath');
expect(fakeWindow.location.href).toBe('http://server/#!/initialPath');
fakeWindow.location.href = 'http://server/#/someTestHash';
fakeWindow.location.href = 'http://server/#!/someTestHash';
$rootScope.$digest();
@@ -774,9 +774,9 @@ describe('browser', function() {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/#/initialPath');
expect(fakeWindow.location.href).toBe('http://server/#!/initialPath');
fakeWindow.location.href = 'http://server/#/someTestHash';
fakeWindow.location.href = 'http://server/#!/someTestHash';
$rootScope.$digest();
@@ -793,9 +793,9 @@ describe('browser', function() {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/#/initialPath');
expect(fakeWindow.location.href).toBe('http://server/#!/initialPath');
fakeWindow.location.href = 'http://server/#/someTestHash';
fakeWindow.location.href = 'http://server/#!/someTestHash';
$rootScope.$digest();
@@ -846,7 +846,7 @@ describe('browser', function() {
$rootScope.$digest();
// from $location for rewriting the initial url into a hash url
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', true);
expect(browser.url).toHaveBeenCalledWith('http://server/#!/some/deep/path', true);
expect(changeUrlCount).toBe(1);
});
+112 -90
View File
@@ -13,6 +13,28 @@ describe('$location', function() {
});
describe('defaults', function() {
it('should have hashPrefix of "!"', function() {
initService({});
inject(
initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
function($location) {
$location.path('/a/b/c');
expect($location.absUrl()).toEqual('http://host.com/base/index.html#!/a/b/c');
});
});
it('should not be html5 mode', function() {
initService({});
inject(
initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
function($location) {
$location.path('/a/b/c');
expect($location.absUrl()).toContain('#!');
});
});
});
describe('File Protocol', function() {
/* global urlParsingNode: true */
var urlParsingNodePlaceholder;
@@ -676,10 +698,10 @@ describe('$location', function() {
$location.path('/').replace();
}
});
expect($browser.url()).toEqual('http://server/base/#/home');
expect($browser.url()).toEqual('http://server/base/#!/home');
$rootScope.$digest();
expect(handlerCalled).toEqual(true);
expect($browser.url()).toEqual('http://server/base/#/');
expect($browser.url()).toEqual('http://server/base/#!/');
}
);
});
@@ -716,7 +738,7 @@ describe('$location', function() {
$rootScope.$digest();
expect($browser.url()).toEqual('http://server/app/#/Home');
expect($browser.url()).toEqual('http://server/app/#!/Home');
expect($location.path()).toEqual('/Home');
expect($browserUrl).toHaveBeenCalledTimes(1);
});
@@ -732,10 +754,10 @@ describe('$location', function() {
$rootScope.$digest();
expect($browser.url()).toEqual('http://server/app/#/');
expect($browser.url()).toEqual('http://server/app/#!/');
expect($location.path()).toEqual('/');
expect($browserUrl).toHaveBeenCalledTimes(1);
expect($browserUrl.calls.argsFor(0)).toEqual(['http://server/app/#/', false, null]);
expect($browserUrl.calls.argsFor(0)).toEqual(['http://server/app/#!/', false, null]);
});
});
@@ -748,10 +770,10 @@ describe('$location', function() {
updatePathOnLocationChangeSuccessTo('/Home');
$rootScope.$digest();
expect($browser.url()).toEqual('http://server/app/#/Home');
expect($browser.url()).toEqual('http://server/app/#!/Home');
expect($location.path()).toEqual('/Home');
expect($browserUrl).toHaveBeenCalledTimes(1);
expect($browserUrl.calls.argsFor(0)).toEqual(['http://server/app/#/Home', false, null]);
expect($browserUrl.calls.argsFor(0)).toEqual(['http://server/app/#!/Home', false, null]);
});
});
@@ -764,7 +786,7 @@ describe('$location', function() {
updatePathOnLocationChangeSuccessTo('/');
$rootScope.$digest();
expect($browser.url()).toEqual('http://server/app/#/');
expect($browser.url()).toEqual('http://server/app/#!/');
expect($location.path()).toEqual('/');
expect($browserUrl).toHaveBeenCalledTimes(1);
});
@@ -1822,38 +1844,6 @@ describe('$location', function() {
// we need to do this otherwise we can't simulate events
$document.find('body').append($rootElement);
var element = $compile('<a href="#/view1">v1</a><a href="#/view2">v2</a>')($rootScope);
$rootElement.append(element);
var av1 = $rootElement.find('a').eq(0);
var av2 = $rootElement.find('a').eq(1);
browserTrigger(av1, 'click');
expect($browser.url()).toEqual(base + '#/view1');
browserTrigger(av2, 'click');
expect($browser.url()).toEqual(base + '#/view2');
$rootElement.remove();
});
});
it('should not mess up hash urls when clicking on links in hashbang mode with a prefix',
function() {
var base;
module(function($locationProvider) {
return function($browser) {
window.location.hash = '!someHash';
$browser.url(base = window.location.href);
base = base.split('#')[0];
$locationProvider.hashPrefix('!');
};
});
inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) {
// we need to do this otherwise we can't simulate events
$document.find('body').append($rootElement);
var element = $compile('<a href="#!/view1">v1</a><a href="#!/view2">v2</a>')($rootScope);
$rootElement.append(element);
var av1 = $rootElement.find('a').eq(0);
@@ -1865,6 +1855,38 @@ describe('$location', function() {
browserTrigger(av2, 'click');
expect($browser.url()).toEqual(base + '#!/view2');
$rootElement.remove();
});
});
it('should not mess up hash urls when clicking on links in hashbang mode with a prefix',
function() {
var base;
module(function($locationProvider) {
return function($browser) {
window.location.hash = '!!someHash';
$browser.url(base = window.location.href);
base = base.split('#')[0];
$locationProvider.hashPrefix('!!');
};
});
inject(function($rootScope, $compile, $browser, $rootElement, $document, $location) {
// we need to do this otherwise we can't simulate events
$document.find('body').append($rootElement);
var element = $compile('<a href="#!!/view1">v1</a><a href="#!!/view2">v2</a>')($rootScope);
$rootElement.append(element);
var av1 = $rootElement.find('a').eq(0);
var av2 = $rootElement.find('a').eq(1);
browserTrigger(av1, 'click');
expect($browser.url()).toEqual(base + '#!!/view1');
browserTrigger(av2, 'click');
expect($browser.url()).toEqual(base + '#!!/view2');
});
});
@@ -1992,11 +2014,11 @@ describe('$location', function() {
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/somePath', 'http://server/', 'http://server/#/somePath']);
toEqual(['after', 'http://server/#!/somePath', 'http://server/', 'http://server/#!/somePath']);
expect($location.url()).toEqual('/somePath');
expect($browser.url()).toEqual('http://server/#/somePath');
expect($browser.url()).toEqual('http://server/#!/somePath');
}));
@@ -2021,7 +2043,7 @@ describe('$location', function() {
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs[1]).toBeUndefined();
expect($location.url()).toEqual('');
expect($browser.url()).toEqual('http://server/');
@@ -2031,7 +2053,7 @@ describe('$location', function() {
inject(function($location, $browser, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
if (newUrl === 'http://server/#/somePath') {
if (newUrl === 'http://server/#!/somePath') {
$location.url('/redirectPath');
}
});
@@ -2043,15 +2065,15 @@ describe('$location', function() {
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/redirectPath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/redirectPath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/redirectPath', 'http://server/',
'http://server/#/redirectPath']);
toEqual(['after', 'http://server/#!/redirectPath', 'http://server/',
'http://server/#!/redirectPath']);
expect($location.url()).toEqual('/redirectPath');
expect($browser.url()).toEqual('http://server/#/redirectPath');
expect($browser.url()).toEqual('http://server/#!/redirectPath');
})
);
@@ -2059,7 +2081,7 @@ describe('$location', function() {
inject(function($location, $browser, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
if (newUrl === 'http://server/#/somePath') {
if (newUrl === 'http://server/#!/somePath') {
event.preventDefault();
$location.url('/redirectPath');
}
@@ -2072,15 +2094,15 @@ describe('$location', function() {
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/redirectPath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/redirectPath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/redirectPath', 'http://server/',
'http://server/#/redirectPath']);
toEqual(['after', 'http://server/#!/redirectPath', 'http://server/',
'http://server/#!/redirectPath']);
expect($location.url()).toEqual('/redirectPath');
expect($browser.url()).toEqual('http://server/#/redirectPath');
expect($browser.url()).toEqual('http://server/#!/redirectPath');
})
);
@@ -2088,9 +2110,9 @@ describe('$location', function() {
inject(function($location, $browser, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
if (newUrl === 'http://server/#/somePath') {
if (newUrl === 'http://server/#!/somePath') {
$location.url('/redirectPath');
} else if (newUrl === 'http://server/#/redirectPath') {
} else if (newUrl === 'http://server/#!/redirectPath') {
$location.url('/redirectPath2');
}
});
@@ -2102,17 +2124,17 @@ describe('$location', function() {
$rootScope.$apply();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/redirectPath', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/redirectPath', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/redirectPath2', 'http://server/', 'http://server/']);
toEqual(['before', 'http://server/#!/redirectPath2', 'http://server/', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/redirectPath2', 'http://server/',
'http://server/#/redirectPath2']);
toEqual(['after', 'http://server/#!/redirectPath2', 'http://server/',
'http://server/#!/redirectPath2']);
expect($location.url()).toEqual('/redirectPath2');
expect($browser.url()).toEqual('http://server/#/redirectPath2');
expect($browser.url()).toEqual('http://server/#!/redirectPath2');
})
);
@@ -2131,13 +2153,13 @@ describe('$location', function() {
});
$browser.url('http://server/#/somePath');
$browser.url('http://server/#!/somePath');
$browser.poll();
expect($log.info.logs.shift()).
toEqual(['start', 'http://server/#/somePath', 'http://server/']);
toEqual(['start', 'http://server/#!/somePath', 'http://server/']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/somePath', 'http://server/']);
toEqual(['after', 'http://server/#!/somePath', 'http://server/']);
})
);
@@ -2146,7 +2168,7 @@ describe('$location', function() {
$location.url('/somepath');
$rootScope.$apply();
expect($browser.url()).toEqual('http://server/#/somepath');
expect($browser.url()).toEqual('http://server/#!/somepath');
expect($location.url()).toEqual('/somepath');
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
@@ -2160,9 +2182,9 @@ describe('$location', function() {
$browser.poll();
expect($log.info.logs.shift()).
toEqual(['start', 'http://server/', 'http://server/#/somepath']);
toEqual(['start', 'http://server/', 'http://server/#!/somepath']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/', 'http://server/#/somepath']);
toEqual(['after', 'http://server/', 'http://server/#!/somepath']);
})
);
@@ -2170,7 +2192,7 @@ describe('$location', function() {
inject(function($location, $browser, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
if (newUrl === 'http://server/#/somePath') {
if (newUrl === 'http://server/#!/somePath') {
$location.url('/redirectPath');
}
});
@@ -2178,21 +2200,21 @@ describe('$location', function() {
$log.info('after', newUrl, oldUrl, $browser.url());
});
$browser.url('http://server/#/somePath');
$browser.url('http://server/#!/somePath');
$browser.poll();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/',
'http://server/#/somePath']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/',
'http://server/#!/somePath']);
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/redirectPath', 'http://server/#/somePath',
'http://server/#/somePath']);
toEqual(['before', 'http://server/#!/redirectPath', 'http://server/#!/somePath',
'http://server/#!/somePath']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/redirectPath', 'http://server/#/somePath',
'http://server/#/redirectPath']);
toEqual(['after', 'http://server/#!/redirectPath', 'http://server/#!/somePath',
'http://server/#!/redirectPath']);
expect($location.url()).toEqual('/redirectPath');
expect($browser.url()).toEqual('http://server/#/redirectPath');
expect($browser.url()).toEqual('http://server/#!/redirectPath');
})
);
@@ -2200,7 +2222,7 @@ describe('$location', function() {
inject(function($location, $browser, $rootScope, $log) {
$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
$log.info('before', newUrl, oldUrl, $browser.url());
if (newUrl === 'http://server/#/somePath') {
if (newUrl === 'http://server/#!/somePath') {
event.preventDefault();
$location.url('/redirectPath');
}
@@ -2209,28 +2231,28 @@ describe('$location', function() {
$log.info('after', newUrl, oldUrl, $browser.url());
});
$browser.url('http://server/#/somePath');
$browser.url('http://server/#!/somePath');
$browser.poll();
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/somePath', 'http://server/',
'http://server/#/somePath']);
toEqual(['before', 'http://server/#!/somePath', 'http://server/',
'http://server/#!/somePath']);
expect($log.info.logs.shift()).
toEqual(['before', 'http://server/#/redirectPath', 'http://server/#/somePath',
'http://server/#/somePath']);
toEqual(['before', 'http://server/#!/redirectPath', 'http://server/#!/somePath',
'http://server/#!/somePath']);
expect($log.info.logs.shift()).
toEqual(['after', 'http://server/#/redirectPath', 'http://server/#/somePath',
'http://server/#/redirectPath']);
toEqual(['after', 'http://server/#!/redirectPath', 'http://server/#!/somePath',
'http://server/#!/redirectPath']);
expect($location.url()).toEqual('/redirectPath');
expect($browser.url()).toEqual('http://server/#/redirectPath');
expect($browser.url()).toEqual('http://server/#!/redirectPath');
})
);
it('should listen on click events on href and prevent browser default in hashbang mode', function() {
module(function() {
return function($rootElement, $compile, $rootScope) {
$rootElement.html('<a href="http://server/#/somePath">link</a>');
$rootElement.html('<a href="http://server/#!/somePath">link</a>');
$compile($rootElement)($rootScope);
jqLite(document.body).append($rootElement);
};
@@ -2310,7 +2332,7 @@ describe('$location', function() {
});
// change through $browser
$browser.url(base + '#/myNewPath');
$browser.url(base + '#!/myNewPath');
$browser.poll();
expect(log).toEqual(['/myNewPath', '/', '/myNewPath']);
+1 -1
View File
@@ -1073,7 +1073,7 @@ describe('$route', function() {
expect($location.path()).toEqual('/bar/id3');
expect($browserUrl.calls.mostRecent().args)
.toEqual(['http://server/#/bar/id3?extra=eId', true, null]);
.toEqual(['http://server/#!/bar/id3?extra=eId', true, null]);
});
});
});