diff --git a/docs/content/error/$location/nobase.ngdoc b/docs/content/error/$location/nobase.ngdoc
new file mode 100644
index 000000000000..b1a489019d30
--- /dev/null
+++ b/docs/content/error/$location/nobase.ngdoc
@@ -0,0 +1,51 @@
+@ngdoc error
+@name $location:nobase
+@fullName $location in HTML5 mode requires a tag to be present!
+@description
+
+If you configure {@link ng.$location `$location`} to use
+{@ng.provider.$locationProvider `html5Mode`} (`history.pushState`), you need to specify the base URL for the application with a [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) tag.
+
+The base URL is then used to resolve all relative URLs throughout the application regardless of the
+entry point into the app.
+
+If you are deploying your app into the root context (e.g. `https://myapp.com/`), set the base URL to `/`:
+
+```html
+
+
+ ...
+
+```
+
+If you are deploying your app into a sub-context (e.g. `https://myapp.com/subapp/`), set the base URL to the
+URL of the subcontext:
+
+```html
+
+
+ ...
+
+```
+
+Before Angular 1.3 we didn't have this hard requirement and it was easy to write apps that worked
+when deployed in the root context but were broken when moved to a sub-context because in the
+sub-context all absolute urls would resolve to the root context of the app. To prevent this,
+use relative URLs throughout your app:
+
+```html
+
+User Profile
+
+
+
+User Profile
+
+```
+
+Additionally, if you want to support [browsers that don't have the `history.pushState`
+API](http://caniuse.com/#feat=history), the fallback mechanism provided by `$location`
+won't work well without specifying the base url of the application.
+
+In order to make it easier to migrate from hashbang mode to html5 mode, we require that the base
+URL is always specified when `$location`'s `html5mode` is enabled.
\ No newline at end of file
diff --git a/docs/content/guide/$location.ngdoc b/docs/content/guide/$location.ngdoc
index f7a1557a90f8..241bed03f2fe 100644
--- a/docs/content/guide/$location.ngdoc
+++ b/docs/content/guide/$location.ngdoc
@@ -325,20 +325,22 @@ to URLs that should be handled with `.`. Now, links to locations, which are not
are not prefixed with `.` and will not be intercepted by the `otherwise` rule in your `$routeProvider`.
-### Server side
+### Relative links
-Using this mode requires URL rewriting on server side, basically you have to rewrite all your links
-to entry point of your application (e.g. index.html)
+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 (``). With that, relative urls will
+always be resolved to this base url, event if the initial url of the document was different.
-### Relative links
+There is one exception: Links that only contain a hash fragment (e.g. ``)
+will only change `$location.hash()` and not modify the url otherwise. This is useful for scrolling
+to anchors on the same page without needing to know on which page the user currently is.
-Be sure to check all relative links, images, scripts etc. You must either specify the url base in
-the head of your main html file (``) or you must use absolute urls
-(starting with `/`) everywhere because relative urls will be resolved to absolute urls using the
-initial absolute url of the document, which is often different from the root of the application.
+### Server side
-Running Angular apps with the History API enabled from document root is strongly encouraged as it
-takes care of all relative link issues.
+Using this mode requires URL rewriting on server side, basically you have to rewrite all your links
+to entry point of your application (e.g. index.html). Requiring a `` tag is also important for
+this case, as it allows Angular to differentiate between the part of the url that is the application
+base and the path that should be handeled by the application.
### Sending links among different browsers
diff --git a/src/ng/location.js b/src/ng/location.js
index 98dec092dd26..2afd68818f60 100644
--- a/src/ng/location.js
+++ b/src/ng/location.js
@@ -127,21 +127,32 @@ function LocationHtml5Url(appBase, basePrefix) {
this.$$absUrl = appBaseNoFile + this.$$url.substr(1); // first char is always '/'
};
- this.$$rewrite = function(url) {
+ this.$$parseLinkUrl = function(url, relHref) {
+ if (relHref && relHref[0] === '#') {
+ // special case for links to hash fragments:
+ // keep the old url and only replace the hash fragment
+ this.hash(relHref.slice(1));
+ return true;
+ }
var appUrl, prevAppUrl;
+ var rewrittenUrl;
if ( (appUrl = beginsWith(appBase, url)) !== undefined ) {
prevAppUrl = appUrl;
if ( (appUrl = beginsWith(basePrefix, appUrl)) !== undefined ) {
- return appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
+ rewrittenUrl = appBaseNoFile + (beginsWith('/', appUrl) || appUrl);
} else {
- return appBase + prevAppUrl;
+ rewrittenUrl = appBase + prevAppUrl;
}
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) !== undefined ) {
- return appBaseNoFile + appUrl;
+ rewrittenUrl = appBaseNoFile + appUrl;
} else if (appBaseNoFile == url + '/') {
- return appBaseNoFile;
+ rewrittenUrl = appBaseNoFile;
+ }
+ if (rewrittenUrl) {
+ this.$$parse(rewrittenUrl);
}
+ return !!rewrittenUrl;
};
}
@@ -231,10 +242,12 @@ function LocationHashbangUrl(appBase, hashPrefix) {
this.$$absUrl = appBase + (this.$$url ? hashPrefix + this.$$url : '');
};
- this.$$rewrite = function(url) {
+ this.$$parseLinkUrl = function(url, relHref) {
if(stripHash(appBase) == stripHash(url)) {
- return url;
+ this.$$parse(url);
+ return true;
}
+ return false;
};
}
@@ -254,16 +267,28 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
var appBaseNoFile = stripFile(appBase);
- this.$$rewrite = function(url) {
+ this.$$parseLinkUrl = function(url, relHref) {
+ if (relHref && relHref[0] === '#') {
+ // special case for links to hash fragments:
+ // keep the old url and only replace the hash fragment
+ this.hash(relHref.slice(1));
+ return true;
+ }
+
+ var rewrittenUrl;
var appUrl;
if ( appBase == stripHash(url) ) {
- return url;
+ rewrittenUrl = url;
} else if ( (appUrl = beginsWith(appBaseNoFile, url)) ) {
- return appBase + hashPrefix + appUrl;
+ rewrittenUrl = appBase + hashPrefix + appUrl;
} else if ( appBaseNoFile === url + '/') {
- return appBaseNoFile;
+ rewrittenUrl = appBaseNoFile;
}
+ if (rewrittenUrl) {
+ this.$$parse(rewrittenUrl);
+ }
+ return !!rewrittenUrl;
};
this.$$compose = function() {
@@ -626,6 +651,10 @@ function $LocationProvider(){
appBase;
if (html5Mode) {
+ if (!baseHref) {
+ throw $locationMinErr('nobase',
+ "$location in HTML5 mode requires a tag to be present!");
+ }
appBase = serverBase(initialUrl) + (baseHref || '/');
LocationMode = $sniffer.history ? LocationHtml5Url : LocationHashbangInHtml5Url;
} else {
@@ -633,7 +662,7 @@ function $LocationProvider(){
LocationMode = LocationHashbangUrl;
}
$location = new LocationMode(appBase, '#' + hashPrefix);
- $location.$$parse($location.$$rewrite(initialUrl));
+ $location.$$parseLinkUrl(initialUrl, initialUrl);
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
@@ -652,6 +681,9 @@ function $LocationProvider(){
}
var absHref = elm.prop('href');
+ // get the actual href attribute - see
+ // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
+ var relHref = elm.attr('href') || elm.attr('xlink:href');
if (isObject(absHref) && absHref.toString() === '[object SVGAnimatedString]') {
// SVGAnimatedString.animVal should be identical to SVGAnimatedString.baseVal, unless during
@@ -662,50 +694,15 @@ function $LocationProvider(){
// Ignore when url is started with javascript: or mailto:
if (IGNORE_URI_REGEXP.test(absHref)) return;
- // Make relative links work in HTML5 mode for legacy browsers (or at least IE8 & 9)
- // The href should be a regular url e.g. /link/somewhere or link/somewhere or ../somewhere or
- // somewhere#anchor or http://example.com/somewhere
- if (LocationMode === LocationHashbangInHtml5Url) {
- // get the actual href attribute - see
- // http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
- var href = elm.attr('href') || elm.attr('xlink:href');
-
- if (href && href.indexOf('://') < 0) { // Ignore absolute URLs
- var prefix = '#' + hashPrefix;
- if (href[0] == '/') {
- // absolute path - replace old path
- absHref = appBase + prefix + href;
- } else if (href[0] == '#') {
- // local anchor
- absHref = appBase + prefix + ($location.path() || '/') + href;
- } else {
- // relative path - join with current path
- var stack = $location.path().split("/"),
- parts = href.split("/");
- if (stack.length === 2 && !stack[1]) stack.length = 1;
- for (var i=0; i');
+ $documentProvider.$get = function() {
+ return {
+ 0: window.document,
+ find: jasmine.createSpy('find').andReturn(baseElement)
+ };
+ };
$browserProvider.$get = function($document, $window) {
var sniffer = {history: true, hashchange: false};
var logs = {log:[], warn:[], info:[], error:[]};
@@ -93,6 +100,7 @@ describe('$location', function() {
/* global Browser: false */
var b = new Browser($window, $document, fakeLog, sniffer);
b.pollFns = [];
+ b.$$baseHref = '/';
return b;
};
});
@@ -796,18 +804,6 @@ describe('$location', function() {
);
});
-
- it('should set appBase to serverBase if base[href] is missing', function() {
- initService({html5Mode:true,hashPrefix: '!',supportHistory: true});
- inject(
- initBrowser({url:'http://domain.com/my/view1#anchor1',basePath: ''}),
- function($rootScope, $location, $browser) {
- expect($browser.url()).toBe('http://domain.com/my/view1#anchor1');
- expect($location.path()).toBe('/my/view1');
- expect($location.hash()).toBe('anchor1');
- }
- );
- });
});
describe('PATH_MATCH', function() {
@@ -854,12 +850,12 @@ describe('$location', function() {
module(function($provide, $locationProvider) {
attrs = attrs ? ' ' + attrs + ' ' : '';
- // fake the base behavior
if (typeof linkHref === 'string') {
if (!relLink) {
if (linkHref[0] == '/') {
linkHref = 'http://host.com' + linkHref;
} else if(!linkHref.match(/:\/\//)) {
+ // fake the behavior of tag
linkHref = 'http://host.com/base/' + linkHref;
}
}
@@ -884,8 +880,8 @@ describe('$location', function() {
}
function initBrowser() {
- return function($browser){
- $browser.url('http://host.com/base');
+ return function($browser, $document){
+ $browser.url('http://host.com/base/index.html');
$browser.$$baseHref = '/base/index.html';
};
}
@@ -999,12 +995,14 @@ describe('$location', function() {
it('should produce relative paths correctly when $location.path() is "/" when history enabled on old browser', function() {
- configureService({linkHref: 'partial1', html5Mode: true, supportHist: false, relLink: true});
+ configureService({linkHref: 'partial1', html5Mode: true, supportHist: false});
inject(
initBrowser(),
initLocation(),
- function($browser, $location) {
- $location.path('/');
+ function($browser, $location, $rootScope) {
+ $rootScope.$apply(function() {
+ $location.path('/');
+ });
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/index.html#!/partial1');
}
@@ -1199,52 +1197,40 @@ describe('$location', function() {
);
});
-
- it('should rewrite relative links relative to current path when history disabled', function() {
- configureService({linkHref: 'link', html5Mode: true, supportHist: false, relLink: true});
- inject(
- initBrowser(),
- initLocation(),
- function($browser, $location) {
- $location.path('/some');
- browserTrigger(link, 'click');
- expectRewriteTo($browser, 'http://host.com/base/index.html#!/some/link');
- }
- );
- });
-
-
- it('should replace current path when link begins with "/" and history disabled', function() {
- configureService({linkHref: '/link', html5Mode: true, supportHist: false, relLink: true});
+ it('should replace current hash fragment when link begins with "#" history disabled', function() {
+ configureService({linkHref: '#link', html5Mode: true, supportHist: false, relLink: true});
inject(
initBrowser(),
initLocation(),
- function($browser, $location) {
- $location.path('/some');
+ function($browser, $location, $rootScope) {
+ $rootScope.$apply(function() {
+ $location.path('/some');
+ $location.hash('foo');
+ });
browserTrigger(link, 'click');
- expectRewriteTo($browser, 'http://host.com/base/index.html#!/link');
+ expect($location.hash()).toBe('link');
+ expectRewriteTo($browser, 'http://host.com/base/index.html#!/some#link');
}
);
});
-
- it('should replace current hash fragment when link begins with "#" history disabled', function() {
- configureService({linkHref: '#link', html5Mode: true, supportHist: false, relLink: true});
+ it('should replace current hash fragment when link begins with "#" history enabled', function() {
+ configureService({linkHref: '#link', html5Mode: true, supportHist: true, relLink: true});
inject(
initBrowser(),
initLocation(),
- function($browser, $location) {
- // Initialize browser URL
- $location.path('/some');
- $location.hash('foo');
+ function($browser, $location, $rootScope) {
+ $rootScope.$apply(function() {
+ $location.path('/some');
+ $location.hash('foo');
+ });
browserTrigger(link, 'click');
expect($location.hash()).toBe('link');
- expectRewriteTo($browser, 'http://host.com/base/index.html#!/some#link');
+ expectRewriteTo($browser, 'http://host.com/base/some#link');
}
);
});
-
// don't run next tests on IE<9, as browserTrigger does not simulate pressed keys
if (!msie || msie >= 9) {
@@ -1356,7 +1342,8 @@ describe('$location', function() {
var event = {
target: jqLite(window.document.body).find('a')[0],
- preventDefault: jasmine.createSpy('preventDefault')
+ preventDefault: jasmine.createSpy('preventDefault'),
+ isDefaultPrevented: jasmine.createSpy().andReturn(false)
};
@@ -1386,7 +1373,8 @@ describe('$location', function() {
var event = {
target: jqLite(window.document.body).find('a')[0],
- preventDefault: jasmine.createSpy('preventDefault')
+ preventDefault: jasmine.createSpy('preventDefault'),
+ isDefaultPrevented: jasmine.createSpy().andReturn(false)
};
@@ -1551,8 +1539,12 @@ describe('$location', function() {
it('should listen on click events on href and prevent browser default in html5 mode', function() {
- module(function($locationProvider) {
+ module(function($locationProvider, $provide) {
$locationProvider.html5Mode(true);
+ $provide.decorator('$browser', function($delegate) {
+ $delegate.$$baseHref = '/';
+ return $delegate;
+ });
return function($rootElement, $compile, $rootScope) {
$rootElement.html('link');
$compile($rootElement)($rootScope);
@@ -1609,6 +1601,13 @@ describe('$location', function() {
);
});
+ function parseLinkAndReturn(location, url, relHref) {
+ if (location.$$parseLinkUrl(url, relHref)) {
+ return location.absUrl();
+ }
+ return undefined;
+ }
+
describe('LocationHtml5Url', function() {
var location, locationIndex;
@@ -1618,13 +1617,18 @@ describe('$location', function() {
});
it('should rewrite URL', function() {
- expect(location.$$rewrite('http://other')).toEqual(undefined);
- expect(location.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
- expect(location.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
- expect(location.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
- expect(locationIndex.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
- expect(locationIndex.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
- expect(locationIndex.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
+ expect(parseLinkAndReturn(location, 'http://other')).toEqual(undefined);
+ expect(parseLinkAndReturn(location, 'http://server/pre')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(location, 'http://server/pre/')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(location, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
+ // Note: relies on the previous state!
+ expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
+
+ expect(parseLinkAndReturn(locationIndex, 'http://server/pre')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationIndex, 'http://server/pre/')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationIndex, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
+ // Note: relies on the previous state!
+ expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
});
});
@@ -1632,14 +1636,21 @@ describe('$location', function() {
describe('LocationHashbangUrl', function() {
var location;
+ function parseLinkAndReturn(location, url, relHref) {
+ if (location.$$parseLinkUrl(url, relHref)) {
+ return location.absUrl();
+ }
+ return undefined;
+ }
+
it('should rewrite URL', function() {
/* jshint scripturl: true */
location = new LocationHashbangUrl('http://server/pre/', '#');
- expect(location.$$rewrite('http://other')).toEqual(undefined);
- expect(location.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
- expect(location.$$rewrite('http://server/pre/#otherPath')).toEqual('http://server/pre/#otherPath');
- expect(location.$$rewrite('javascript:void(0)')).toEqual(undefined);
+ expect(parseLinkAndReturn(location, 'http://other')).toEqual(undefined);
+ expect(parseLinkAndReturn(location, 'http://server/pre/')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(location, 'http://server/pre/#otherPath')).toEqual('http://server/pre/#/otherPath');
+ expect(parseLinkAndReturn(location, 'javascript:void(0)')).toEqual(undefined);
});
it("should not set hash if one was not originally specified", function() {
@@ -1689,13 +1700,18 @@ describe('$location', function() {
});
it('should rewrite URL', function() {
- expect(location.$$rewrite('http://other')).toEqual(undefined);
- expect(location.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
- expect(location.$$rewrite('http://server/pre/')).toEqual('http://server/pre/');
- expect(location.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/#!otherPath');
- expect(locationIndex.$$rewrite('http://server/pre')).toEqual('http://server/pre/');
- expect(locationIndex.$$rewrite('http://server/pre/')).toEqual(undefined);
- expect(locationIndex.$$rewrite('http://server/pre/otherPath')).toEqual('http://server/pre/index.html#!otherPath');
+ expect(parseLinkAndReturn(location, 'http://other')).toEqual(undefined);
+ expect(parseLinkAndReturn(location, 'http://server/pre')).toEqual('http://server/pre/#!');
+ expect(parseLinkAndReturn(location, 'http://server/pre/')).toEqual('http://server/pre/#!');
+ expect(parseLinkAndReturn(location, 'http://server/pre/otherPath')).toEqual('http://server/pre/#!/otherPath');
+ // Note: relies on the previous state!
+ expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/#!/otherPath#test');
+
+ expect(parseLinkAndReturn(locationIndex, 'http://server/pre')).toEqual('http://server/pre/index.html#!');
+ expect(parseLinkAndReturn(locationIndex, 'http://server/pre/')).toEqual(undefined);
+ expect(parseLinkAndReturn(locationIndex, 'http://server/pre/otherPath')).toEqual('http://server/pre/index.html#!/otherPath');
+ // Note: relies on the previous state!
+ expect(parseLinkAndReturn(locationIndex, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/index.html#!/otherPath#test');
});
});
});