feat($location): add support for selectively rewriting links based on attribute

In HTML5 mode, links can now be selectively rewritten, by setting `mode.rewriteLinks` to a string
(denoting an attribute name). Anchor elements that have the specified attribute will be rewritten,
while other links will remain untouched.

This can be useful in situations where it is desirable to use HTML5 mode without a `<base>` tag, but
still support rewriting specific links only. See #14959 for more details on a possible usecase.

Closes #14976
This commit is contained in:
Wei Wang
2016-08-01 19:14:20 -04:00
committed by Georgios Kalpakas
parent 8df43677e2
commit 3d686a988d
3 changed files with 122 additions and 53 deletions
+33 -12
View File
@@ -91,15 +91,27 @@ To configure the `$location` service, retrieve the
{@link ng.$locationProvider $locationProvider} and set the parameters as follows:
- **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`
- **html5Mode(mode)**: `{boolean|Object}`<br />
`false` or `{enabled: false}` (default) -
see [Hashbang mode](guide/$location#hashbang-mode-default-mode-)<br />
`true` or `{enabled: true}` -
see [HTML5 mode](guide/$location#html5-mode)<br />
`{..., requireBase: true/false}` (only affects HTML5 mode) -
see [Relative links](guide/$location#relative-links)<br />
`{..., rewriteLinks: true/false/'string'}` (only affects HTML5 mode) -
see [HTML link rewriting](guide/$location#html-link-rewriting)<br />
Default:
```j
{
enabled: false,
requireBase: true,
rewriteLinks: true
}
```
- **hashPrefix(prefix)**: {string}<br />
prefix used for Hashbang URLs (used in Hashbang mode or in legacy browser in Html5 mode)<br />
default: `"!"`
- **hashPrefix(prefix)**: `{string}`<br />
Prefix used for Hashbang URLs (used in Hashbang mode or in legacy browsers in HTML5 mode).<br />
Default: `'!'`
### Example configuration
```js
@@ -305,7 +317,7 @@ path and search. If the history API is not supported by a browser, `$location` s
URL. This frees you from having to worry about whether the browser viewing your app supports the
history API or not; the `$location` service makes this transparent to you.
### Html link rewriting
### HTML link rewriting
When you use HTML5 history API mode, you will not need special hashbang links. All you have to do
is specify regular URL links, such as: `<a href="/some?foo=bar">link</a>`
@@ -326,6 +338,18 @@ reload to the original link.
- Links starting with '/' that lead to a different base path<br>
Example: `<a href="/not-my-base/link">link</a>`
If `mode.rewriteLinks` is set to `false` in the `mode` configuration object passed to
`$locationProvider.html5Mode()`, the browser will perform a full page reload for every link.
`mode.rewriteLinks` can also be set to a string, which will enable link rewriting only on anchor
elements that have the given attribute.
For example, if `mode.rewriteLinks` is set to `'internal-link'`:
- `<a href="/some/path" internal-link>link</a>` will be rewritten
- `<a href="/some/path">link</a>` will perform a full page reload
Note that [attribute name normalization](guide/directive#normalization) does not apply here, so
`'internalLink'` will **not** match `'internal-link'`.
### Relative links
@@ -853,6 +877,3 @@ angular.module('locationExample', [])
# Related API
* {@link ng.$location `$location` API}
+43 -36
View File
@@ -87,13 +87,13 @@ function serverBase(url) {
/**
* LocationHtml5Url represents an url
* LocationHtml5Url represents a URL
* This object is exposed as $location service when HTML5 mode is enabled and supported
*
* @constructor
* @param {string} appBase application base URL
* @param {string} appBaseNoFile application base URL stripped of any filename
* @param {string} basePrefix url path prefix
* @param {string} basePrefix URL path prefix
*/
function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
this.$$html5 = true;
@@ -102,8 +102,8 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
* Parse given html5 (regular) url string into properties
* @param {string} url HTML5 url
* Parse given HTML5 (regular) URL string into properties
* @param {string} url HTML5 URL
* @private
*/
this.$$parse = function(url) {
@@ -165,7 +165,7 @@ function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
/**
* LocationHashbangUrl represents url
* LocationHashbangUrl represents URL
* This object is exposed as $location service when developer doesn't opt into html5 mode.
* It also serves as the base class for html5 mode fallback on legacy browsers.
*
@@ -180,8 +180,8 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
* Parse given hashbang url into properties
* @param {string} url Hashbang url
* Parse given hashbang URL into properties
* @param {string} url Hashbang URL
* @private
*/
this.$$parse = function(url) {
@@ -190,7 +190,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
if (!isUndefined(withoutBaseUrl) && withoutBaseUrl.charAt(0) === '#') {
// The rest of the url starts with a hash so we have
// The rest of the URL starts with a hash so we have
// got either a hashbang path or a plain hash fragment
withoutHashUrl = stripBaseUrl(hashPrefix, withoutBaseUrl);
if (isUndefined(withoutHashUrl)) {
@@ -255,7 +255,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
};
/**
* Compose hashbang url and update `absUrl` property
* Compose hashbang URL and update `absUrl` property
* @private
*/
this.$$compose = function() {
@@ -277,7 +277,7 @@ function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
/**
* LocationHashbangUrl represents url
* LocationHashbangUrl represents URL
* This object is exposed as $location service when html5 history api is enabled but the browser
* does not support it.
*
@@ -329,7 +329,7 @@ function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
var locationPrototype = {
/**
* Ensure absolute url is initialized.
* Ensure absolute URL is initialized.
* @private
*/
$$absUrl:'',
@@ -353,17 +353,17 @@ var locationPrototype = {
* @description
* This method is getter only.
*
* Return full url representation with all segments encoded according to rules specified in
* Return full URL representation with all segments encoded according to rules specified in
* [RFC 3986](http://www.ietf.org/rfc/rfc3986.txt).
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var absUrl = $location.absUrl();
* // => "http://example.com/#/some/path?foo=bar&baz=xoxo"
* ```
*
* @return {string} full url
* @return {string} full URL
*/
absUrl: locationGetter('$$absUrl'),
@@ -374,18 +374,18 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
* Return url (e.g. `/path?a=b#hash`) when called without any parameter.
* Return URL (e.g. `/path?a=b#hash`) when called without any parameter.
*
* Change path, search and hash, when called with parameter and return `$location`.
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var url = $location.url();
* // => "/some/path?foo=bar&baz=xoxo"
* ```
*
* @param {string=} url New url without base prefix (e.g. `/path?a=b#hash`)
* @param {string=} url New URL without base prefix (e.g. `/path?a=b#hash`)
* @return {string} url
*/
url: function(url) {
@@ -408,16 +408,16 @@ var locationPrototype = {
* @description
* This method is getter only.
*
* Return protocol of current url.
* Return protocol of current URL.
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var protocol = $location.protocol();
* // => "http"
* ```
*
* @return {string} protocol of current url
* @return {string} protocol of current URL
*/
protocol: locationGetter('$$protocol'),
@@ -428,24 +428,24 @@ var locationPrototype = {
* @description
* This method is getter only.
*
* Return host of current url.
* Return host of current URL.
*
* Note: compared to the non-angular version `location.host` which returns `hostname:port`, this returns the `hostname` portion only.
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var host = $location.host();
* // => "example.com"
*
* // given url http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* // given URL http://user:password@example.com:8080/#/some/path?foo=bar&baz=xoxo
* host = $location.host();
* // => "example.com"
* host = location.host;
* // => "example.com:8080"
* ```
*
* @return {string} host of current url.
* @return {string} host of current URL.
*/
host: locationGetter('$$host'),
@@ -456,11 +456,11 @@ var locationPrototype = {
* @description
* This method is getter only.
*
* Return port of current url.
* Return port of current URL.
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var port = $location.port();
* // => 80
* ```
@@ -476,7 +476,7 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
* Return path of current url when called without any parameter.
* Return path of current URL when called without any parameter.
*
* Change path when called with parameter and return `$location`.
*
@@ -485,7 +485,7 @@ var locationPrototype = {
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var path = $location.path();
* // => "/some/path"
* ```
@@ -505,13 +505,13 @@ var locationPrototype = {
* @description
* This method is getter / setter.
*
* Return search part (as object) of current url when called without any parameter.
* Return search part (as object) of current URL when called without any parameter.
*
* Change search part when called with parameter and return `$location`.
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo
* var searchObject = $location.search();
* // => {foo: 'bar', baz: 'xoxo'}
*
@@ -527,7 +527,7 @@ var locationPrototype = {
* of `$location` to the specified value.
*
* If the argument is a hash object containing an array of values, these values will be encoded
* as duplicate search parameters in the url.
* as duplicate search parameters in the URL.
*
* @param {(string|Number|Array<string>|boolean)=} paramValue If `search` is a string or number, then `paramValue`
* will override only a single search property.
@@ -589,7 +589,7 @@ var locationPrototype = {
*
*
* ```js
* // given url http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
* // given URL http://example.com/#/some/path?foo=bar&baz=xoxo#hashValue
* var hash = $location.hash();
* // => "hashValue"
* ```
@@ -750,8 +750,12 @@ function $LocationProvider() {
* 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}
* - **rewriteLinks** - `{boolean}` - (default: `true`) When html5Mode is enabled,
* enables/disables url rewriting for relative links.
* - **rewriteLinks** - `{boolean|string}` - (default: `true`) When html5Mode is enabled,
* enables/disables URL rewriting for relative links. If set to a string, URL rewriting will
* only happen on links with an attribute that matches the given string. For example, if set
* to `'internal-link'`, then the URL will only be rewritten for `<a internal-link>` links.
* Note that [attribute name normalization](guide/directive#normalization) does not apply
* here, so `'internalLink'` will **not** match `'internal-link'`.
*
* @returns {Object} html5Mode object if used as getter or itself (chaining) if used as setter
*/
@@ -769,7 +773,7 @@ function $LocationProvider() {
html5Mode.requireBase = mode.requireBase;
}
if (isBoolean(mode.rewriteLinks)) {
if (isBoolean(mode.rewriteLinks) || isString(mode.rewriteLinks)) {
html5Mode.rewriteLinks = mode.rewriteLinks;
}
@@ -866,10 +870,11 @@ function $LocationProvider() {
}
$rootElement.on('click', function(event) {
var rewriteLinks = html5Mode.rewriteLinks;
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
// currently we open nice url link and redirect then
if (!html5Mode.rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return;
if (!rewriteLinks || event.ctrlKey || event.metaKey || event.shiftKey || event.which === 2 || event.button === 2) return;
var elm = jqLite(event.target);
@@ -879,6 +884,8 @@ function $LocationProvider() {
if (elm[0] === $rootElement[0] || !(elm = elm.parent())[0]) return;
}
if (isString(rewriteLinks) && isUndefined(elm.attr(rewriteLinks))) return;
var absHref = elm.prop('href');
// get the actual href attribute - see
// http://msdn.microsoft.com/en-us/library/ie/dd347148(v=vs.85).aspx
+46 -5
View File
@@ -1588,7 +1588,7 @@ describe('$location', function() {
it('should not rewrite links when rewriting links is disabled', function() {
configureTestLink({linkHref: 'link?a#b', html5Mode: {enabled: true, rewriteLinks:false}, supportHist: true});
configureTestLink({linkHref: 'link?a#b'});
initService({html5Mode:{enabled: true, rewriteLinks:false},supportHistory:true});
inject(
initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
@@ -1601,6 +1601,34 @@ describe('$location', function() {
});
it('should rewrite links when the specified rewriteLinks attr is present', function() {
configureTestLink({linkHref: 'link?a#b', attrs: 'do-rewrite'});
initService({html5Mode: {enabled: true, rewriteLinks: 'do-rewrite'}, supportHistory: true});
inject(
initBrowser({url: 'http://host.com/base/index.html', basePath: '/base/index.html'}),
setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/link?a#b');
}
);
});
it('should not rewrite links when the specified rewriteLinks attr is not present', function() {
configureTestLink({linkHref: 'link?a#b'});
initService({html5Mode: {enabled: true, rewriteLinks: 'do-rewrite'}, supportHistory: true});
inject(
initBrowser({url: 'http://host.com/base/index.html', basePath: '/base/index.html'}),
setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
}
);
});
it('should rewrite full url links to same domain and base path', function() {
configureTestLink({linkHref: 'http://host.com/base/new'});
initService({html5Mode:true,supportHistory:false,hashPrefix:'!'});
@@ -1692,7 +1720,7 @@ describe('$location', function() {
it('should not rewrite when full link to different base path when history enabled on old browser',
function() {
configureTestLink({linkHref: 'http://host.com/other_base/link', html5Mode: true, supportHist: false});
configureTestLink({linkHref: 'http://host.com/other_base/link'});
inject(
initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
setupRewriteChecks(),
@@ -2343,7 +2371,7 @@ describe('$location', function() {
describe('$locationProvider', function() {
describe('html5Mode', function() {
it('should set enabled, requireBase and rewriteLinks when called with object', function() {
it('should set enabled, requireBase and rewriteLinks when called with object', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({enabled: true, requireBase: false, rewriteLinks: false});
expect($locationProvider.html5Mode()).toEqual({
@@ -2357,12 +2385,12 @@ describe('$location', function() {
});
it('should only overwrite existing properties if values are boolean', function() {
it('should only overwrite existing properties if values are of the correct type', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
enabled: 'duh',
requireBase: 'probably',
rewriteLinks: 'nope'
rewriteLinks: 0
});
expect($locationProvider.html5Mode()).toEqual({
@@ -2376,6 +2404,19 @@ describe('$location', function() {
});
it('should support setting rewriteLinks to a string', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({
rewriteLinks: 'yes-rewrite'
});
expect($locationProvider.html5Mode().rewriteLinks).toEqual('yes-rewrite');
});
inject(function() {});
});
it('should not set unknown input properties to html5Mode object', function() {
module(function($locationProvider) {
$locationProvider.html5Mode({