Vue.js Modal Portal Demo

by Torwan

HTML

<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/tailwind.min.css">
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<script src="https://unpkg.com/[email protected]/dist/portal-vue.js"></script>
<div id="app" class="font-sans min-h-screen flex items-center justify-center">
  <!-- Imagine this is surrounded... -->
    <new-tweet-button></new-tweet-button>
  <!-- ...by tons of other markup for your site. -->


  <!-- At the very end of your markup to avoid absolute/relative positioning bugs... -->
  <portal-target name="modals"></portal-target>
</div>

CSS

.fade-shrink-in-enter-active,
.fade-shrink-in-leave-active {
  transition: all .3s ease;
}

.fade-shrink-in-enter,
.fade-shrink-in-leave-to {
  opacity: 0;
  transform: scale(1.1);
}

Babel + JSX

Vue.use(PortalVue)

Vue.component('new-tweet-button', {
  template: `
    <button class="bg-blue text-white font-semibold px-4 py-2 rounded hover:bg-blue-dark inline-flex items-center"
      @click="openModal"
    >
        <span>New Tweet</span>

        <portal to="modals">
          <new-tweet-modal :open="open" @close="close"></new-tweet-modal>
        </portal>
    </button>
  `,
  data() {
    return {
      open: false,
    }
  },
  methods: {
    openModal() {
      this.open = true
    },
    close() {
      this.open = false
    }
  }
})

Vue.component('new-tweet-modal', {
  template: `
    <transition name="fade-shrink-in" appear>
      <div v-show="open" @click="close" class="absolute pin p-8" style="background-color: hsla(0, 0%, 0%, .5)">
        <div @click.stop class="max-w-sm w-full mx-auto bg-white rounded p-6 mt-8 z-10 shadow-lg">
          <h1 class="font-normal text-xl text-center mb-6">
            Compose New Tweet
          </h1>
          <div class="mb-6">
            <textarea rows="3" class="text-lg w-full block appearance-none border px-4 py-2 rounded" v-model="tweet" placeholder="What's happening?"></textarea>
          </div>
          <div class="text-right">
              <button type="button" class="text-grey-darker hover:text-black hover:underline mr-6" @click="close">
                Cancel
              </button>
              <button type="button" class="bg-blue text-white font-semibold px-4 py-2 rounded hover:bg-blue-dark">
                Post
              </button>
            </div>
          </div>
        </div>
      </div>
    </transition>
  `,
  props: ['open'],
  data() {
    return {
      tweet: '',
    }
  },
  methods: {
    close() {
      this.tweet = ''
      this.$emit('close')
    },
  },
  created() {
    document.addEventListener('keydown', (e) => {
      if (this.open && e.keyCode == 27) {
        this.close()
      }
    })
  }
})

const app = new Vue({
  el: '#app',
})