Nested Routes
It’s worth noting that nested routes will match only if the params relevant to the rendered route are the same.
E.g., given these routes:
const routes = [
{
path: '/parent/:id',
children: [
// empty child
{ path: '' },
// child with id
{ path: 'child/:id' },
{ path: 'child-second/:id' }
]
}
]
If the current route is /parent/1/child/2
, these links will be active:
url | active | exact active |
---|---|---|
/parent/1/child/2 | ✅ | ✅ |
/parent/1/child/3 | ❌ | ❌ |
/parent/1/child-second/2 | ❌ | ❌ |
/parent/1 | ✅ | ❌ |
/parent/2 | ❌ | ❌ |
/parent/2/child/2 | ❌ | ❌ |
/parent/2/child-second/2 | ❌ | ❌ |
Unrelated but similiar routes
Routes that are unrelated from a record point of view but share a common path are no longer active.
E.g., given these routes:
const routes = [
{ path: '/movies' },
{ path: '/movies/new' },
{ path: '/movies/search' }
]
If the current route is /movies/new
, these links will be active:
url | active | exact active |
---|---|---|
/movies | ❌ | ❌ |
/movies/new | ✅ | ✅ |
/movies/search | ❌ | ❌ |
Note: This behavior is different from actual behavior.
It’s worth noting, it is possible to nest them to still benefit from links being active:
// Vue 3
import { h } from 'vue'
import { RouterView } from 'vue-router'
const routes = [
{
path: '/movies',
// we need this to render the children (see note below)
component: { render: () => h(RouterView) },
// for vue 2 use render: h => h('RouterView')
children: [
{ path: 'new' },
// different child
{ path: 'search' }
]
}
]
If the current route is /movies/new
, these links will be active:
url | active | exact active |
---|---|---|
/movies | ✅ | ❌ |
/movies/new | ✅ | ✅ |
/movies/search | ❌ | ❌ |
Note: To make this easier to use, we could maybe allow component
to be absent and internally behave as if there where a component
option that renders a RouterView
component
Alias
Given that an alias is only a different path
while keeping everything else on a record, it makes sense for aliases to be active when the path
they are aliasing is matched and vice versa.
E.g., given these routes:
const routes = [{ path: '/movies', alias: ['/films'] }]
If the current route is /movies
or /films
, both links will be active:
url | active | exact active |
---|---|---|
/movies | ✅ | ✅ |
/films | ✅ | ✅ |
Nested aliases
The behavior is similar when dealing with nested children of an aliased route.
E.g., given these routes:
const routes = [
{
path: '/parent/:id',
alias: '/p/:id',
children: [
// empty child
{ path: '' },
// child with id
{ path: 'child/:id', alias: 'c/:id' }
]
}
]
If the current route is /parent/1/child/2
, /p/1/child/2
, /p/1/c/2
, or, /parent/1/c/2
these links will be active:
url | active | exact active |
---|---|---|
/parent/1/child/2 | ✅ | ✅ |
/parent/1/c/2 | ✅ | ✅ |
/p/1/child/2 | ✅ | ✅ |
/p/1/c/2 | ✅ | ✅ |
/p/1/child/3 | ❌ | ❌ |
/parent/1/child/3 | ❌ | ❌ |
/parent/1 | ✅ | ❌ |
/p/1 | ✅ | ❌ |
/parent/2 | ❌ | ❌ |
/p/2 | ❌ | ❌ |
Absolute nested aliases
Nested children can have an absolute path
by making it start with /
, in this scenario the same rules apply. Given these routes:
E.g., given these routes:
const routes = [
{
path: '/parent/:id',
alias: '/p/:id',
name: 'parent',
children: [
// empty child
{ path: '', alias: ['alias', '/p_:id'], name: 'child' },
// child with absolute path. we need to add an `id` because the parent needs it
{ path: '/p_:id/absolute-a', alias: 'as-absolute-a' },
// same as above but the alias is absolute
{ path: 'as-absolute-b', alias: '/p_:id/absolute-b' }
]
}
]
If the current route is /p_1/absolute-a
, /p/1/as-absolute-a
, or, /parent/1/as-absolute-a
, these links will be active:
url | active | exact active |
---|---|---|
/p/1/as-absolute-a | ✅ | ✅ |
/p_1/absolute-a | ✅ | ✅ |
/parent/1/absolute-a | ✅ | ✅ |
/parent/2/absolute-a | ❌ | ❌ |
/parent/1/absolute-b | ❌ | ❌ |
/p/1/absolute-b | ❌ | ❌ |
/p_1/absolute-b | ❌ | ❌ |
/parent/1 | ✅ | ❌ |
/p/1 | ✅ | ❌ |
/parent/1/alias | ✅ | ❌ |
/p/1/alias | ✅ | ❌ |
/p_1 | ✅ | ❌ |
/parent/2 | ❌ | ❌ |
/p/2 | ❌ | ❌ |
Notice how the empty path
record is active but not exact active differently from the other child /p/1/absolute-b
. All its aliases are active as well because they are aliases of an empty path
. If it was the other way around: the path
wasn’t empty but one of the aliases was an empty path
, then none of them would be active because the original path
takes precedence over aliases.
Named nested routes
If the url is resolved through the name
of the parent, in this case parent
, it will not include the empty path child route. This is important because they both resolve to the same url but when used in router-link
's to
prop, they would yield different results when it comes to being active and/or exact active. This is consistent with what they render being different and the rest of the active behavior.
E.g., given the routes from the previous example, if the current location is /parent/1
and, both the parent and child views are rendering, meaning we are effectively at { name: 'child' }
and not at { name: 'parent' }
, here is a similar table to the ones before but also including to
:
to 's value |
resolved url | active | exact active |
---|---|---|---|
{ name: 'parent', params: { id: '1' } } |
/parent/1 (parent) |
✅ | ❌ |
'/parent/1' |
/parent/1 (child) |
✅ | ✅ |
{ name: 'child', params: { id: '1' } } |
/parent/1 (child) |
✅ | ✅ |
'/p_1' |
/p_1 (child) |
✅ | ✅ |
'/parent/1/alias' |
/parent/1/alias (child) |
✅ | ✅ |
But if the current location is { name: 'parent' }
, it will still yield the same url, /parent/1
, but a different table:
to 's value |
resolved url | active | exact active |
---|---|---|---|
{ name: 'parent', params: { id: '1' } } |
/parent/1 (parent) |
✅ | ✅ |
'/parent/1' |
/parent/1 (child) |
❌ | ❌ |
{ name: 'child', params: { id: '1' } } |
/parent/1 (child) |
❌ | ❌ |
'/p_1' |
/p_1 (child) |
❌ | ❌ |
'/parent/1/alias' |
/parent/1/alias (child) |
❌ | ❌ |
Repeated params
With repeating params like
/articles/:id+
/articles/:id*
All params must match, with the same exact order for a link to be both, active and exact active.
exact
prop
Before these changes, exact
worked by matching the whole location. Its main purpose was to get around the /
caveat but it also checked query
and hash
.
Because of this, with the new active behavior, the exact
prop only purpose would be for a link to display router-link-active
only if router-link-exact-active
is also present. But this isn’t really useful anymore as we can directly target the element using the router-link-exact-active
class.
Because of this, I think the exact
prop can be removed from router-link
. This outdates https://github.com/vuejs/rfcs/pull/37 while still introducing the behavior of exact
for the path section of the loction like explained above.
Some users will probably have to change the class used in CSS from router-link-exact-active
to router-link-active
to adapt to this change.
Drawbacks
- This is not backwards compatible. It’s probably worth adding
exact-path
in Vue Router v3. - Users using the
exact
prop will have to rely on therouter-link-exact-active
or use theexact-active-class
prop. - The function
includesQuery
must be added by the user.
Alternatives
- Leaving the
exact
prop to only applyrouter-link-active
whenrouter-link-exact-active
as also applied. - Keep active behavior of doing an inclusive match of
query
andhash
instead of just relying on the params. - Changing active behavior to only apply to the
path
section of a route (this includes params) and ignorequery
andhash
.
Adoption strategy
- Add
exact-path
to mitigate existing problems in v3