mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-19 01:45:53 +00:00
refactor(nuxt): improve NuxtLink
types (#25599)
This commit is contained in:
parent
5d51df5816
commit
365eaba229
@ -25,6 +25,23 @@ In this example, we use `<NuxtLink>` component to link to another page of the ap
|
|||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Handling 404s
|
||||||
|
|
||||||
|
When using `<NuxtLink>` for `/public` directory files or when pointing to a different app on the same domain, you should use the `external` prop.
|
||||||
|
|
||||||
|
Using `external` forces the link to be rendered as an `a` tag instead of a Vue Router `RouterLink`.
|
||||||
|
|
||||||
|
```vue [pages/index.vue]
|
||||||
|
<template>
|
||||||
|
<NuxtLink to="/the-important-report.pdf" external>
|
||||||
|
Download Report
|
||||||
|
</NuxtLink>
|
||||||
|
<!-- <a href="/the-important-report.pdf"></a> -->
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
The external logic is applied by default when using absolute URLs and when providing a `target` prop.
|
||||||
|
|
||||||
## External Routing
|
## External Routing
|
||||||
|
|
||||||
In this example, we use `<NuxtLink>` component to link to a website.
|
In this example, we use `<NuxtLink>` component to link to a website.
|
||||||
@ -68,19 +85,30 @@ In this example, we use `<NuxtLink>` with `target`, `rel`, and `noRel` props.
|
|||||||
|
|
||||||
## Props
|
## Props
|
||||||
|
|
||||||
|
### RouterLink
|
||||||
|
|
||||||
|
When not using `external`, `<NuxtLink>` supports all Vue Router's [`RouterLink` props](https://router.vuejs.org/api/interfaces/RouterLinkProps.html)
|
||||||
|
|
||||||
- `to`: Any URL or a [route location object](https://router.vuejs.org/api/interfaces/RouteLocation.html) from Vue Router
|
- `to`: Any URL or a [route location object](https://router.vuejs.org/api/interfaces/RouteLocation.html) from Vue Router
|
||||||
- `href`: An alias for `to`. If used with `to`, `href` will be ignored
|
- `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/interfaces/RouterLinkProps.html#Properties-custom)
|
||||||
- `target`: A `target` attribute value to apply on the link
|
|
||||||
- `rel`: A `rel` attribute value to apply on the link. Defaults to `"noopener noreferrer"` for external links.
|
|
||||||
- `noRel`: If set to `true`, no `rel` attribute will be added to the link
|
|
||||||
- `activeClass`: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-activeClass) on internal links. Defaults to Vue Router's default (`"router-link-active"`)
|
|
||||||
- `exactActiveClass`: A class to apply on exact active links. Works the same as [Vue Router's `exact-active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-exactActiveClass) on internal links. Defaults to Vue Router's default `"router-link-exact-active"`)
|
- `exactActiveClass`: A class to apply on exact active links. Works the same as [Vue Router's `exact-active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-exactActiveClass) on internal links. Defaults to Vue Router's default `"router-link-exact-active"`)
|
||||||
- `replace`: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/interfaces/RouteLocationOptions.html#Properties-replace) on internal links
|
- `replace`: Works the same as [Vue Router's `replace` prop](https://router.vuejs.org/api/interfaces/RouteLocationOptions.html#Properties-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/interfaces/RouterLinkProps.html#Properties-ariaCurrentValue) 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/interfaces/RouterLinkProps.html#Properties-ariaCurrentValue) on internal links
|
||||||
- `external`: Forces the link to be considered as external (`true`) or internal (`false`). This is helpful to handle edge-cases
|
- `activeClass`: A class to apply on active links. Works the same as [Vue Router's `active-class` prop](https://router.vuejs.org/api/interfaces/RouterLinkProps.html#Properties-activeClass) on internal links. Defaults to Vue Router's default (`"router-link-active"`)
|
||||||
- `prefetch` and **noPrefetch**: Whether to enable prefetching assets for links that enter the view port.
|
|
||||||
|
### NuxtLink
|
||||||
|
|
||||||
|
- `href`: An alias for `to`. If used with `to`, `href` will be ignored
|
||||||
|
- `noRel`: If set to `true`, no `rel` attribute will be added to the link
|
||||||
|
- `external`: Forces the link to be rendered as an `a` tag instead of a Vue Router `RouterLink`.
|
||||||
|
- `prefetch`: When enabled will prefetch middleware, layouts and payloads (when using [payloadExtraction](/docs/api/nuxt-config#crossoriginprefetch)) of links in the viewport. Used by the experimental [crossOriginPrefetch](/docs/api/nuxt-config#crossoriginprefetch) config.
|
||||||
|
- `noPrefetch`: Disables prefetching.
|
||||||
- `prefetchedClass`: A class to apply to links that have been prefetched.
|
- `prefetchedClass`: A class to apply to links that have been prefetched.
|
||||||
- `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/interfaces/RouterLinkProps.html#Properties-custom)
|
|
||||||
|
### Anchor
|
||||||
|
|
||||||
|
- `target`: A `target` attribute value to apply on the link
|
||||||
|
- `rel`: A `rel` attribute value to apply on the link. Defaults to `"noopener noreferrer"` for external links.
|
||||||
|
|
||||||
::callout
|
::callout
|
||||||
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.
|
||||||
@ -102,17 +130,18 @@ You can then use `<MyNuxtLink />` component as usual with your new defaults.
|
|||||||
### `defineNuxtLink` Signature
|
### `defineNuxtLink` Signature
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
defineNuxtLink({
|
interface NuxtLinkOptions {
|
||||||
componentName?: string;
|
componentName?: string;
|
||||||
externalRelAttribute?: string;
|
externalRelAttribute?: string;
|
||||||
activeClass?: string;
|
activeClass?: string;
|
||||||
exactActiveClass?: string;
|
exactActiveClass?: string;
|
||||||
prefetchedClass?: string;
|
prefetchedClass?: string;
|
||||||
trailingSlash?: 'append' | 'remove'
|
trailingSlash?: 'append' | 'remove'
|
||||||
}) => Component
|
}
|
||||||
|
function defineNuxtLink(options: NuxtLinkOptions): Component {}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `componentName`: A name for the defined `<NuxtLink>` component.
|
- `componentName`: A name for the component. Default is `NuxtLink`.
|
||||||
- `externalRelAttribute`: A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable
|
- `externalRelAttribute`: A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable
|
||||||
- `activeClass`: A default class to apply on active links. Works the same as [Vue Router's `linkActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkActiveClass). Defaults to Vue Router's default (`"router-link-active"`)
|
- `activeClass`: A default class to apply on active links. Works the same as [Vue Router's `linkActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkActiveClass). Defaults to Vue Router's default (`"router-link-active"`)
|
||||||
- `exactActiveClass`: A default class to apply on exact active links. Works the same as [Vue Router's `linkExactActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkExactActiveClass). Defaults to Vue Router's default (`"router-link-exact-active"`)
|
- `exactActiveClass`: A default class to apply on exact active links. Works the same as [Vue Router's `linkExactActiveClass` option](https://router.vuejs.org/api/interfaces/RouterOptions.html#Properties-linkExactActiveClass). Defaults to Vue Router's default (`"router-link-exact-active"`)
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
import type { ComputedRef, DefineComponent, InjectionKey, PropType } from 'vue'
|
import type {
|
||||||
|
AllowedComponentProps,
|
||||||
|
AnchorHTMLAttributes,
|
||||||
|
ComputedRef,
|
||||||
|
DefineComponent,
|
||||||
|
InjectionKey, PropType,
|
||||||
|
VNodeProps
|
||||||
|
} from 'vue'
|
||||||
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
|
import { computed, defineComponent, h, inject, onBeforeUnmount, onMounted, provide, ref, resolveComponent } from 'vue'
|
||||||
import type { RouteLocation, RouteLocationRaw } from '#vue-router'
|
import type { RouteLocation, RouteLocationRaw, Router, RouterLinkProps } from '#vue-router'
|
||||||
import { hasProtocol, joinURL, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'
|
import { hasProtocol, joinURL, parseQuery, parseURL, withTrailingSlash, withoutTrailingSlash } from 'ufo'
|
||||||
|
|
||||||
import { preloadRouteComponents } from '../composables/preload'
|
import { preloadRouteComponents } from '../composables/preload'
|
||||||
import { onNuxtReady } from '../composables/ready'
|
import { onNuxtReady } from '../composables/ready'
|
||||||
import { navigateTo, useRouter } from '../composables/router'
|
import { navigateTo, useRouter } from '../composables/router'
|
||||||
@ -17,52 +23,94 @@ const firstNonUndefined = <T> (...args: (T | undefined)[]) => args.find(arg => a
|
|||||||
const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer'
|
const DEFAULT_EXTERNAL_REL_ATTRIBUTE = 'noopener noreferrer'
|
||||||
const NuxtLinkDevKeySymbol: InjectionKey<boolean> = Symbol('nuxt-link-dev-key')
|
const NuxtLinkDevKeySymbol: InjectionKey<boolean> = Symbol('nuxt-link-dev-key')
|
||||||
|
|
||||||
export type NuxtLinkOptions = {
|
/**
|
||||||
|
* Create a NuxtLink component with given options as defaults.
|
||||||
|
* @see https://nuxt.com/docs/api/components/nuxt-link
|
||||||
|
*/
|
||||||
|
export interface NuxtLinkOptions extends
|
||||||
|
Pick<RouterLinkProps, 'activeClass' | 'exactActiveClass'>,
|
||||||
|
Pick<NuxtLinkProps, 'prefetchedClass'> {
|
||||||
|
/**
|
||||||
|
* The name of the component.
|
||||||
|
* @default "NuxtLink"
|
||||||
|
*/
|
||||||
componentName?: string
|
componentName?: string
|
||||||
|
/**
|
||||||
|
* A default `rel` attribute value applied on external links. Defaults to `"noopener noreferrer"`. Set it to `""` to disable.
|
||||||
|
*/
|
||||||
externalRelAttribute?: string | null
|
externalRelAttribute?: string | null
|
||||||
activeClass?: string
|
/**
|
||||||
exactActiveClass?: string
|
* An option to either add or remove trailing slashes in the `href`.
|
||||||
prefetchedClass?: string
|
* If unset or not matching the valid values `append` or `remove`, it will be ignored.
|
||||||
|
*/
|
||||||
trailingSlash?: 'append' | 'remove'
|
trailingSlash?: 'append' | 'remove'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NuxtLinkProps = {
|
/**
|
||||||
// Routing
|
* <NuxtLink> is a drop-in replacement for both Vue Router's <RouterLink> component and HTML's <a> tag.
|
||||||
to?: RouteLocationRaw
|
* @see https://nuxt.com/docs/api/components/nuxt-link
|
||||||
href?: RouteLocationRaw
|
*/
|
||||||
|
export interface NuxtLinkProps extends Omit<RouterLinkProps, 'to'> {
|
||||||
|
/**
|
||||||
|
* Route Location the link should navigate to when clicked on.
|
||||||
|
*/
|
||||||
|
to?: RouteLocationRaw // need to manually type to avoid breaking typedPages
|
||||||
|
/**
|
||||||
|
* An alias for `to`. If used with `to`, `href` will be ignored
|
||||||
|
*/
|
||||||
|
href?: NuxtLinkProps['to']
|
||||||
|
/**
|
||||||
|
* Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
|
||||||
|
*/
|
||||||
external?: boolean
|
external?: boolean
|
||||||
replace?: boolean
|
/**
|
||||||
custom?: boolean
|
* Where to display the linked URL, as the name for a browsing context.
|
||||||
|
*/
|
||||||
// Attributes
|
|
||||||
target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null
|
target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null
|
||||||
rel?: string | null
|
/**
|
||||||
|
* A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
|
||||||
|
*/
|
||||||
|
rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null
|
||||||
|
/**
|
||||||
|
* If set to true, no rel attribute will be added to the link
|
||||||
|
*/
|
||||||
noRel?: boolean
|
noRel?: boolean
|
||||||
|
/**
|
||||||
|
* A class to apply to links that have been prefetched.
|
||||||
|
*/
|
||||||
|
prefetchedClass?: string
|
||||||
|
/**
|
||||||
|
* When enabled will prefetch middleware, layouts and payloads of links in the viewport.
|
||||||
|
*/
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
|
/**
|
||||||
|
* Escape hatch to disable `prefetch` attribute.
|
||||||
|
*/
|
||||||
noPrefetch?: boolean
|
noPrefetch?: boolean
|
||||||
|
|
||||||
// Styling
|
|
||||||
activeClass?: string
|
|
||||||
exactActiveClass?: string
|
|
||||||
|
|
||||||
// Vue Router's `<RouterLink>` additional props
|
|
||||||
ariaCurrentValue?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@__NO_SIDE_EFFECTS__*/
|
/*@__NO_SIDE_EFFECTS__*/
|
||||||
export function defineNuxtLink (options: NuxtLinkOptions) {
|
export function defineNuxtLink (options: NuxtLinkOptions) {
|
||||||
const componentName = options.componentName || 'NuxtLink'
|
const componentName = options.componentName || 'NuxtLink'
|
||||||
|
|
||||||
const checkPropConflicts = (props: NuxtLinkProps, main: keyof NuxtLinkProps, sub: keyof NuxtLinkProps): void => {
|
function checkPropConflicts (props: NuxtLinkProps, main: keyof NuxtLinkProps, sub: keyof NuxtLinkProps): void {
|
||||||
if (import.meta.dev && props[main] !== undefined && props[sub] !== undefined) {
|
if (import.meta.dev && props[main] !== undefined && props[sub] !== undefined) {
|
||||||
console.warn(`[${componentName}] \`${main}\` and \`${sub}\` cannot be used together. \`${sub}\` will be ignored.`)
|
console.warn(`[${componentName}] \`${main}\` and \`${sub}\` cannot be used together. \`${sub}\` will be ignored.`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const resolveTrailingSlashBehavior = (
|
|
||||||
|
function resolveTrailingSlashBehavior (
|
||||||
|
to: string,
|
||||||
|
resolve: Router['resolve']
|
||||||
|
): string
|
||||||
|
function resolveTrailingSlashBehavior (
|
||||||
to: RouteLocationRaw,
|
to: RouteLocationRaw,
|
||||||
resolve: (to: RouteLocationRaw) => RouteLocation & { href?: string }
|
resolve: Router['resolve']
|
||||||
): RouteLocationRaw | RouteLocation => {
|
): Omit<RouteLocationRaw, string>
|
||||||
|
function resolveTrailingSlashBehavior (
|
||||||
|
to: RouteLocationRaw,
|
||||||
|
resolve: Router['resolve']
|
||||||
|
): RouteLocationRaw | RouteLocation {
|
||||||
if (!to || (options.trailingSlash !== 'append' && options.trailingSlash !== 'remove')) {
|
if (!to || (options.trailingSlash !== 'append' && options.trailingSlash !== 'remove')) {
|
||||||
return to
|
return to
|
||||||
}
|
}
|
||||||
@ -97,72 +145,72 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
|||||||
|
|
||||||
// Attributes
|
// Attributes
|
||||||
target: {
|
target: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<NuxtLinkProps['target']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
rel: {
|
rel: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<NuxtLinkProps['rel']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
noRel: {
|
noRel: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<NuxtLinkProps['noRel']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Prefetching
|
// Prefetching
|
||||||
prefetch: {
|
prefetch: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<NuxtLinkProps['prefetch']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
noPrefetch: {
|
noPrefetch: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<NuxtLinkProps['noPrefetch']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Styling
|
// Styling
|
||||||
activeClass: {
|
activeClass: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<NuxtLinkProps['activeClass']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
exactActiveClass: {
|
exactActiveClass: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<NuxtLinkProps['exactActiveClass']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
prefetchedClass: {
|
prefetchedClass: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<NuxtLinkProps['prefetchedClass']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Vue Router's `<RouterLink>` additional props
|
// Vue Router's `<RouterLink>` additional props
|
||||||
replace: {
|
replace: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<NuxtLinkProps['replace']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
ariaCurrentValue: {
|
ariaCurrentValue: {
|
||||||
type: String as PropType<string>,
|
type: String as PropType<NuxtLinkProps['ariaCurrentValue']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Edge cases handling
|
// Edge cases handling
|
||||||
external: {
|
external: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<NuxtLinkProps['external']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
|
|
||||||
// Slot API
|
// Slot API
|
||||||
custom: {
|
custom: {
|
||||||
type: Boolean as PropType<boolean>,
|
type: Boolean as PropType<NuxtLinkProps['custom']>,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
@ -254,7 +302,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (!isExternal.value) {
|
if (!isExternal.value) {
|
||||||
const routerLinkProps: Record<string, any> = {
|
const routerLinkProps: RouterLinkProps & VNodeProps & AllowedComponentProps & AnchorHTMLAttributes = {
|
||||||
ref: elRef,
|
ref: elRef,
|
||||||
to: to.value,
|
to: to.value,
|
||||||
activeClass: props.activeClass || options.activeClass,
|
activeClass: props.activeClass || options.activeClass,
|
||||||
@ -270,7 +318,7 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
|||||||
if (prefetched.value) {
|
if (prefetched.value) {
|
||||||
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
routerLinkProps.class = props.prefetchedClass || options.prefetchedClass
|
||||||
}
|
}
|
||||||
routerLinkProps.rel = props.rel
|
routerLinkProps.rel = props.rel || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal link
|
// Internal link
|
||||||
@ -319,14 +367,13 @@ export function defineNuxtLink (options: NuxtLinkOptions) {
|
|||||||
fullPath: url.pathname,
|
fullPath: url.pathname,
|
||||||
get query () { return parseQuery(url.search) },
|
get query () { return parseQuery(url.search) },
|
||||||
hash: url.hash,
|
hash: url.hash,
|
||||||
// stub properties for compat with vue-router
|
|
||||||
params: {},
|
params: {},
|
||||||
name: undefined,
|
name: undefined,
|
||||||
matched: [],
|
matched: [],
|
||||||
redirectedFrom: undefined,
|
redirectedFrom: undefined,
|
||||||
meta: {},
|
meta: {},
|
||||||
href
|
href
|
||||||
}
|
} satisfies RouteLocation & { href: string }
|
||||||
},
|
},
|
||||||
rel,
|
rel,
|
||||||
target,
|
target,
|
||||||
|
Loading…
Reference in New Issue
Block a user