Skip to content
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

Closed
MtDalPizzol opened this issue Jun 7, 2020 · 25 comments
Closed

[Feature Request] Add supoort for <inertia-link> component #11573

MtDalPizzol opened this issue Jun 7, 2020 · 25 comments
Labels
wontfix The issue is expected and will not be fixed

Comments

@MtDalPizzol
Copy link

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>

@ghost ghost added the S: triage label Jun 7, 2020
@KaelWD
Copy link
Member

KaelWD commented Oct 9, 2020

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.

@KaelWD KaelWD closed this as completed Oct 9, 2020
@KaelWD KaelWD added wontfix The issue is expected and will not be fixed and removed S: triage labels Oct 9, 2020
@wfjsw
Copy link

wfjsw commented Jan 15, 2021

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?

@garrensweet
Copy link

garrensweet commented Feb 5, 2021

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>

@wfjsw
Copy link

wfjsw commented Feb 5, 2021

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 }

        }
    }
})

@CPSibo
Copy link

CPSibo commented Jun 2, 2021

<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.

@OrkhanAlikhanov
Copy link

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)
  },
})

@ibnu-ja
Copy link

ibnu-ja commented Mar 11, 2022

anyone know how to do it on Vuetify3?

@ThaDaVos
Copy link

ThaDaVos commented Jun 14, 2022

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

@jakemake
Copy link

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)
  },
})

is there any solution to get this work on vue 3 + vuetify 3

@alexanderfriederich
Copy link

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

@jakemake
Copy link

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

@ibnu-ja
Copy link

ibnu-ja commented Oct 20, 2022

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" />

@MKRazz
Copy link

MKRazz commented Dec 21, 2022

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.

@gcaraciolo
Copy link

gcaraciolo commented Mar 5, 2023

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.

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"
/>

@maxflex
Copy link

maxflex commented Mar 9, 2023

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)
          },
        }
      },
    })
  },
}

@hw-rjuzak
Copy link

@maxflex Could you elaborate this a little bit? Where to put this code?

@maxflex
Copy link

maxflex commented Mar 9, 2023

@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

@robjuz
Copy link

robjuz commented Mar 11, 2023

Leaving this for reference hier:

Laraverl 9 + Inertia 1.0 + Vue 3 + Vuetify 3

Basic setup

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)
          },
        }
      },
    })
  },
}

Usage

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>

@tangamampilia
Copy link

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>

@robjuz
Copy link

robjuz commented Mar 12, 2023

@tangamampilia
This will produce a button tag inside an a tag, what is not a valid html

https://stackoverflow.com/a/6393863

@jakemake
Copy link

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
Copy link

tangamampilia commented Mar 13, 2023

@tangamampilia This will produce a button tag inside an a tag, what is not a valid html

https://stackoverflow.com/a/6393863

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.

@Mohammad-Alavi
Copy link

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
#19515

@gigerIT
Copy link

gigerIT commented Apr 18, 2024

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 !

@ibnu-ja
Copy link

ibnu-ja commented Apr 30, 2024

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
wontfix The issue is expected and will not be fixed
Projects
None yet
Development

No branches or pull requests