-
-
Notifications
You must be signed in to change notification settings - Fork 7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Feature Request] Add supoort for <inertia-link> component #11573
Comments
Thank you for the Feature Request and interest in improving Vuetify. Unfortunately this is not functionality that we are looking to implement at this time. |
This really sounds like a deal breaker to me. With BootstrapVue there is a workaround (see bootstrap-vue/bootstrap-vue#5759) as there is a way to assign custom link component to it. Is there any similar workaround here? |
Stumbled across this and wanted to an answer as well. I found that the
|
Document my workaround here: Component Code// @ts-nocheck
import Vue, { VNodeData } from 'vue'
import {VBtn} from 'vuetify/lib'
export default Vue.extend({
extends: VBtn,
methods: {
generateRouteLink() {
let exact = this.exact
let tag
const data: VNodeData = {
attrs: {
tabindex: 'tabindex' in this.$attrs ? this.$attrs.tabindex : undefined,
},
class: this.classes,
style: this.styles,
props: {},
directives: [{
name: 'ripple',
value: this.computedRipple,
}],
// [this.to ? 'nativeOn' : 'on']: {
'on': {
...this.$listeners,
click: this.click,
},
ref: 'link',
}
if (typeof this.exact === 'undefined') {
exact = this.to === '/' ||
(this.to === Object(this.to) && this.to.path === '/')
}
if (this.to) {
// Add a special activeClass hook
// for component level styles
let activeClass = this.activeClass
let exactActiveClass = this.exactActiveClass || activeClass
if (this.proxyClass) {
activeClass = `${activeClass} ${this.proxyClass}`.trim()
exactActiveClass = `${exactActiveClass} ${this.proxyClass}`.trim()
}
tag = 'inertia-link'
Object.assign(data.props, {
href: this.to,
exact,
activeClass,
exactActiveClass,
append: this.append,
replace: this.replace,
})
} else {
tag = (this.href && 'a') || this.tag || 'div'
if (tag === 'a' && this.href) data.attrs!.href = this.href
}
if (this.target) data.attrs!.target = this.target
return { tag, data }
}
}
}) |
Worth noting that this only works for vue components that are actually included in the build process. If you're using treeshaking and automatic component loading but aren't referencing You'll either have to manually reference the component: import { VBtn } from 'vuetify/lib'
export default {
//...
components: {
VBtn,
}
//...
} or use the Probably a non-issue for the |
In my case I was not using vue router in the project, so I registered the Vue.component('router-link', {
functional: true,
render(h, context) {
const data = { ...context.data }
delete data.nativeOn
const props = data.props as any || {}
props.href = props.to /// v-btn passes `to` prop but inertia-link requires `href`, so we just copy it
return h('inertia-link', data, context.children)
},
}) |
anyone know how to do it on Vuetify3? |
I was looking for this too after I noticed everywhere we used Edit: Sadly I cannot get the solution to work with VuetifyJs 3.x |
is there any solution to get this work on vue 3 + vuetify 3 |
I use a helper function for Inertia Links within vuetify 3 with vue 3. see: https://gist.github.com/alexanderfriederich/887ecfd3ae99d54445a4126bb302d2d7 Import it, and use it like: <v-list-item @click="triggerInertiaLink('accounts.index')">
<v-list-item-title>Accounts</v-list-item-title>
</v-list-item> Works flawless in my vue 3 + vuetify 3 setup |
yes, but in this case you cannot use browsers "Open in new tab" context menu action |
Open ...
as: {
type: String,
default: 'a'
}, to ...
as: {
type: [String, Object],
default: 'a'
}, save it as new file, import it <Link :as="VBtn" :href="route('home')" :active="route().current('home')" label="Home" /> |
I'm trying Inertia with Vue 3 + Vuetify 3 for a POC and ran into this issue. I came up with another solution that seems to be working well so far, but I have only tried it for my specific use case (making a A plugin to create a fake import {computed} from 'vue';
import {Inertia} from '@inertiajs/inertia';
import {useBrowserLocation} from '@vueuse/core';
export default {
install(app, options) {
app.component('RouterLink', {
useLink(props) {
const browserLocation = useBrowserLocation();
const currentUrl = computed(() => `${browserLocation.value.origin}${browserLocation.value.pathname}`);
return {
route: computed(() => ({href: props.to})),
isExactActive: computed(() => currentUrl.value === props.to),
isActive: computed(() => currentUrl.value.startsWith(props.to)),
navigate(e) {
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
e.preventDefault();
Inertia.visit(props.to);
}
}
},
});
},
}; Just install that as a plugin as you normally would <v-list-item :to="route('index')">
<v-list-item-title>
Home
</v-list-item-title>
</v-list-item> Note: This will only apply to |
An improvement I needed was to add router.visit(props.to, {
method: e.currentTarget.getAttribute('method') || 'get'
}); to make thinkgs like logout this way: <VListItem
:prepend-icon="mdiLogoutVariant"
:to="route('logout')"
method="post"
title="Logout"
/> |
My setup import { router, usePage } from "@inertiajs/vue3"
import { computed } from "vue"
export default {
install(app) {
app.component("RouterLink", {
useLink(props) {
const href = props.to
const currentUrl = computed(() => usePage().url)
return {
route: computed(() => ({ href })),
isActive: computed(() => currentUrl.value.startsWith(href)),
isExactActive: computed(() => href === currentUrl.value),
navigate(e) {
if (e.shiftKey || e.metaKey || e.ctrlKey) return
e.preventDefault()
router.visit(href)
},
}
},
})
},
} |
@maxflex Could you elaborate this a little bit? Where to put this code? |
As @MKRazz suggested, you can create a plugin and then use it in |
Leaving this for reference hier: Laraverl 9 + Inertia 1.0 + Vue 3 + Vuetify 3Basic setup
{
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vite build"
},
"dependencies": {
"@inertiajs/vue3": "^1.0.2",
"@types/ziggy-js": "^1.3.2",
"vuetify": "^3.1.8"
},
"devDependencies": {
"@mdi/js": "^7.1.96",
"@types/node": "^18.14.6",
"@vitejs/plugin-vue": "^3.0.0",
"laravel-vite-plugin": "^0.7.2",
"typescript": "^4.9.5",
"vue": "^3.2.31"
}
}
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
import vuetify from "@/plugins/vuetify";
import link from "@/plugins/link";
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
progress: {
color: '#29d',
},
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.use(vuetify)
.use(link)
.mount(el);
},
});
import 'vuetify/styles'
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi-svg'
import { mdiHome } from "@mdi/js";
export default createVuetify({
components,
directives,
defaults: {
VTextField: {
variant: 'outlined'
}
},
icons: {
defaultSet: 'mdi',
aliases: {
...aliases,
home: mdiHome,
},
sets: {
mdi,
}
},
})
import { router, usePage } from "@inertiajs/vue3"
import { computed } from "vue"
export default {
install(app) {
app.component("RouterLink", {
useLink(props) {
const href = props.to
const currentUrl = computed(() => usePage().url)
return {
route: computed(() => ({ href })),
isActive: computed(() => currentUrl.value.startsWith(href)),
isExactActive: computed(() => href === currentUrl),
navigate(e) {
if (e.shiftKey || e.metaKey || e.ctrlKey) return
e.preventDefault()
router.visit(href)
},
}
},
})
},
} UsageNotice the usage of <v-btn v-if="canResetPassword" variant="plain" size="small" :to="route('password.request')">
Forgot your password?
</v-btn>
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import { Head, useForm } from '@inertiajs/vue3';
defineProps({
canResetPassword: Boolean,
status: String,
});
const form = useForm({
email: '',
password: '',
remember: false
});
const submit = () => {
form.post(route('login'), {
onFinish: () => form.reset('password'),
});
};
</script>
<template>
<GuestLayout>
<Head title="Log in"/>
<v-alert v-if="status">
{{ status }}
</v-alert>
<v-card class="mx-auto" width="100%" max-width="344">
<v-toolbar>
<v-toolbar-title>Login</v-toolbar-title>
</v-toolbar>
<v-form @submit.prevent="submit">
<v-container>
<v-text-field
v-model="form.email"
:error-messages="form.errors.email"
autocomplete="username"
autofocus
label="Email"
required
type="email"
class="mb-2"
/>
<v-text-field
v-model="form.password"
:error-messages="form.errors.password"
autocomplete="current-password"
autofocus
label="Password"
required
type="password"
/>
<v-checkbox
v-model="form.remember"
name="remember"
label="Remember me"
/>
<v-btn v-if="canResetPassword" variant="plain" size="small"
:to="route('password.request')"
>
Forgot your password?
</v-btn>
</v-container>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="success" :loading="form.processing" type="submit">
Log in
</v-btn>
</v-card-actions>
</v-form>
</v-card>
</GuestLayout>
</template> |
Not sure if this is the right way, but I found a more simple solution. <Link :href="route('route.index')" method="get" as="div" class="d-inline">
<v-btn type="button">Submit</v-btn>
</Link> |
@tangamampilia |
This will not applyable to v-list 's, actually will, but you probably face some difficulties in setting routing active states |
Actually it wrap the button inside of a div, the attribute as defines the parent element. I know it's not the best solution, but seems to be working even with the routing states. |
For anyone having problems using this awersome solution after upgradting to Vuetify v3.5.14: Update this line in the - const href = props.to;
+ const href = props.to.value; This is needed because Vuetify fixed a bug and Links for reference: |
Hello Everyone I decided to create a package to maintain this excellent functionality across all my projects, should Vuetify or Intertia publish changes in the future. You can find the package here: https://www.npmjs.com/package/vuetify-inertia-link Many thanks to @robjuz ! |
I adapted https://github.com/inertiajs/inertia/blob/master/packages/vue3/src/link.ts to accept component for the :as props <script lang="ts" setup>
import {
ComponentOptionsBase,
computed,
ComputedOptions,
CreateComponentPublicInstance,
FunctionalComponent,
h,
MethodOptions,
useAttrs,
useSlots,
VNode,
} from 'vue'
import {
FormDataConvertible,
mergeDataIntoQueryString,
Method,
PreserveStateOption,
router,
shouldIntercept,
} from '@inertiajs/core'
import { useBrowserLocation } from '@vueuse/core'
/* eslint-disable @typescript-eslint/no-explicit-any */
type CustomComponentType =
(ComponentOptionsBase<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, any> & ThisType<CreateComponentPublicInstance<any, any, any, ComputedOptions, MethodOptions, any, any, any, Readonly<any>>>)
| FunctionalComponent<any, any>
/* eslint-enable */
interface ModifiedInertiaLinkProps {
method?: Method
replace?: boolean
preserveScroll?: PreserveStateOption
preserveState?: PreserveStateOption
only?: Array<string>
headers?: Record<string, string>
errorBag?: string | null
forceFormData?: boolean
queryStringArrayFormat?: 'indices' | 'brackets'
href?: string | null
as?: string | CustomComponentType
active?: boolean
exactActive?: boolean
data?: Record<string, FormDataConvertible>
}
const props = withDefaults(defineProps<ModifiedInertiaLinkProps>(), {
href: null,
method: 'get',
data: () => ({}),
as: 'a',
preserveScroll: false,
preserveState: false,
replace: false,
headers: () => ({}),
errorBag: null,
queryStringArrayFormat: () => ('brackets'),
active: () => false,
exactActive: () => false,
boolean: () => false,
only: () => ([]),
})
const slots = useSlots()
let as = props.as
const method = props.method
const attrs = useAttrs()
let component: string | VNode
const [href, data] = mergeDataIntoQueryString(props.method, props.href || '', props.data, props.queryStringArrayFormat)
const browserLocation = useBrowserLocation()
const computedActive = computed(() => {
if (!browserLocation.value.pathname || !props.href) {
return false
}
const currentUrl = browserLocation.value.origin + browserLocation.value.pathname.replace(/\/$/, '')
const hrefWithoutTrailingSlash = props.href.replace(/\/$/, '')
if (props.active) {
return true
}
if (props.exactActive) {
// bad
// return `${browserLocation.value.origin}${browserLocation.value.pathname}` === props.href
return currentUrl === hrefWithoutTrailingSlash
}
return currentUrl.startsWith(props.href)
})
// vuetify-specific props
const onClick = (event: KeyboardEvent) => {
if (shouldIntercept(event)) {
event.preventDefault()
router.visit(href, {
data: data,
method: method,
replace: props.replace,
preserveScroll: props.preserveScroll,
preserveState: props.preserveState ?? method !== 'get',
only: props.only,
headers: props.headers,
// @ts-expect-error TODO: Fix this
onCancelToken: attrs.onCancelToken || (() => ({})),
// @ts-expect-error TODO: Fix this
onBefore: attrs.onBefore || (() => ({})),
// @ts-expect-error TODO: Fix this
onStart: attrs.onStart || (() => ({})),
// @ts-expect-error TODO: Fix this
onProgress: attrs.onProgress || (() => ({})),
// @ts-expect-error TODO: Fix this
onFinish: attrs.onFinish || (() => ({})),
// @ts-expect-error TODO: Fix this
onCancel: attrs.onCancel || (() => ({})),
// @ts-expect-error TODO: Fix this
onSuccess: attrs.onSuccess || (() => ({})),
// @ts-expect-error TODO: Fix this
onError: attrs.onError || (() => ({})),
})
}
}
if (typeof as === 'string') {
as = as.toLowerCase()
if (as === 'a' && method !== 'get') {
console.warn(
`Creating POST/PUT/PATCH/DELETE <a> links is discouraged as it causes "Open Link in New Tab/Window" accessibility issues.\n\nPlease specify a more appropriate element using the "as" attribute. For example:\n\n<LinkComponent href="${href}" method="${method}" as="button">...</LinkComponent>`,
)
}
component = h(
as,
{
active: computedActive.value,
...attrs,
...(as === 'a' ? { href } : {}),
onClick,
},
slots,
)
} else {
component = h(
as,
{
active: computedActive.value,
...attrs,
href,
onClick,
},
slots,
)
}
</script>
<template>
<component :is="component" />
</template> <script lang="ts" setup>
import { VBtn } from 'vuetify/components'
import Link from '../components/InertiaLink.vue'
</script>
<template>
<Link
:as="VBtn"
href="/playground"
color="primary"
block
>
to playground route
</Link>
<Link>
empty link
</Link>
</template> it's closer to what inertia actually do instead of just calling |
Problem to solve
I'm using Inertia.js with Laravel after a lot of frustration with Nuxt. Inertia do it's navigation magic with the
<inertia-link>
component. It's something linke<nuxt-link>
. It would be nice to have support for this on<v-btn>
,<v-chip>
just like we have for nuxt.Proposed solution
It could be something as simple as:
<v-btn inertia to="route">
. The same deal going on with<v-btn nuxt to="route>
The text was updated successfully, but these errors were encountered: