Control modals using vue router

12.3k views Asked by At

I am working in the project developed with Vue 2 with VueRouter and I am trying to work with my modals controlled by my VueRouter!

I've done the following code

Main vue component: My normal components will be loaded on the default router-view and all my modals will be loaded on the modal router-view

<div id="app">
  <router-view v-if="!isLoading"></router-view>
  <router-view v-if="!isLoading" name="modal"></router-view>
</div>

RoutedModals Mixing As you can see on my beforeRouteEnter method I am checking if there is a previous "from" route (which means the user got the page navigating inside the app)... If it's I set that one as default component... if not (which means the user got directly from the URL) I set my dashboard as default and it will be opened behind my modal.

import Dashboard from 'modules/dashboard/components/Main.vue'
import { isNil } from 'lodash'

export default {
  data() {
      return {
        canAccessDirect: true,
        goBackTo: '/'
      }
    },
    beforeRouteEnter(to, from, next) {
      to.matched[0].components.default = isNil(from.matched[0]) ? Dashboard : from.matched[0].components.default

      next(vm => {
        if (!vm.canAccessDirect)
          vm.$router.push({
            name: 'dashboard.index'
          })

        vm.fetchRecords()
        vm.goBackTo = from.path
        window.jQuery(vm.$el).modal('show')
        window.jQuery(vm.$el).on('hide.bs.modal', () => {
          vm.$router.push(vm.goBackTo)
        })
      })
    },
    beforeRouteLeave(to, from, next) {
      setTimeout(() => {
        next()
      }, 200)
    },
    methods: {
      fetchRecords() {
        // Do list request
      }
    }
}

An example of my router object: The first route will open a modal on the router-view modal and the second will open only on the default router-view

{
  name: 'leads.quick-add',
  path: '/leads/quick-add',
  components: { modal: QuickAdd },
}, 
{
  name: 'leads.index',
  path: '/leads',
  component: Main,
},

It works great! The problem comes when I access my modal URL (does not matter if it's directly or navigating) and the default component has a child component! The child component get away on that case!

There is attached some screenshots to help you out understand what happens...

Image 1 enter image description here

Image 2 enter image description here

At Image 1 we can 2 components where the number 1 is my default component on my VueRouter and the number 2 is his child!

Ar the Image 2, after clicking on the + Quotation button the modal is loaded and the component number 2 getaway!

Any ideas on how to do it keeping the others components?

Just to be clear I want to do it by routing and no calling my modal manually!

########################## Edit I am trying to do something like that instead of check on beforeRouterEnter method:

{
  name: 'leads.show.quotations.create',
  path: '/leads/:id/quotations/create',
  components: {
    default: Show,
    'default.tab': Quotations,
    modal: Add
  },
  meta: {
    requiresAuth: true
  }
},

Where there is a sub-router-view but it does not work!

Thinking about possibilities I've added this issue on the github repo: https://github.com/vuejs/vue-router/issues/1030

2

There are 2 answers

1
Alykam Burdzaki On

I did this in a project at work. It is quite simple actually, the hard part lies in mixing the jquery that you have there, and maybe the css to position a modal, since it's entry point is the router-view inside your component I recommend using the package portal-vue to move the modal content to the end of the body.

Here is a working exemple: https://codesandbox.io/s/vue-router-modal-j10t2

First create a Modal Component that receives it's child by props:

<template>
  <portal to="modal">
    <div class="modal-wrapper">
      <div class="overlay" @click="$router.back()"></div>
      <div class="modal">
        <!-- you can use v-bind to pass down all props -->
        <component :is="component" v-bind="$attrs"/>
      </div>
    </div>
  </portal>
</template>

<script>
export default {
  name: "Modal",
  props: ["component"],
};
</script>

<style scoped>
.modal-wrapper {
  position: absolute;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
}
.modal {
  position: absolute;
  z-index: 1;
  margin: auto;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  background-color: white;
  padding: 5em;
  border: 1px solid #ccc;
  border-radius: 1em;
  box-shadow: 0 0 1em #00000033;
}

.overlay {
  z-index: 1;
  position: absolute;
  width: 100vw;
  height: 100vh;
  background-color: #00000055;
}
</style>

Then you can use the props in routes to assign the content for a given route:

import Home from "./Home.vue";
import Modal from "./Modal.vue";
import Content from "./Content.vue";

const router = new VueRouter({
  routes: [
    {
      path: "/",
      component: Home,
      children: [
        {
          path: "create",
          component: Modal,
          props: {
            component: Content
          }
        }
      ]
    }
  ]
});

Since the modal is a child route of '/', the Home component needs to render the router-view:

<template>
  <div>
    Hi i'am home
    <router-view />
    <router-link to="/create">open modal</router-link>
  </div>
</template>

And the App.vue needs to render the portal target, so the modal goes to the end of your content (it makes positioning the modal a lot easier):

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" width="25%" />
    <router-view />
    <portal-target name="modal" />
  </div>
</template>

<script>
export default {
  name: "App",
};
</script>

And that's it

0
kriskhoury On

I had a different approach that worked for me.

My use-case: I have a support link in the app header and is accessible throughout the site. Clicking the link I wanted a support contact form modal. I decided to set the to= to have a hash value:

<router-link to="#support" tag="button">Support</router-link>

In the global component where this link resides, I added a watcher:

methods:{
  showSupport(){
    this.support.dialog = true;
  }
},
watch: {
  '$route'(to) {
    if(to.hash == '#support'){
      this.showSupport();
    }
  }
}

My modal was persistent, so it required a manual close of the window and in that function, I went back in router history:

closeSupport(){
  this.support.dialog = false;
  this.$router.go(-1);
}

That seemed to work great for me. The only caveat is if the hash persists in the url (ie. domain.com/page#support), that will not allow Vue Router to fire a change event and the modal won't open again if it was once opened. But in most my cases the hash was removed from the url.

Cheers!