feat(nuxt3): support custom prop for <nuxt-link> (#4249)

Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: pooya parsa <pyapar@gmail.com>
This commit is contained in:
Francisco Buceta 2022-07-07 19:28:23 +02:00 committed by GitHub
parent c88e1716ab
commit 38e0fa6e1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 72 additions and 20 deletions

View File

@ -81,6 +81,7 @@ In this example, we use `<NuxtLink>` with `target`, `rel`, and `noRel` props.
- **replace**: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/#replace) on internal links - **replace**: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/#replace) on internal links
- **ariaCurrentValue**: An `aria-current` attribute value to apply on exact active links. Works the same as [Vue Router's `aria-current-value` prop](https://router.vuejs.org/api/#aria-current-value) on internal links - **ariaCurrentValue**: An `aria-current` attribute value to apply on exact active links. Works the same as [Vue Router's `aria-current-value` prop](https://router.vuejs.org/api/#aria-current-value) on internal links
- **external**: Forces the link to be considered as external (`true`) or internal (`false`). This is helpful to handle edge-cases - **external**: Forces the link to be considered as external (`true`) or internal (`false`). This is helpful to handle edge-cases
- **custom**: Whether `<NuxtLink>` should wrap its content in an `<a>` element. It allows taking full control of how a link is rendered and how navigation works when it is clicked. Works the same as [Vue Router's `custom` prop](https://router.vuejs.org/api/#custom)
::alert{icon=👉} ::alert{icon=👉}
Defaults can be overwritten, see [overwriting defaults](#overwriting-defaults) if you want to change them. Defaults can be overwritten, see [overwriting defaults](#overwriting-defaults) if you want to change them.

View File

@ -3,9 +3,17 @@
<NuxtLink to="/about"> <NuxtLink to="/about">
About page About page
</NuxtLink> </NuxtLink>
<NuxtLink v-slot="{ navigate }" to="/about" custom>
<button @click="navigate">
Custom about page
</button>
</NuxtLink>
<NuxtLink to="https://nuxtjs.org"> <NuxtLink to="https://nuxtjs.org">
Nuxt website Nuxt website
</NuxtLink> </NuxtLink>
<NuxtLink v-slot="{ href, target }" to="https://nuxtjs.org" custom>
<a :href="href" :target="target">Go to {{ href }}</a>
</NuxtLink>
<NuxtLink to="https://twitter.com/nuxt_js" target="_blank"> <NuxtLink to="https://twitter.com/nuxt_js" target="_blank">
Nuxt Twitter with a blank target Nuxt Twitter with a blank target
</NuxtLink> </NuxtLink>

View File

@ -23,6 +23,13 @@ const timer = useState('timer', () => 0)
<NuxtLink to="/redirect" class="n-link-base"> <NuxtLink to="/redirect" class="n-link-base">
Redirect Redirect
</NuxtLink> </NuxtLink>
<NuxtLink custom to="/redirect">
<template #default="{ href, navigate }">
<button @click="navigate">
Custom: {{ href }}
</button>
</template>
</NuxtLink>
</nav> </nav>
</template> </template>

View File

@ -2,37 +2,38 @@ import { defineComponent, h, resolveComponent, PropType, computed, DefineCompone
import { RouteLocationRaw, Router } from 'vue-router' import { RouteLocationRaw, Router } from 'vue-router'
import { hasProtocol } from 'ufo' import { hasProtocol } from 'ufo'
import { useRouter } from '#app' import { navigateTo, useRouter } from '#app'
const firstNonUndefined = <T>(...args: T[]): T => args.find(arg => arg !== undefined) const firstNonUndefined = <T>(...args: T[]): T => args.find(arg => arg !== undefined)
const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer' const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer'
export type NuxtLinkOptions = { export type NuxtLinkOptions = {
componentName?: string; componentName?: string
externalRelAttribute?: string | null; externalRelAttribute?: string | null
activeClass?: string; activeClass?: string
exactActiveClass?: string; exactActiveClass?: string
} }
export type NuxtLinkProps = { export type NuxtLinkProps = {
// Routing // Routing
to?: string | RouteLocationRaw; to?: string | RouteLocationRaw
href?: string | RouteLocationRaw; href?: string | RouteLocationRaw
external?: boolean; external?: boolean
replace?: boolean
custom?: boolean
// Attributes // Attributes
target?: string; target?: string
rel?: string; rel?: string
noRel?: boolean; noRel?: boolean
// Styling // Styling
activeClass?: string; activeClass?: string
exactActiveClass?: string; exactActiveClass?: string
// Vue Router's `<RouterLink>` additional props // Vue Router's `<RouterLink>` additional props
replace?: boolean; ariaCurrentValue?: string
ariaCurrentValue?: string;
}; };
export function defineNuxtLink (options: NuxtLinkOptions) { export function defineNuxtLink (options: NuxtLinkOptions) {
@ -154,9 +155,9 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
activeClass: props.activeClass || options.activeClass, activeClass: props.activeClass || options.activeClass,
exactActiveClass: props.exactActiveClass || options.exactActiveClass, exactActiveClass: props.exactActiveClass || options.exactActiveClass,
replace: props.replace, replace: props.replace,
ariaCurrentValue: props.ariaCurrentValue ariaCurrentValue: props.ariaCurrentValue,
custom: props.custom
}, },
// TODO: Slot API
slots.default slots.default
) )
} }
@ -175,6 +176,22 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
// converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`) // converts `""` to `null` to prevent the attribute from being added as empty (`rel=""`)
: firstNonUndefined<string | null>(props.rel, options.externalRelAttribute, href ? DEFAULT_EXTERNAL_REL_ATTRIBUTE : '') || null : firstNonUndefined<string | null>(props.rel, options.externalRelAttribute, href ? DEFAULT_EXTERNAL_REL_ATTRIBUTE : '') || null
const navigate = () => navigateTo(href, { replace: props.replace })
// https://router.vuejs.org/api/#custom
if (props.custom) {
if (!slots.default) { return null }
return slots.default({
href,
navigate,
route: router.resolve(href),
rel,
target,
isActive: false,
isExactActive: false
})
}
return h('a', { href, rel, target }, slots.default?.()) return h('a', { href, rel, target }, slots.default?.())
} }
} }

View File

@ -179,8 +179,24 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
nuxtApp.vueApp.component('RouterLink', { nuxtApp.vueApp.component('RouterLink', {
functional: true, functional: true,
props: { to: String }, props: {
setup: (props, { slots }) => () => h('a', { href: props.to, onClick: (e) => { e.preventDefault(); router.push(props.to) } }, slots) to: String,
custom: Boolean,
replace: Boolean,
// Not implemented
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: String
},
setup: (props, { slots }) => {
const navigate = () => handleNavigation(props.to, props.replace)
return () => {
const route = router.resolve(props.to)
return props.custom
? slots.default?.({ href: props.to, navigate, route })
: h('a', { href: props.to, onClick: (e) => { e.preventDefault(); return navigate() } }, slots)
}
}
}) })
if (process.client) { if (process.client) {

View File

@ -81,7 +81,10 @@ export default defineNuxtModule({
} }
nuxt.hook('autoImports:extend', (autoImports) => { nuxt.hook('autoImports:extend', (autoImports) => {
autoImports.push({ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') }) autoImports.push(
{ name: 'definePageMeta', as: 'definePageMeta', from: resolve(runtimeDir, 'composables') },
{ name: 'useLink', as: 'useLink', from: 'vue-router' }
)
}) })
// Extract macros from pages // Extract macros from pages