1

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");

1 Answer 1

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

This is great! Marking as answer. I was really struggling with this and ended up taking it another direction which was a lot more "hacky" and even had some issues. I will be refactoring based on this. Basically what I did was I removed the router-links and added click listeners that passed the router-link paths as an argument. I then emitted linkClicked from TheSidebar.vue to App.vue which pushed the path to my router via this.$router.push(path). This seems to cause a problem with my history state though. Thanks so much!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.