Skip to content

Commit 0f7a7b9

Browse files
committed
feat: support disabling router integrations
1 parent ea43070 commit 0f7a7b9

File tree

8 files changed

+369
-1
lines changed

8 files changed

+369
-1
lines changed

docs/content/1.getting-started/2.installation/2.vue.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,29 @@ export default defineConfig({
403403
When using this option, `vue-router` is not required as Inertia.js provides its own routing system. The components that would normally use `RouterLink` will automatically use Inertia's `InertiaLink` component instead.
404404
::
405405

406+
### `router`
407+
408+
Use the `router` option to disable router integration entirely.
409+
410+
```ts [vite.config.ts]
411+
import { defineConfig } from 'vite'
412+
import vue from '@vitejs/plugin-vue'
413+
import ui from '@nuxt/ui/vite'
414+
415+
export default defineConfig({
416+
plugins: [
417+
vue(),
418+
ui({
419+
router: false
420+
})
421+
]
422+
})
423+
```
424+
425+
::note
426+
When this option is disabled, components that would normally integrate with routing (like `ULink`) will behave as regular anchor tags for external links or plain elements for internal navigation. This is useful for single-page applications without routing or when using custom routing solutions.
427+
::
428+
406429
## Continuous Releases
407430

408431
Nuxt UI uses [pkg.pr.new](https://github.com/stackblitz-labs/pkg.pr.new) for continuous preview releases, providing developers with instant access to the latest features and bug fixes without waiting for official releases.

src/defaults.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const defaultOptions = {
1919
prefix: 'U',
2020
fonts: true,
2121
colorMode: true,
22+
router: true,
2223
theme: {
2324
colors: undefined,
2425
transitions: true

src/module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export interface ModuleOptions {
3131
*/
3232
colorMode?: boolean
3333

34+
/**
35+
* Enable or disable router integration
36+
* @defaultValue `true`
37+
*/
38+
router?: boolean
39+
3440
/**
3541
* Customize how the theme is generated
3642
* @link https://ui.nuxt.com/getting-started/theme

src/plugins/components.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
2121
const inertiaOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'inertia/components') })
2222
const inertiaOverrideNames = new Set(inertiaOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
2323

24+
const minimalOverrides = globSync('**/*.vue', { cwd: join(runtimeDir, 'minimal/components') })
25+
const minimalOverrideNames = new Set(minimalOverrides.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
26+
2427
const pluginOptions = defu(options.components, <ComponentsOptions>{
2528
dts: options.dts ?? true,
2629
exclude: [
@@ -30,6 +33,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
3033
],
3134
resolvers: [
3235
(componentName) => {
36+
if (options.router === false && minimalOverrideNames.has(componentName)) {
37+
return { name: 'default', from: join(runtimeDir, 'minimal/components', `${componentName.slice(options.prefix.length)}.vue`) }
38+
}
3339
if (options.inertia && inertiaOverrideNames.has(componentName)) {
3440
return { name: 'default', from: join(runtimeDir, 'inertia/components', `${componentName.slice(options.prefix.length)}.vue`) }
3541
}
@@ -63,6 +69,9 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
6369
}
6470

6571
const filename = id.match(/([^/]+)\.vue$/)?.[1]
72+
if (filename && options.router === false && minimalOverrideNames.has(`${options.prefix}${filename}`)) {
73+
return join(runtimeDir, 'minimal/components', `${filename}.vue`)
74+
}
6675
if (filename && options.inertia && inertiaOverrideNames.has(`${options.prefix}${filename}`)) {
6776
return join(runtimeDir, 'inertia/components', `${filename}.vue`)
6877
}

src/plugins/nuxt-environment.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,18 @@ import type { NuxtUIOptions } from '../unplugin'
1010
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
1111
*/
1212
export default function NuxtEnvironmentPlugin(options: NuxtUIOptions) {
13-
const stubPath = resolvePathSync(options.inertia ? '../runtime/inertia/stubs' : '../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
13+
let stubPath: string
14+
15+
if (options.router === false) {
16+
// Router disabled - use minimal stubs
17+
stubPath = resolvePathSync('../runtime/minimal/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
18+
} else if (options.inertia) {
19+
// Inertia mode
20+
stubPath = resolvePathSync('../runtime/inertia/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
21+
} else {
22+
// Default Vue mode
23+
stubPath = resolvePathSync('../runtime/vue/stubs', { extensions: ['.ts', '.mjs', '.js'], url: import.meta.url })
24+
}
1425

1526
return {
1627
name: 'nuxt:ui',
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
<script lang="ts">
2+
import type { ButtonHTMLAttributes } from 'vue'
3+
import type { AppConfig } from '@nuxt/schema'
4+
import theme from '#build/ui/link'
5+
import type { ComponentConfig } from '../../types/utils'
6+
7+
type Link = ComponentConfig<typeof theme, AppConfig, 'link'>
8+
9+
interface NuxtLinkProps {
10+
/**
11+
* Route Location the link should navigate to when clicked on.
12+
*/
13+
to?: string
14+
/**
15+
* An alias for `to`. If used with `to`, `href` will be ignored
16+
*/
17+
href?: string
18+
/**
19+
* Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
20+
*/
21+
external?: boolean
22+
/**
23+
* Where to display the linked URL, as the name for a browsing context.
24+
*/
25+
target?: '_blank' | '_parent' | '_self' | '_top' | (string & {}) | null
26+
/**
27+
* A rel attribute value to apply on the link. Defaults to "noopener noreferrer" for external links.
28+
*/
29+
rel?: 'noopener' | 'noreferrer' | 'nofollow' | 'sponsored' | 'ugc' | (string & {}) | null
30+
/**
31+
* If set to true, no rel attribute will be added to the link
32+
*/
33+
noRel?: boolean
34+
/**
35+
* A class to apply to links that have been prefetched.
36+
*/
37+
prefetchedClass?: string
38+
/**
39+
* When enabled will prefetch middleware, layouts and payloads of links in the viewport.
40+
*/
41+
prefetch?: boolean
42+
/**
43+
* Allows controlling when to prefetch links. By default, prefetch is triggered only on visibility.
44+
*/
45+
prefetchOn?: 'visibility' | 'interaction' | Partial<{
46+
visibility: boolean
47+
interaction: boolean
48+
}>
49+
/**
50+
* Escape hatch to disable `prefetch` attribute.
51+
*/
52+
noPrefetch?: boolean
53+
/**
54+
* Allows passing additional attributes to the actual rendered link.
55+
*/
56+
linkAttrs?: Record<string, any>
57+
ariaCurrentValue?: string
58+
}
59+
60+
export interface LinkProps extends NuxtLinkProps {
61+
/**
62+
* The element or component this component should render as when not a link.
63+
* @defaultValue 'button'
64+
*/
65+
as?: any
66+
/**
67+
* The type of the button when not a link.
68+
* @defaultValue 'button'
69+
*/
70+
type?: ButtonHTMLAttributes['type']
71+
disabled?: boolean
72+
/** Force the link to be active independent of the current route. */
73+
active?: boolean
74+
/** Will only be active if the current route is an exact match. */
75+
exact?: boolean
76+
/** Will only be active if the current route query is an exact match. */
77+
exactQuery?: boolean
78+
/** Will only be active if the current route hash is an exact match. */
79+
exactHash?: boolean
80+
/** The class to apply when the link is active. */
81+
activeClass?: string
82+
/** The class to apply when the link is inactive. */
83+
inactiveClass?: string
84+
custom?: boolean
85+
/** When `true`, only styles from `class`, `activeClass`, and `inactiveClass` will be applied. */
86+
raw?: boolean
87+
class?: any
88+
}
89+
90+
export interface LinkSlots {
91+
default(props: { active: boolean }): any
92+
}
93+
</script>
94+
95+
<script setup lang="ts">
96+
import { computed } from 'vue'
97+
import { defu } from 'defu'
98+
import { hasProtocol } from 'ufo'
99+
import { useAppConfig } from '#imports'
100+
import { tv } from '../../utils/tv'
101+
import ULinkBase from '../../components/LinkBase.vue'
102+
103+
defineOptions({ inheritAttrs: false })
104+
105+
const props = withDefaults(defineProps<LinkProps>(), {
106+
as: 'button',
107+
type: 'button',
108+
active: undefined,
109+
activeClass: '',
110+
inactiveClass: ''
111+
})
112+
defineSlots<LinkSlots>()
113+
114+
const appConfig = useAppConfig() as Link['AppConfig']
115+
116+
const ui = computed(() => tv({
117+
extend: tv(theme),
118+
...defu({
119+
variants: {
120+
active: {
121+
true: props.activeClass,
122+
false: props.inactiveClass
123+
}
124+
}
125+
}, appConfig.ui?.link || {})
126+
}))
127+
128+
const to = computed(() => props.to ?? props.href)
129+
130+
const isExternal = computed(() => {
131+
if (props.external) {
132+
return true
133+
}
134+
135+
if (!to.value) {
136+
return false
137+
}
138+
139+
return typeof to.value === 'string' && hasProtocol(to.value, { acceptRelative: true })
140+
})
141+
142+
const active = computed(() => {
143+
if (props.active !== undefined) {
144+
return props.active
145+
}
146+
147+
// Without router, we can't determine if link is active
148+
return false
149+
})
150+
151+
const rel = computed(() => {
152+
if (props.noRel) {
153+
return undefined
154+
}
155+
156+
if (props.rel) {
157+
return props.rel
158+
}
159+
160+
if (isExternal.value) {
161+
return 'noopener noreferrer'
162+
}
163+
164+
return undefined
165+
})
166+
167+
function resolveLinkClass() {
168+
if (props.raw) {
169+
return [props.class, active.value ? props.activeClass : props.inactiveClass]
170+
}
171+
172+
return ui.value({
173+
active: active.value,
174+
disabled: !!props.disabled,
175+
class: [props.class]
176+
})
177+
}
178+
</script>
179+
180+
<template>
181+
<template v-if="custom">
182+
<slot
183+
v-bind="{
184+
...$attrs,
185+
as,
186+
type,
187+
disabled,
188+
href: to,
189+
rel,
190+
target: isExternal ? '_blank' : props.target,
191+
isExternal,
192+
active
193+
}"
194+
/>
195+
</template>
196+
<ULinkBase
197+
v-else
198+
v-bind="{
199+
...$attrs,
200+
as: to && !isExternal && !disabled ? 'a' : as,
201+
type,
202+
disabled,
203+
href: to,
204+
rel,
205+
target: isExternal ? '_blank' : props.target,
206+
isExternal
207+
}"
208+
:class="resolveLinkClass()"
209+
>
210+
<slot :active="active" />
211+
</ULinkBase>
212+
</template>

0 commit comments

Comments
 (0)