feat($location): add ability to opt-out of <base/> tag requirement in html5Mode

This feature allows disabling Angular's requirement of using a <base/> tag
when using location in html5Mode, for applications that do not require
using $location in html5Mode in IE9. To accomplish this, the $locationProvider.html5Mode 
method has been changed to accept a definition object which can optionally set a 
requireBase property to false, removing the requirement of a <base> tag being present
when html5Mode is enabled.

BREAKING CHANGE: The $location.html5Mode API has changed to allow enabling html5Mode by
    passing an object (as well as still supporting passing a boolean). Symmetrically, the
    method now returns an object instead of a boolean value.

    To migrate, follow the code example below:

    Before:

    var mode = $locationProvider.html5Mode();

    After:

    var mode = $locationProvider.html5Mode().enabled;

Fixes #8934
This commit is contained in:
Jeff Cross
2014-09-22 11:52:01 -07:00
parent ace40d5526
commit dc3de7fb7a
4 changed files with 155 additions and 15 deletions

View File

@@ -4,7 +4,19 @@
@description
If you configure {@link ng.$location `$location`} to use
{@link api/ng.provider.$locationProvider `html5Mode`} (`history.pushState`), you need to specify the base URL for the application with a [`<base href="">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag.
{@link api/ng.provider.$locationProvider `html5Mode`} (`history.pushState`), you need to specify the base URL for the application with a [`<base href="">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag or configure
`$locationProvider` to not require a base tag by passing a definition object with
`requireBase:false` to `$locationProvider.html5Mode()`:
```javascript
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
```
Note that removing the requirement for a <base> tag will have adverse side effects when resolving
relative paths with `$location` in IE9.
The base URL is then used to resolve all relative URLs throughout the application regardless of the
entry point into the app.

View File

@@ -91,10 +91,11 @@ To configure the `$location` service, retrieve the
{@link ng.$locationProvider $locationProvider} and set the parameters as follows:
- **html5Mode(mode)**: {boolean}<br />
`true` - see HTML5 mode<br />
`false` - see Hashbang mode<br />
default: `false`
- **html5Mode(mode)**: {boolean|Object}<br />
`true` or `enabled:true` - see HTML5 mode<br />
`false` or `enabled:false` - see Hashbang mode<br />
`requireBase:true` - see Relative links<br />
default: `enabled:false`
- **hashPrefix(prefix)**: {string}<br />
prefix used for Hashbang URLs (used in Hashbang mode or in legacy browser in Html5 mode)<br />
@@ -328,9 +329,11 @@ reload to the original link.
### Relative links
Be sure to check all relative links, images, scripts etc. Angular requires you to specify the url base in
the head of your main html file (`<base href="/my-base">`). With that, relative urls will
always be resolved to this base url, event if the initial url of the document was different.
Be sure to check all relative links, images, scripts etc. Angular requires you to specify the url
base in the head of your main html file (`<base href="/my-base">`) unless `html5Mode.requireBase` is
set to `false` in the html5Mode definition object passed to `$locationProvider.html5Mode()`. With
that, relative urls will always be resolved to this base url, event if the initial url of the
document was different.
There is one exception: Links that only contain a hash fragment (e.g. `<a href="#target">`)
will only change `$location.hash()` and not modify the url otherwise. This is useful for scrolling

View File

@@ -584,7 +584,10 @@ function locationGetterSetter(property, preprocess) {
*/
function $LocationProvider(){
var hashPrefix = '',
html5Mode = false;
html5Mode = {
enabled: false,
requireBase: true
};
/**
* @ngdoc method
@@ -606,12 +609,30 @@ function $LocationProvider(){
* @ngdoc method
* @name $locationProvider#html5Mode
* @description
* @param {boolean=} mode Use HTML5 strategy if available.
* @returns {*} current value if used as getter or itself (chaining) if used as setter
* @param {(boolean|Object)=} mode If boolean, sets `html5Mode.enabled` to value.
* If object, sets `enabled` and `requireBase` to respective values.
* - **enabled** `{boolean}` Sets `html5Mode.enabled`. If true, will rely on
* `history.pushState` to change urls where supported. Will fall back to hash-prefixed paths
* in browsers that do not support `pushState`.
* - **requireBase** - `{boolean}` - Sets `html5Mode.requireBase` (default: `true`). When
* html5Mode is enabled, specifies whether or not a <base> tag is required to be present. If
* `enabled` and `requireBase` are true, and a base tag is not present, an error will be
* thrown when `$location` is injected. See the
* {@link guide/$location $location guide for more information}
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
this.html5Mode = function(mode) {
if (isDefined(mode)) {
html5Mode = mode;
if (isBoolean(mode)) {
html5Mode.enabled = mode;
return this;
} else if (isObject(mode)) {
html5Mode.enabled = isBoolean(mode.enabled) ?
mode.enabled :
html5Mode.enabled;
html5Mode.requireBase = isBoolean(mode.requireBase) ?
mode.requireBase :
html5Mode.requireBase;
return this;
} else {
return html5Mode;
@@ -653,8 +674,8 @@ function $LocationProvider(){
initialUrl = $browser.url(),
appBase;
if (html5Mode) {
if (!baseHref) {
if (html5Mode.enabled) {
if (!baseHref && html5Mode.requireBase) {
throw $locationMinErr('nobase',
"$location in HTML5 mode requires a <base> tag to be present!");
}

View File

@@ -1639,6 +1639,77 @@ describe('$location', function() {
return undefined;
}
describe('html5Mode', function() {
it('should set enabled and requireBase when called with object', function() {
module(function($locationProvider) {
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should only overwrite existing properties if values are boolean', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
enabled: 'duh',
requireBase: 'probably'
});
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should not set unknown input properties to html5Mode object', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
someProp: 'foo'
});
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should default to enabled:false and requireBase:true', function() {
module(function($locationProvider) {
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
it('should return html5Mode object when called without value', function() {
module(function($locationProvider) {
expect($locationProvider.html5Mode()).toEqual({
enabled: false,
requireBase: true
});
});
inject(function(){});
});
});
describe('LocationHtml5Url', function() {
var location, locationIndex;
@@ -1661,6 +1732,39 @@ describe('$location', function() {
// Note: relies on the previous state!
expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
});
it('should complain if no base tag present', function() {
module(function($locationProvider) {
$locationProvider.html5Mode(true);
});
inject(function($browser, $injector) {
$browser.$$baseHref = undefined;
expect(function() {
$injector.get('$location');
}).toThrowMinErr('$location', 'nobase',
"$location in HTML5 mode requires a <base> tag to be present!");
});
});
it('should not complain if baseOptOut set to true in html5Mode', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
enabled: true,
requireBase: false
});
});
inject(function($browser, $injector) {
$browser.$$baseHref = undefined;
expect(function() {
$injector.get('$location');
}).not.toThrowMinErr('$location', 'nobase',
"$location in HTML5 mode requires a <base> tag to be present!");
});
});
});