- Start Date: 2019-04-29
- Target Major Version: Vue (2.x / 3.x) Vue Router (3.x / 4.x)
- Reference Issues: vuejs/vue-router#2611,
- Implementation PR: Implemented in both v3.x and v4.x
- Remove
tag
prop - Remove
event
prop - Stop automatically assigning click events to inner anchors
- Add a scoped-slot API
- Add a
custom
prop to fully customizerouter-link
's rendering
<router-link to="/">
<Icon>home</Icon> Home
</router-link>
Current implementation of Router Link has many limitations:
- Active state customization is not complete (#2611
- Cannot be integrated with custom components (#2021)
- click event cannot be prevented (through
@click.prevent
nor through adisabled
attribute #2098)
The idea of this RFC is to solve those issues by providing a scoped slot that allow app developers to easily extend Links based on their applications and to allow library authors to provide an even easier integration with Vue Router.
A simple use case would be slot with content (no nested anchors or buttons)
<router-link to="/">
<Icon>home</Icon> Home
</router-link>
This implementation would:
- generate an anchor (
a
) element and apply the corresponding properties:href
with the destinationclass
withrouter-link-active
and/orrouter-link-exact-active
(can be changed through prop or global option)- click listener to trigger navigation through
router.push
orrouter.replace
with aevent.preventDefault
(except when the link is clicked using a modifier like ⌘ or Ctrl)
- Put anything passed as the children of the anchor
- Pass down any attributes that aren't props to the
a
element
Breaking changes:
- no longer accept a
tag
prop -> use the scoped slot instead (see the point below) - no longer accepts
event
-> use the scoped slot instead - no longer works as a wrapper automatically looking for the first
a
inside -> use the scoped slot instead
A scoped slot would get access to every bit of information needed to provide a custom integration and allows applying the active classes, click listener, links, etc at any level. This would allow a better integration with UI frameworks like Bootstrap (https://getbootstrap.com/docs/4.3/components/navbar/). The idea would be to create a Vue component to avoid the boilerplate like bootstrap-vue does (https://bootstrap-vue.js.org/docs/components/navbar/#navbar)
<router-link to="/" custom v-slot="{ href, navigate, isActive }">
<li :class="{ 'active': isActive }">
<a :href="href" @click="navigate">
<Icon>home</Icon><span class="xs-hidden">Home</span>
</a>
</li>
</router-link>
The custom
prop is necessary to take full control over router-link
's rendering: not rendering a wrapping a
element.
Why is a custom
prop necessary: in Vue 3, scoped slots and regular slots cannot be differentiated from each other, which means vue router is unable to make the difference between these 3 cases:
<router-link to="/" v-slot="{ href, navigate, isActive }"></router-link>
<router-link to="/" v-slot></router-link>
<router-link to="/">Some Link</router-link>
In all three cases we need to render the slot content but router-link
needs to know if it has to render a wrapping a
element. In Vue 2, we are able to do so by checking $scopedSlots
but in Vue 3, only slots
exists. This means that the behavior is slightly different in Vue Router v3 and Vue Router v4:
- In v3, the
custom
prop is required (see Adoption strategy) alongsidev-slot
.router-link
will not wrap the slot content with ana
element. - In v4, the
custom
prop is not required alongsidev-slot
. It controls whetherrouter-link
should wrap its slot content with ana
element or not:<router-link to="/" v-slot="{ href }"> <router-link to="/" custom v-slot="{ href, navigate }"> <a :href="href" @click="navigate">{{ href }}</a> </router-link> <!-- both render the same --> <a href="/">/</a> <a href="/">/</a>
The slot should provide values that are computed inside router-link
:
href
: resolved relative url to be added to an anchor tag (contains the base if provided while route.fullPath doesn't)route
: resolved normalized route location from theto
(same shape as$route
)navigate
: function to trigger navigation (usually attached to a click). Also callspreventDefault
if the click is directly pressed.isActive
: true wheneverrouter-link-active
is applied. Can be modified byexact
propisExactActive
: true wheneverrouter-link-exact-active
is aplied. Can be modified byexact
prop.
The tag
prop can be replaced which a scoped slot and make the code clearer while not being exposed to any caveat. Its removal will also lighten the vue-router library.
<router-link to="/" tag="button">
<Icon>home</Icon><span class="xs-hidden">Home</span>
</router-link>
is equivalent to
<router-link to="/" custom v-slot="{ navigate, isActive, isExactActive }">
<button role="link" @click="navigate" :class="{ active: isActive, 'exact-active': isExactActive }">
<Icon>home</Icon><span class="xs-hidden">Home</span>
</button>
</router-link>
(see above for explanation about the attributes passed to the scoped-slot)
- Whereas it's possible to keep existing behaviour working and only expose a new behaviour with scoped slots, it will still prevent us from fixing existing issues with current implementation. That's why there are some breaking changes, to make things more consistent.
- No access to the default
router-link
classes likerouter-link-active
androuter-link-exact-active
.
-
Keeping
event
prop for convienience -
Use a different named slot instead of a
prop
:<router-link #custom="{ href }"> <a :href="href"></a> </router-link> <router-link v-slot:custom="{ href }"> <a :href="href"></a> </router-link> <router-link custom v-slot="{ href }"> <a :href="href"></a> </router-link>
The adoption strategy in this case would be similar but the warning would tell the user to use a different slot instead of a prop named
custom
-
Create a new component like
router-link-custom
to differentiate the behavior. This solution is however heavier (in terms of size) than a prop or a different named slot. It is also less suitable than a prop because we are only changing a behavior of the component. The difference between the two components woud be too small to justify a whole new component.
- Document new slot behaviour based on examples
- Deprecate
tag
andevent
with a message in v3 and link to documentation, then remove in v4 - In v3, if no
custom
prop is provided when using a scoped slot, warn the user to use thecustom
prop