recorded decision · public · no signup
Summary
What was chosen
- The `tag` prop was removed from `router-link`.adr ↗
- The `event` prop was removed from `router-link`.adr ↗
- `router-link` no longer automatically assigns click events to inner anchors.adr ↗
- A scoped-slot API was added to `router-link` for custom rendering.adr ↗
- A `custom` prop was added to `router-link` to fully customize its rendering behavior.adr ↗
What was ruled out· 1
- Creating a separate `router-link-custom` component was rejected due to its heavier implementation and small behavioral difference.adr ↗
Constraints
Consequences
The recorded why
- Start Date: 2019-04-29
- Target Major Version: Vue (2.x / 3.x) Vue Router (3.x / 4.x)
- Reference Issues: https://github.com/vuejs/vue-router/issues/2611,
- Implementation PR: Implemented in both v3.x and v4.x
Summary
- Remove
tagprop - Remove
eventprop - Stop automatically assigning click events to inner anchors
- Add a scoped-slot API
- Add a
customprop to fully customizerouter-link's rendering
Basic example
<router-link to="/">
<Icon>home</Icon> Home
</router-link>
Motivation
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.preventnor through adisabledattribute #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.
Detailed design
§ Slot with content
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:hrefwith the destinationclasswithrouter-link-activeand/orrouter-link-exact-active(can be changed through prop or global option)- click listener to trigger navigation through
router.pushorrouter.replacewith aevent.preventDefault(except when the link is clicked using a modifier like <kbd>⌘</kbd> or <kbd>Ctrl</kbd>)
- Put anything passed as the children of the anchor
- Pass down any attributes that aren't props to the
aelement
Breaking changes:
- no longer accept a
tagprop -> 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
ainside -> use the scoped slot instead
§ Scoped slot
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
customprop is required (see Adoption strategy) alongsidev-slot.router-linkwill not wrap the slot content with anaelement. - In v4, the
customprop is not required alongsidev-slot. It controls whetherrouter-linkshould wrap its slot content with anaelement 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>
Accessible variables
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 callspreventDefaultif the click is directly pressed.isActive: true wheneverrouter-link-activeis applied. Can be modified byexactpropisExactActive: true wheneverrouter-link-exact-activeis aplied. Can be modified byexactprop.
§ The removal of the tag 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)
Drawbacks
- 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-linkclasses likerouter-link-activeandrouter-link-exact-active.
Alternatives
-
Keeping
eventprop 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-customto 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.
Adoption strategy
- Document new slot behaviour based on examples
- Deprecate
tagandeventwith a message in v3 and link to documentation, then remove in v4 - In v3, if no
customprop is provided when using a scoped slot, warn the user to use thecustomprop