Skip to content
Advertisement

How to toggle sidebar without page reload when router-link is clicked in Vue 3

I have a sidebar component that works similar to a modal. When a button is clicked, the sidebar translates into the viewport with nav links. These nav links are actually router-links that are wired up to a vue-router.

What I’m trying to accomplish

When I click on a router-link that is inside my sidebar component, I want the sidebar to transition off the viewport and I want the clicked router-link’s component to render without the page reloading.

What’s currently happening

When I click on the router-link, the sidebar is removed instantly from the DOM. It does not translate off the screen as intended. Also, the page is reloaded.

What else have I tried

I also tried moving the <transition> wrapper inside TheSidebar.vue component along with the associated CSS classes, and I passed sidebarIsVisible as a prop from App.vue to TheSidebar.vue.

My code

A Codesandbox demo can be found here

App.vue

<template>
  <router-view></router-view>
  <button @click="toggleSidebar" class="toggleBtn">Toggle Sidebar</button>
  <transition name="sidebar">
    <the-sidebar
      v-if="sidebarIsVisible"
      @link-clicked="toggleSidebar"
    ></the-sidebar>
  </transition>
</template>

<script>
import TheSidebar from "./components/TheSidebar.vue";

export default {
  components: {
    TheSidebar,
  },
  data() {
    return {
      sidebarIsVisible: false,
    };
  },
  methods: {
    toggleSidebar() {
      this.sidebarIsVisible = !this.sidebarIsVisible;
    },
    closeSidebar() {
      this.sidebarIsVisible = false;
    },
  },
};
</script>

<style>
/* basic styling */

.toggleBtn {
  position: fixed;
  top: 5px;
  left: 5px;
}

.sidebar-enter-active {
  animation: slide-sidebar 0.3s ease;
}

.sidebar-leave-active {
  animation: slide-sidebar 0.3s ease reverse;
}

@keyframes slide-sidebar {
  from {
    transform: translateX(-100%);
  }
  to {
    transform: translateX(0);
  }
}
</style>

TheSidebar.vue

<template>
  <div class="sidebar">
    <nav>
      <ul>
        <li>
          <router-link @click="$emit('link-clicked')" to="/link1">
            Link 1
          </router-link>
        </li>
        <li>
          <router-link @click="$emit('link-clicked')" to="/link2">
            Link 2
          </router-link>
        </li>
      </ul>
    </nav>
  </div>
</template>

<script>
export default {
  emits: ["link-clicked"],
};
</script>

<style scoped>
/* basic styling */
</style>

main.js

import { createApp } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import App from "./App.vue";
import LinkOne from "./components/LinkOne.vue";
import LinkTwo from "./components/LinkTwo.vue";

const app = createApp(App);
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: "/link1", component: LinkOne },
    { path: "/link2", component: LinkTwo }
  ]
});

app.use(router);

app.mount("#app");

Advertisement

Answer

There are a couple of things I’m unsure about here but I’ll try and explain what I think is happening.

Firstly, the click event on the router-link is what’s causing the page to reload, but I can’t find anything in the docs mentioning this as expected behaviour (it might be worth opening an issue on the GitHub repo).

The fix for this is to use event-delegation by moving the event-handler onto the ul and creating a method to determine if a link has been clicked (example below).

Secondly, and this is where things get weird, in VSCode, using kebab-case in the child components’ emitted event seems to prevent anything from being emitted, so changing them to camelCase fixes this. But, trying this in CodeSandbox simply doesn’t work, and ESLint complains that the emitted event should be kebab-case. So, in CodeSandbox, the opposite is true: the emitted event names should be kebab-case and the listener should be camelCase! Absolutely no idea why as this goes against what the docs say on casing:

…we recommend using kebab-cased event listeners when you are using in-DOM templates.

Again, I can’t find anything in the docs explicitly saying you need to use camelCase when emitting an event, it just says kebab-case is preferred when listening for an event.


So, all in all, for your code to work in VSCode and in a way which follows what is recommended by the docs, you need to change it to this:

<template>
    <div class="sidebar">
        <nav>
            <!-- Move event here -->
            <ul @click="handleClick($event)">
                <li>
                    <router-link to="/link1">
                        Link 1
                    </router-link>
                </li>
                <li>
                    <router-link to="/link2">
                        Link 2
                    </router-link>
                </li>
            </ul>
        </nav>
    </div>
</template>

<script>
export default {
    emits: ['linkClicked'], // Use camelCase for emitting

    methods: {
        handleClick(e) {
            // Check the target is a link being clicked
            if (e.target.localName !== 'a') return

            this.$emit('linkClicked')
        }
    }
}
</script>

Keep App.vue exactly as you have it already and it should work.


For your code to work in CodeSandbox, swap the casing:

...
    emits: ['link-clicked'], // Use kebab-case for emitting
...
            this.$emit('link-clicked')
...

App.vue:

@linkClicked="toggleSidebar"

Working example.


If anyone could shed some light on this, it’d be great as I’m completely stumped on what’s happening here.

User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement