Closed MtDalPizzol closed 3 years ago
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 https://github.com/bootstrap-vue/bootstrap-vue/issues/5759) as there is a way to assign custom link component to it. Is there any similar workaround here?
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 inertia-link
component has the as
property which accepts any DOM element or vue component. Additionally, the props passed to the inertia-link
component are passed to the rendered component implementation when using as
so you're able to leverage the standard Vuetify v-btn properties on it as well and it all works great.
<inertia-link as="v-btn" href="/" color="secondary" block small> Link </inertia-link>
Document my workaround here:
<inertia-link as="v-btn" href="/" color="secondary" block small> Link </inertia-link>
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 v-btn
anywhere outside of the inertia-link
, you'll just get a literal <v-btn>
tag and an error.
You'll either have to manually reference the component:
import { VBtn } from 'vuetify/lib'
export default {
//...
components: {
VBtn,
}
//...
}
or use the v-btn
tag normally somewhere else.
Probably a non-issue for the v-btn
component (unless you're doing a small prototype like me) but could affect a less common component.
In my case I was not using vue router in the project, so I registered the inertia-link
globally as router-link
.
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 v-btn
that the whole page was getting reloaded - gonna try @OrkhanAlikhanov solution
Edit:
Sadly I cannot get the solution to work with VuetifyJs 3.x
In my case I was not using vue router in the project, so I registered the
inertia-link
globally asrouter-link
.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) }, })
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
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 link.js
from inertia-vue3 source code and change as
prop type to accept both string and object
...
as: {
type: String,
default: 'a'
},
to
...
as: {
type: [String, Object],
default: 'a'
},
save it as new file, import it
now use the component with vuetify component for as
prop
<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 <v-list-item />
a link with to
) so it may need additional work, but I figure this can be a good starting point for others still having issues with this.
A plugin to create a fake RouterLink
component that provides the useLink
function that Vuetify uses. I just did a basic implementation of isActive and isExactActive; it may not behave exactly as VueRouter's does.
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 createApp()...use(inertia)...
. Then you can just use :to
as you normally would in Vuetify:
<v-list-item :to="route('index')">
<v-list-item-title>
Home
</v-list-item-title>
</v-list-item>
Note: This will only apply to to
and not href
, so you can use to
when you want to use internal Inertia routing, and href
for standard external routing.
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
<v-list-item />
a link withto
) so it may need additional work, but I figure this can be a good starting point for others still having issues with this.A plugin to create a fake
RouterLink
component that provides theuseLink
function that Vuetify uses. I just did a basic implementation of isActive and isExactActive; it may not behave exactly as VueRouter's does.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
createApp()...use(inertia)...
. Then you can just use:to
as you normally would in Vuetify:<v-list-item :to="route('index')"> <v-list-item-title> Home </v-list-item-title> </v-list-item>
Note: This will only apply to
to
and nothref
, so you can useto
when you want to use internal Inertia routing, andhref
for standard external routing.
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?
@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 createApp
settings. The idea is to modify the link object for useLink()
composable that Vuetify uses
Leaving this for reference hier:
package.json
{
"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"
}
}
app.js
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);
},
});
plugins/vuetify.ts
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,
}
},
})
link.ts
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)
},
}
},
})
},
}
Notice the usage of :to="..."
<v-btn v-if="canResetPassword" variant="plain" size="small" :to="route('password.request')">
Forgot your password?
</v-btn>
App.vue
<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 produce a button tag inside an a tag, what is not a valid html
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>
This will not applyable to v-list 's, actually will, but you probably face some difficulties in setting routing active states
@tangamampilia This will produce a button tag inside an a tag, what is not a valid html
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 link.ts
:
- const href = props.to;
+ const href = props.to.value;
This is needed because Vuetify fixed a bug and props.to
is now reactive.
Links for reference: https://github.com/vuetifyjs/vuetify/releases/tag/v3.5.14 https://github.com/vuetifyjs/vuetify/issues/19515
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 router.visit()
with both inertia link and component props support
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>