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'); }); }); });