feat(unplugin): routing support for inertia (#3845)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Romain Hamel
2025-04-14 10:47:26 +02:00
committed by GitHub
parent eea14155aa
commit d059efca25
8 changed files with 564 additions and 15 deletions

View File

@@ -0,0 +1,181 @@
<script lang="ts">
import type { ButtonHTMLAttributes } from 'vue'
import type { AppConfig } from '@nuxt/schema'
import type { InertiaLinkProps } from '@inertiajs/vue3'
import theme from '#build/ui/link'
import type { ComponentConfig } from '../../types/utils'
type Link = ComponentConfig<typeof theme, AppConfig, 'link'>
interface NuxtLinkProps extends Omit<InertiaLinkProps, 'href'> {
activeClass?: string
/**
* Route Location the link should navigate to when clicked on.
*/
to?: string // 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
/**
* Where to display the linked URL, as the name for a browsing context.
*/
target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null
ariaCurrentValue?: string
}
export interface LinkProps extends NuxtLinkProps {
/**
* The element or component this component should render as when not a link.
* @defaultValue 'button'
*/
as?: any
/**
* The type of the button when not a link.
* @defaultValue 'button'
*/
type?: ButtonHTMLAttributes['type']
disabled?: boolean
/** Force the link to be active independent of the current route. */
active?: boolean
/** Will only be active if the current route is an exact match. */
exact?: boolean
/** The class to apply when the link is inactive. */
inactiveClass?: string
custom?: boolean
/** When `true`, only styles from `class`, `activeClass`, and `inactiveClass` will be applied. */
raw?: boolean
class?: any
}
export interface LinkSlots {
default(props: { active: boolean }): any
}
</script>
<script setup lang="ts">
import { computed } from 'vue'
import { defu } from 'defu'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { usePage, Link as InertiaLink } from '@inertiajs/vue3'
import { hasProtocol } from 'ufo'
import { useAppConfig } from '#imports'
import { tv } from '../../utils/tv'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<LinkProps>(), {
as: 'button',
type: 'button',
active: undefined,
activeClass: '',
inactiveClass: ''
})
defineSlots<LinkSlots>()
const appConfig = useAppConfig() as Link['AppConfig']
const routerLinkProps = useForwardProps(reactiveOmit(props, 'as', 'type', 'disabled', 'active', 'exact', 'activeClass', 'inactiveClass', 'to', 'raw', 'class'))
const ui = computed(() => tv({
extend: tv(theme),
...defu({
variants: {
active: {
true: props.activeClass,
false: props.inactiveClass
}
}
}, appConfig.ui?.link || {})
}))
const isExternal = computed(() => {
if (!props.to) return false
return typeof props.to === 'string' && hasProtocol(props.to, { acceptRelative: true })
})
const linkClass = computed(() => {
const active = isActive.value
if (props.raw) {
return [props.class, active ? props.activeClass : props.inactiveClass]
}
return ui.value({ class: props.class, active, disabled: props.disabled })
})
const page = usePage()
const url = computed(() => props.to ?? props.href ?? '#')
const isActive = computed(() => props.active || (props.exact ? url.value === props.href : page?.url.startsWith(url.value)))
</script>
<template>
<template v-if="!isExternal">
<InertiaLink v-bind="routerLinkProps" :href="url" custom>
<template v-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
href: url,
active: isActive
}"
/>
</template>
<ULinkBase
v-else
v-bind="{
...$attrs,
as,
type,
disabled,
href: url,
active: isActive
}"
:class="linkClass"
>
<slot :active="isActive" />
</ULinkBase>
</InertiaLink>
</template>
<template v-else>
<template v-if="custom">
<slot
v-bind="{
...$attrs,
as,
type,
disabled,
href: to,
target: isExternal ? '_blank' : undefined,
active: isActive
}"
/>
</template>
<ULinkBase
v-else
v-bind="{
...$attrs,
as,
type,
disabled,
href: url,
target: isExternal ? '_blank' : undefined,
active: isActive
}"
:is-external="isExternal"
:class="linkClass"
>
<slot :active="isActive" />
</ULinkBase>
</template>
</template>

View File

@@ -0,0 +1,98 @@
import { ref, onScopeDispose } from 'vue'
import type { Ref, Plugin as VuePlugin } from 'vue'
import { createHooks } from 'hookable'
import appConfig from '#build/app.config'
import type { NuxtApp } from '#app'
import { useColorMode as useColorModeVueUse } from '@vueuse/core'
import { usePage } from '@inertiajs/vue3'
export { useHead } from '@unhead/vue'
export { defineShortcuts } from '../composables/defineShortcuts'
export { defineLocale } from '../composables/defineLocale'
export { useLocale } from '../composables/useLocale'
export const useRoute = () => {
const page = usePage()
return {
fullPath: page.url
}
}
export const useRouter = () => {
}
export const useColorMode = () => {
if (!appConfig.colorMode) {
return {
forced: true
}
}
const { store, system } = useColorModeVueUse()
return {
get preference() { return store.value === 'auto' ? 'system' : store.value },
set preference(value) { store.value = value === 'system' ? 'auto' : value },
get value() { return store.value === 'auto' ? system.value : store.value },
forced: false
}
}
export const useAppConfig = () => appConfig
export const useCookie = <T = string>(
_name: string,
_options: Record<string, any> = {}
) => {
const value = ref(null) as Ref<T>
return {
value,
get: () => value.value,
set: () => {},
update: () => {},
refresh: () => Promise.resolve(value.value),
remove: () => {}
}
}
const state: Record<string, any> = {}
export const useState = <T>(key: string, init: () => T): Ref<T> => {
if (state[key]) {
return state[key] as Ref<T>
}
const value = ref(init())
state[key] = value
return value as Ref<T>
}
const hooks = createHooks()
export function useNuxtApp() {
return {
isHydrating: true,
payload: { serverRendered: false },
hooks,
hook: hooks.hook
}
}
export function useRuntimeHook(name: string, fn: (...args: any[]) => void): void {
const nuxtApp = useNuxtApp()
const unregister = nuxtApp.hook(name, fn)
onScopeDispose(unregister)
}
export function defineNuxtPlugin(plugin: (nuxtApp: NuxtApp) => void) {
return {
install(app) {
app.runWithContext(() => plugin({ vueApp: app } as NuxtApp))
}
} satisfies VuePlugin
}

View File

@@ -93,8 +93,8 @@ import { isEqual, diff } from 'ohash/utils'
import { useForwardProps } from 'reka-ui'
import { reactiveOmit } from '@vueuse/core'
import { hasProtocol } from 'ufo'
import { useRoute, useAppConfig } from '#imports'
import { RouterLink } from 'vue-router'
import { useRoute, RouterLink } from 'vue-router'
import { useAppConfig } from '#imports'
import { tv } from '../../utils/tv'
defineOptions({ inheritAttrs: false })