Search code examples
vue.jsnuxt.jsvue-router

Nuxt: Open component/page as modal with dedicated url like dribble.com


In my Nuxt application I have a list of properties, and on click I would like to open the corresponding property page (/properties/_slug.vue) as a modal in front of whatever page the user was on.

dribble.com is an exact example of the functionality I'm trying to recreate.

If a property link is clicked on from /, the url should be /properties/slug-of-prop, and when I hit the close button or back button, it should close the modal and return the url to what it was before click. In addition, I'd like to be able to visit localhost:3000/properties/slug-of-prop and have it show me the properties list with the modal in front of it.

My file structure is:

/pages
--index.vue
--/properties
----index.vue
----_slug.vue

I have tried the solution listed here: https://stackoverflow.com/a/60793173/2232797 but, it doesn't remove the modal on hitting the back button. In addition, I've also attempted to implement this vue.js solution here by using middleware to add showModal as a parameter of my route, but it's using router-view I am having a really hard time understanding how it works.

Here is what my /properties/index.vue looks like:

<template>
  <section class="container-fluid">
    <!-- property cards -->
    <div class="info-row scroll-row border-bottom">
      <nuxt-link
        v-for="property in properties"
        :key="property"
        :to="`properties/${property.uid}`"
        tag="div"
      >
        {{ property.uid }}
      </nuxt-link>
    </div>
    <div v-if="showModal" class="modal-route">
      <div class="modal-content">
        <router-view></router-view>
      </div>
    </div>
  </section>
</template>

<script>
import { store } from '@/store/index.js'

export default {
  computed: {
    showModal() {
      return store.showModal
    },
  },
.....

Can anyone please point me in the right direction? I am obviously pretty new at this, so any help is greatly appreciated!


Solution

  • There's an easier way to handle this without all the fuss of middleware or manually messing with the history.

    I've created a CodeSandbox project here, and tried to keep it as similar to your setup as possible: https://codesandbox.io/s/wizardly-knuth-rolrn. Check it out, it has everything I'm about to explain in a working example.

    The first thing you should do is check out Nuxt Child. It's basically router-view, but designed specifically for Nuxt.

    <div class="info-row scroll-row border-bottom"
      @click.prevent.stop
    >
      <nuxt-link
        v-for="property in properties"
        :key="property.uid"
        :to="`/properties/${property.uid}`"
        tag="div"
        style="text-style: underline"
      >{{ property.uid }}</nuxt-link>
    </div>
    <div v-if="showModal" class="modal-route">
      <div class="modal-content">
        <nuxt-child></nuxt-child>
      </div>
    </div>
    

    Couple notes here:

    • I've added @click.prevent.stop to cancel the click event from propagating, which can trigger the cards listeners
    • I've prefixed the to with a /. When missing, the route will just be appended to whatever route is already in the address bar.
    • I've switched your key to the property.uid, since that should be unique.

    One very important thing for nuxt child to work: The folder containing the children elements must be the same name as the parent filename. In this example, I used index.vue and index/. Index.vue is the parent, and everything inside /index is considered a child route that can be rendered.

    This gives you a folder structure like this:

    /pages
      index.vue
      /index
        /properties
          _slug.vue
    

    The next thing that's changed is the showModal method. I'm using this.$route.matched.length to determine if there's a child component.

    computed: {
        showModal() {
          return this.$route.matched.length;
        }
    }
    

    The slug itself is the most complicated piece here, just due to the way that I implemented the example listeners. However, I'm sure you have some other ways of handling this.

    <template>
      <div ref="cardContainer" style="padding: 20px; border: 1px solid black" >I'm a card for property {{this.$route.params.slug }}</div>
    </template>
    
    <script>
    export default {
      mounted() {
        // Attach listeners for handling clicks outside the card, while preventing propagation
        // of clicks in the cards
        this.$refs.cardContainer.addEventListener('click', this.stopPropagation)  
        document.body.addEventListener('click', this.closeModal)
      },
    
      beforeDestroy() {
        // Make sure to cleanup!
        this.$refs.cardContainer.removeEventListener('click', this.stopPropagation)  
        document.body.removeEventListener('click', this.closeModal)
      },
    
      methods: {
        // Prevent clicking inside the card from triggering the closeModal
        stopPropagation(e) {
           e.stopPropagation()
        },
        closeModal() {
          // You can also do this.$router.push('/') to preserve the history
          this.$router.replace('/')
        }
      }
    }
    </script>