vuejs / vue-router

🚦 The official router for Vue 2
http://v3.router.vuejs.org/
MIT License
18.99k stars 5.06k forks source link

Allow defining an inactive-class #2648

Open christhofer opened 5 years ago

christhofer commented 5 years ago

What problem does this feature solve?

We already have prop active-class that will be appended to the element when the route is active. But what if that we need to specify class only when the route is not active?

example: when active => .btn-primary when inactive => .btn-secondary

What does the proposed API look like?

<router-link
    :to="{name: 'purchase'}"
    class="btn"
    active-class="btn-primary"
    inactive-class="btn-secondary">
    Purchase
</router-link>
martiendt commented 5 years ago

You can use class binding and compare it with your routes

<router-link
    to="/purchase/purchase-request"
    class="btn"
    :class="{'btn-secondary': this.$route.path != '/purchase/purchase-request'}"
    active-class="btn-primary">
    <span><i class="si si-folder-alt"></i> Purchase Request</span>
</router-link>
christhofer commented 5 years ago

You can use class binding and compare it with your routes

<router-link
    to="/purchase/purchase-request"
    class="btn"
    :class="{'btn-secondary': this.$route.path != '/purchase/purchase-request'}"
    active-class="btn-primary">
    <span><i class="si si-folder-alt"></i> Purchase Request</span>
</router-link>

Still need additional logic if want to match purchase/purchase-request/create and other link You need indexOf !== -1 for this

posva commented 5 years ago

Add a regular class to those specific links and override them with the active class: class="btn btn-secondary" active-class="btn-primary"

christhofer commented 5 years ago

Add a regular class to those specific links and override them with the active class: class="btn btn-secondary" active-class="btn-primary"

@posva but that will be rendered as class="btn btn-secondary btn-primary" instead of class="btn btn-primary"

posva commented 5 years ago

I'm reopening this as it does make sense for utility-first CSS frameworks like Tailwind

posva commented 4 years ago

I realized the feature request isn't clear, especially given the changes on v4 for the active/exact active behavior: https://github.com/vuejs/rfcs/pull/136

Should inactive be non active or non exact active? At this point why not add both inactive-class and exact-inactive-class? Which would create 4 classes that can be overridden by the user, making it more complicated for beginners.

It's important to note that in v4, exact-active is likely to be the class most people will hook on because links are active if any of their children are active (see RFC). TLDR: Exact active will be active only if the link corresponds to the actual location the user is at, there is no need for exact prop anymore

christhofer commented 4 years ago

I don't quite understand the confusion about this request. High prio high complex?

Vue-router already know when to append active class Can't we just make it a simple ternary

isActive ? activeClass : inactiveClass

With inactiveClass default value is empty string. So if user doesn't set inactive class, it will just append empty string.

posva commented 4 years ago

It could also be isExactActive ? exactActiveClass : exactInactiveClass

mdwheele commented 4 years ago

At this point why not add both inactive-class and exact-inactive-class?

Yeah, I could totally see that being useful. Would you like me to go ahead and put that into #3184 with appropriate tests?

posva commented 4 years ago

Thank you but let's wait for more feedback

mdwheele commented 4 years ago

Hey @posva!

Do you have an update on whether or not this will be supported in vue-router? It's one of those things that's only a thorn in the side when I happen to touch our lower-level navigation components, so no big deal. But today was one of those days for me 😂

posva commented 4 years ago

There is no update on this. Keep in mind it's completely fine to extend RouterLink and add your own props while using the v-slot api to implement this feature request

mdwheele commented 4 years ago

There is no update on this.

No worries! Just curious.

Keep in mind it's completely fine to extend RouterLink and add your own props while using the v-slot api to implement this feature request

That's exactly what we do, but it is not legible code to immediately see what's happening and it breaks a lot of the productivity benefits we otherwise get from Tailwind CSS (to have to switch gears mentally). I'd prefer to not have to abstract even further to hide some of these details, but it's likely coming to that soon (read: if I have to touch this code again, it's happening).

I do think having a way to apply a CSS class when the <router-link> are inactive would be very useful to users of Tailwind, Tachyons, Basscss, etc. so I'm hopeful we can implement at some point. Otherwise, I dread having to NavLink all over the place, but it is what it is.

posva commented 4 years ago

I mean creating a custom component that extends router link so you can write in your code <my-router-link inactive-class="something"/>. You write it once, use it everywhere:

<template>
  <router-link v-bind="$props" v-slot="{ isActive, href, navigate }">
    <a
      v-bind="$attrs"
      v-on="$listeners"
      :href="href"
      @click="navigate"
      :class="isActive ? activeClass : inactiveClass"
    >
      <slot />
    </a>
  </router-link>
</template>

<script lang="ts">
import Vue from 'vue'

const RouterLink = Vue.component('RouterLink')

export default {
  name: 'AppLink',
  props: {
    // @ts-ignore
    ...RouterLink.options.props,
    inactiveClass: String,
  },
}
</script>
ImJustAMan commented 4 years ago

I really thought that if expose isActive and isExactActive, we don't need inactive-class any more. we can just use <router-link :class="isActive ? activeClass : inactiveClass"/>.I think thats enough.Maybe we don't even need active-class anymore;

posva commented 4 years ago

Given the current feedback I think it is more important to document how to extend RouterLink via the v-slot api to create custom router links (example at https://github.com/vuejs/vue-router/issues/2648#issuecomment-645643957) that fit the need of any application than adding two new props + two new global options (for consistency).

While I agree this is essential when using Tailwind CSS (being a big fan myself), it's still something that most of users are not using and ends up being useless for anybody not using utility-first CSS libraries. On top of that, even it those scenarios, you still have to decide between exact inactive and inactive class

christhofer commented 4 years ago

If we can get the active / exactActive value from vue router, I'm fine with custom component that wraps router link component. I wrapped all of my components that comes from a library, and defining most of the props there, so that I don't writes the same props every time I use the component.

cosmini commented 4 years ago

In my opinion if you provide option for custom class for some state, it should cover all possible states. Having only active state is not good enough. Complex applications are detailed and we use vue, vue-router frameworks so we can created such apps easier.

ImJustAMan commented 4 years ago

In my opinion if you provide option for custom class for some state, it should cover all possible states. Having only active state is not good enough. Complex applications are detailed and we use vue, vue-router frameworks so we can created such apps easier.

agree with you. If we can get all options(like active and exactActive).I'm sure about we can use those state that make our apps easier

christhofer commented 4 years ago

Now that Laravel 8 uses Tailwind as the default, this use case should becomes more common, isn't it?

I mean creating a custom component that extends router link so you can write in your code <my-router-link inactive-class="something"/>. You write it once, use it everywhere:

<template>
  <router-link v-bind="$props" v-slot="{ isActive, href, navigate }">
    <a
      v-bind="$attrs"
      v-on="$listeners"
      :href="href"
      @click="navigate"
      :class="isActive ? activeClass : inactiveClass"
    >
      <slot />
    </a>
  </router-link>
</template>

<script lang="ts">
import Vue from 'vue'

const RouterLink = Vue.component('RouterLink')

export default {
  name: 'AppLink',
  props: {
    // @ts-ignore
    ...RouterLink.options.props,
    inactiveClass: String,
  },
}
</script>

@posva Is this already possible? or should we wait for Vue 3 to be able to do this?

posva commented 4 years ago

The thing is with TailwindCSS you end up creating your own custom link anyway because you don't want to write a big list of class every time you write a link:

Custom NavLink.vue reusing AppLink from https://github.com/vuejs/vue-router/issues/2648#issuecomment-645643957:

<template>
  <AppLink
    v-bind="$attrs"
    class="inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 focus:outline-none transition duration-150 ease-in-out hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"
    active-class="border-indigo-500 text-gray-900 focus:border-indigo-700"
    inactive-class="text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300"
  >
    <slot />
  </AppLink>
</template>

It will take more time for this to be adopted, so let's just focus on other more important problems for the moment since this solution works on Vue router for Vue 2

christhofer commented 4 years ago

since this solution works on Vue router for Vue 2

Got it. Will try this in my next project.

christhofer commented 4 years ago

@posva , I see that $listeners is deprecated in vue 3. Any alternative for this? I can't find about it in v3 docs.

durub commented 3 years ago

@christhofer, $listeners is now part of $attrs.

You can read more about it in the migration guide: https://v3.vuejs.org/guide/migration/listeners-removed.html

vlasscontreras commented 3 years ago

Anyone interested in how to implement @posva's solution in Vue Router 4 + Vue 3, there's a full example in the extending RouterLink page.

ddahan commented 2 years ago

Like some other users, I have a strong feeling that extending a Vue Component by myself for such a basic need is a non sense. Even if it's technically possible, it would makes the code more complex for no good reason.

The fact that the API isn't consistent by default (active-class without inactive-class) makes me think of a mistake in the conception, like the developer did not think of a use case in which specifying inactive-class could be useful for users.

Please consider adding it, especially if it's not a complex feature and wanted by many.

apprat commented 2 years ago

I encapsulated a component myself to implement the function of vue-router 3, WTF

kilakewe commented 2 years ago

+1 to adding this feature.

hyrumwhite commented 1 year ago

Ooh, is this feature coming soon? There's some commented out code for inactive classes in the RouterLink component. Really think it'd be a nice feature.

mukundshah commented 1 year ago

Any update?

fernandoanael commented 1 year ago

The worst part for me is that active-class address the classes to the beginning of the class statement. This makes it impossible to have the active class overwrite the inactive class. That would be a nice workaround if it worked.

+1 to adding inactive-class

henribru commented 1 year ago

The worst part for me is that active-class address the classes to the beginning of the class statement. This makes it impossible to have the active class overwrite the inactive class. That would be a nice workaround if it worked.

+1 to adding inactive-class

It doesn't work like that, it's the order in the CSS file that matters.

mukundshah commented 1 year ago

maybe we can use clsx or something like tailwind-merge.

i will also check into it.

uonick commented 1 year ago

Perhaps this will help someone. When using tailwind, you can apply this trick:

<router-link :to="...." active-class="!text-rose-500" class="text-gray-500">
    <component :is="icon" />
</router-link>

Just use the !

jenky commented 1 year ago

Another trick is using :not() pseudo-class. For example (Tailwind):

.nav-link {
  &.active {
    @apply border-indigo-500 text-gray-900 focus:border-indigo-700
  }
  &:not(.active) {
    @apply text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:text-gray-700 focus:border-gray-300
  }
}
<RouterLink
  to="/"
  active-class="active"
  class="nav-link inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"
>
  Home
</RouterLink>
zelds commented 1 year ago

Solved this issue in same way as @uonick did. Using ! + exact since using nested routes.

<router-link
  to="path"
  exact-active-class="!border-indigo-500 !text-gray-900"
  class="inline-flex items-center border-b-2 border-indigo-500 px-1 pt-1 text-sm font-medium border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700"
  exact
  >Navbar Item</router-link>
viglia commented 10 months ago

I'm following the example in the extending RouterLink page but I can't get it to work with typescript + composition API.

Anyone got it working while using both the composition API and typescript?

+1 to adding inactive-class as it'd be really handy

viglia commented 10 months ago

I'm posting the typescript solution here in case anyone else is having the same trouble as I did to get it working by following the official docs.

Inside AppLink.vue:

<script lang="ts">
export default {
    inheritAttrs: false,
}
</script>

<script setup lang="ts">
import { computed, toRefs } from 'vue'
import { RouterLink } from 'vue-router'

const props = defineProps({
    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    ...RouterLink.props,

    activeClass: {
        type: String,
        default: '',
    },

    inactiveClass: {
        type: String,
        default: '',
    },
})

const { to, activeClass, inactiveClass } = toRefs(props)

const isExternalLink = computed(() => typeof to.value === 'string' && to.value.startsWith('http'))
</script>

<template>
    <a v-if="isExternalLink" v-bind="$attrs" :href="to" target="_blank">
        <slot />
    </a>
    <RouterLink v-else v-slot="{ isActive, href, navigate }" v-bind="$props" custom>
        <a v-bind="$attrs" :href="href" :class="isActive ? activeClass : inactiveClass" @click="navigate"
            :aria-current="isActive ? 'page' : false">
            <slot />
        </a>
    </RouterLink>
</template>

Then in your own component file where you need to use the custom RouterLink:

<script setup lang="ts">
import { ref } from 'vue'
import AppLink from './AppLink.vue'

const activeClass = ref("block py-2 pr-4 pl-3 text-white rounded bg-primary-700 lg:bg-transparent lg:text-primary-700 lg:p-0 dark:text-white")
const inactiveClass = ref("block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700")
</script>

<template>
                <!-- This is just an example-->
                <ul class="flex flex-col mt-4 font-medium lg:flex-row lg:space-x-8 lg:mt-0">
                    <li>
                        <AppLink :to="{ name: 'home' }" :active-class="activeClass" :inactive-class="inactiveClass">
                            Home
                        </AppLink>
                    </li>
                    <li>
                        <AppLink :to="{ name: 'about' }" :active-class="activeClass" :inactive-class="inactiveClass">
                            About</AppLink>
                    </li>

                </ul>
</template>
karol-bujacek commented 10 months ago

I think that the easiest way how to use inactive classes bay be the Link component from Nuxt UI.

The Link component is a wrapper around <NuxtLink> through the custom prop that provides a few extra props:

  • inactive-class prop to set a class when the link is inactive, active-class is used when active.

See https://ui.nuxt.com/elements/link