diff --git a/docs/api/README.md b/docs/api/README.md index a9e381452..7570dee96 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -131,6 +131,30 @@ In this case the `` will be the actual link (and will get the correct `href`) Configure the active CSS class applied when the link is active with exact match. Note the default value can also be configured globally via the `linkExactActiveClass` router constructor option. +### exact-path + +- type: `boolean` +- default: `false` + + The exact active class matching behavior is **strict match**. For example, `` will get this class applied as long as the current path is `/a`. + + One consequence of this is that `` won't be active when the query parameters do not match. To force the link into "exact path match mode", use the `exact-path` prop: + + ```html + + + ``` + + This is useful when using pagination + +### exact-path-active-class + +- type: `string` +- default: `"router-link-exact-path-active"` + + Configure the active CSS class applied when the link is active with exact path match. Note the default value can also be configured globally via the `linkExactPathActiveClass` router constructor option. + + ## `` The `` component is a functional component that renders the matched component for the given path. Components rendered in `` can also contain its own ``, which will render components for nested paths. @@ -260,6 +284,15 @@ Since it's just a component, it works with `` and ``. Wh Setting this to `false` essentially makes every `router-link` navigation a full page refresh in IE9. This is useful when the app is server-rendered and needs to work in IE9, because a hash mode URL does not work with SSR. +### linkExactPathActiveClass + +- type: `string` + +- default: `"router-link-exact-path-active"` + + Globally configure `` default active class for exact path matches. Also see [router-link](router-link.md). + + ## Router Instance Properties ### router.app diff --git a/examples/active-links/app.js b/examples/active-links/app.js index a1a3238d4..e68b4d0dd 100644 --- a/examples/active-links/app.js +++ b/examples/active-links/app.js @@ -42,6 +42,7 @@ new Vue({
  • /users
  • /users (exact match)
  • +
  • /users?foo=bar (exact path match)
  • /users/evan
  • /users/evan#foo
  • @@ -60,6 +61,11 @@ new Vue({ /users/evan?foo=bar&baz=qux
    +
  • + + /users/evan?baz=qux (named view + exact path match) + +
  • /about
  • diff --git a/examples/active-links/index.html b/examples/active-links/index.html index 3f7e03489..7e152095b 100644 --- a/examples/active-links/index.html +++ b/examples/active-links/index.html @@ -7,6 +7,9 @@ a.router-link-exact-active, li.router-link-exact-active a { border-bottom: 1px solid #f66; } +a.router-link-exact-path-active, li.router-link-exact-path-active a { + border-bottom: 1px solid #f66; +}
    ← Examples index
    diff --git a/flow/declarations.js b/flow/declarations.js index 0072907a1..e6dad14b4 100644 --- a/flow/declarations.js +++ b/flow/declarations.js @@ -36,6 +36,7 @@ declare type RouterOptions = { fallback?: boolean; base?: string; linkActiveClass?: string; + linkExactPathActiveClass?: string; parseQuery?: (query: string) => Object; stringifyQuery?: (query: Object) => string; scrollBehavior?: ( diff --git a/src/components/link.js b/src/components/link.js index a6fbec0f6..58603d8d7 100644 --- a/src/components/link.js +++ b/src/components/link.js @@ -19,10 +19,12 @@ export default { default: 'a' }, exact: Boolean, + exactPath: Boolean, append: Boolean, replace: Boolean, activeClass: String, exactActiveClass: String, + exactPathActiveClass: String, event: { type: eventTypes, default: 'click' @@ -36,6 +38,7 @@ export default { const classes = {} const globalActiveClass = router.options.linkActiveClass const globalExactActiveClass = router.options.linkExactActiveClass + const globalExactPathActiveClass = router.options.linkExactPathActiveClass // Support global empty active class const activeClassFallback = globalActiveClass == null ? 'router-link-active' @@ -43,20 +46,29 @@ export default { const exactActiveClassFallback = globalExactActiveClass == null ? 'router-link-exact-active' : globalExactActiveClass + const exactPathActiveClassFallback = globalExactPathActiveClass == null + ? 'router-link-exact-path-active' + : globalExactPathActiveClass const activeClass = this.activeClass == null ? activeClassFallback : this.activeClass const exactActiveClass = this.exactActiveClass == null ? exactActiveClassFallback : this.exactActiveClass + const exactPathActiveClass = this.exactPathActiveClass == null + ? exactPathActiveClassFallback + : this.exactPathActiveClass const compareTarget = location.path ? createRoute(null, location, null, router) : route + classes[exactPathActiveClass] = this.exactPath && isSameRoute(current, compareTarget, true) classes[exactActiveClass] = isSameRoute(current, compareTarget) classes[activeClass] = this.exact ? classes[exactActiveClass] - : isIncludedRoute(current, compareTarget) + : this.exactPath + ? classes[exactPathActiveClass] + : isIncludedRoute(current, compareTarget) const handler = e => { if (guardEvent(e)) { diff --git a/src/util/route.js b/src/util/route.js index 54a91a738..84c938313 100644 --- a/src/util/route.js +++ b/src/util/route.js @@ -70,7 +70,7 @@ function getFullPath ( return (path || '/') + stringify(query) + hash } -export function isSameRoute (a: Route, b: ?Route): boolean { +export function isSameRoute (a: Route, b: ?Route, exactPath: ?boolean): boolean { if (b === START) { return a === b } else if (!b) { @@ -78,14 +78,12 @@ export function isSameRoute (a: Route, b: ?Route): boolean { } else if (a.path && b.path) { return ( a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && - a.hash === b.hash && - isObjectEqual(a.query, b.query) + (exactPath || (a.hash === b.hash && isObjectEqual(a.query, b.query))) ) } else if (a.name && b.name) { return ( a.name === b.name && - a.hash === b.hash && - isObjectEqual(a.query, b.query) && + (exactPath || (a.hash === b.hash && isObjectEqual(a.query, b.query))) && isObjectEqual(a.params, b.params) ) } else { diff --git a/test/e2e/specs/active-links.js b/test/e2e/specs/active-links.js index 164978762..bada4805a 100644 --- a/test/e2e/specs/active-links.js +++ b/test/e2e/specs/active-links.js @@ -4,36 +4,40 @@ module.exports = { browser .url('http://localhost:8080/active-links/') .waitForElementVisible('#app', 1000) - .assert.count('li a', 11) + .assert.count('li a', 13) // assert correct href with base .assert.attributeContains('li:nth-child(1) a', 'href', '/active-links/') .assert.attributeContains('li:nth-child(2) a', 'href', '/active-links/') .assert.attributeContains('li:nth-child(3) a', 'href', '/active-links/users') .assert.attributeContains('li:nth-child(4) a', 'href', '/active-links/users') - .assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users/evan') - .assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan#foo') - .assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan?foo=bar') + .assert.attributeContains('li:nth-child(5) a', 'href', '/active-links/users?foo=bar') + .assert.attributeContains('li:nth-child(6) a', 'href', '/active-links/users/evan') + .assert.attributeContains('li:nth-child(7) a', 'href', '/active-links/users/evan#foo') .assert.attributeContains('li:nth-child(8) a', 'href', '/active-links/users/evan?foo=bar') - .assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar&baz=qux') - .assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/about') - .assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/about') + .assert.attributeContains('li:nth-child(9) a', 'href', '/active-links/users/evan?foo=bar') + .assert.attributeContains('li:nth-child(10) a', 'href', '/active-links/users/evan?foo=bar&baz=qux') + .assert.attributeContains('li:nth-child(11) a', 'href', '/active-links/users/evan?baz=qux') + .assert.attributeContains('li:nth-child(12) a', 'href', '/active-links/about') + .assert.attributeContains('li:nth-child(13) a', 'href', '/active-links/about') .assert.containsText('.view', 'Home') assertActiveLinks(1, [1, 2], null, [1, 2]) assertActiveLinks(2, [1, 2], null, [1, 2]) - assertActiveLinks(3, [1, 3, 4], null, [3, 4]) - assertActiveLinks(4, [1, 3, 4], null, [3, 4]) - assertActiveLinks(5, [1, 3, 5], null, [5]) - assertActiveLinks(6, [1, 3, 5, 6], null, [6]) - assertActiveLinks(7, [1, 3, 5, 7, 8], null, [7, 8]) - assertActiveLinks(8, [1, 3, 5, 7, 8], null, [7, 8]) - assertActiveLinks(9, [1, 3, 5, 7, 9], null, [9]) - assertActiveLinks(10, [1, 10], [11], [10], [11]) - assertActiveLinks(11, [1, 10], [11], [10], [11]) + assertActiveLinks(3, [1, 3, 4, 5], null, [3, 4], null, [5]) + assertActiveLinks(4, [1, 3, 4, 5], null, [3, 4], null, [5]) + assertActiveLinks(5, [1, 3, 5], null, [5], null, [5]) + assertActiveLinks(6, [1, 3, 6, 11], null, [6], null, [11]) + assertActiveLinks(7, [1, 3, 6, 7, 11], null, [7], null, [11]) + assertActiveLinks(8, [1, 3, 6, 8, 9, 11], null, [8, 9], null, [11]) + assertActiveLinks(9, [1, 3, 6, 8, 9, 11], null, [8, 9], null, [11]) + assertActiveLinks(10, [1, 3, 6, 8, 10, 11], null, [10], null, [11]) + assertActiveLinks(11, [1, 3, 6, 11], null, [11], null, [11]) + assertActiveLinks(12, [1, 12], [13], [12], [13]) + assertActiveLinks(13, [1, 12], [13], [12], [13]) browser.end() - function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI) { + function assertActiveLinks (n, activeA, activeLI, exactActiveA, exactActiveLI, exactPathActiveA, exactPathActiveLI) { browser.click(`li:nth-child(${n}) a`) activeA.forEach(i => { browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active') @@ -49,6 +53,14 @@ module.exports = { browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-active') .assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active') }) + exactPathActiveA && exactPathActiveA.forEach(i => { + browser.assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-exact-path-active') + .assert.cssClassPresent(`li:nth-child(${i}) a`, 'router-link-active') + }) + exactPathActiveLI && exactPathActiveLI.forEach(i => { + browser.assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-exact-path-active') + .assert.cssClassPresent(`li:nth-child(${i})`, 'router-link-active') + }) } } } diff --git a/test/unit/specs/route.spec.js b/test/unit/specs/route.spec.js index 8e8118537..0f10556e0 100644 --- a/test/unit/specs/route.spec.js +++ b/test/unit/specs/route.spec.js @@ -66,6 +66,18 @@ describe('Route utils', () => { expect(isSameRoute(a, b)).toBe(true) expect(isSameRoute(a, c)).toBe(false) }) + + it('exact path', () => { + const a = { path: '/abc' } + const b = { path: '/abc', query: { foo: 'bar' }, hash: '#foo' } + const c = { path: '/abc', query: { baz: 'qux' }} + const d = { path: '/xyz', query: { foo: 'bar' }} + expect(isSameRoute(a, b, true)).toBe(true) + expect(isSameRoute(a, c, true)).toBe(true) + expect(isSameRoute(a, d, true)).toBe(false) + expect(isSameRoute(b, c, true)).toBe(true) + expect(isSameRoute(b, d, true)).toBe(false) + }) }) describe('isIncludedRoute', () => { diff --git a/types/router.d.ts b/types/router.d.ts index c6c8eef65..d21113647 100644 --- a/types/router.d.ts +++ b/types/router.d.ts @@ -53,6 +53,7 @@ export interface RouterOptions { base?: string; linkActiveClass?: string; linkExactActiveClass?: string; + linkExactPathActiveClass?: string; parseQuery?: (query: string) => Object; stringifyQuery?: (query: Object) => string; scrollBehavior?: (