javascriptvue.jspopperjsvue-portal

Creating Vue Component Dropdown with Popper Js


I'm on progress to make a dropdown component with vue and popperjs. To be known i'm using vuejs v.2.6.12 and popperjs v.2.9.2 and here is the code

<template>
        <button type="button" @click="show = true">
            <slot />
            <portal v-if="show" to="dropdown">
                <div>
                    <div
                        style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2"
                        @click="show = false"
                    />
                    <div
                        ref="dropdown"
                        style="position: absolute; z-index: 99999;"
                        @click.stop="show = autoClose ? false : true"
                    >
                        <slot name="dropdown" />
                    </div>
                </div>
            </portal>
        </button>
    </template>
    
    <script>
    import { createPopper } from "@popperjs/core";
    
    export default {
        props: {
            placement: {
                type: String,
                default: "bottom-end"
            },
            boundary: {
                type: String,
                default: "scrollParent"
            },
            autoClose: {
                type: Boolean,
                default: true
            }
        },
        data() {
            return {
                show: false
            };
        },
        watch: {
            show(show) {
                if (show) {
                    this.$nextTick(() => {
                        this.popper = createPopper(this.$el, this.$refs.dropdown, {
                            placement: this.placement,
                            modifiers: [
                                {
                                    name: "preventOverflow",
                                    options: {
                                        boundary: this.boundary
                                    }
                                }
                            ]
                        });
                    });
                } else if (this.popper) {
                    setTimeout(() => this.popper.destroy(), 100);
                }
            }
        },
        mounted() {
            document.addEventListener("keydown", e => {
                if (e.keyCode === 27) {
                    this.show = false;
                }
            });
        }
    };
    </script>

When i'm trying to run the code, i get error message Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element. I don't know why i got this error, i think i've put the reference this.$el and the popper this.$refs.dropdown on the right place.

Could anybody here to help me for solving this problem?
Thank You


Solution

  • I created a snippet that works without the mentioned errors.

    const createPopper = Popper.createPopper
    
    /* import {
      createPopper
    } from "@popperjs/core"; */
    
    Vue.component('DropDown', {
      props: {
        placement: {
          type: String,
          default: "bottom-end"
        },
        boundary: {
          type: String,
          default: "scrollParent"
        },
        autoClose: {
          type: Boolean,
          default: true
        }
      },
      data() {
        return {
          show: false
        };
      },
      watch: {
        show(show) {
          if (show) {
            this.$nextTick(() => {
              this.popper = createPopper(this.$el, this.$refs.dropdown, {
                placement: this.placement,
                modifiers: [{
                  name: "preventOverflow",
                  options: {
                    boundary: this.boundary
                  }
                }]
              });
            });
          } else if (this.popper) {
            setTimeout(() => this.popper.destroy(), 100);
          }
        }
      },
      mounted() {
        document.addEventListener("keydown", e => {
          if (e.keyCode === 27) {
            this.show = false;
          }
        });
      },
      template: `
        <button type="button" @click="show = true">
          <slot />
          <portal
            v-if="show"
            to="dropdown"
          >
            <div>
              <div
                style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2"
                @click="show = false"
              />
              <div
                ref="dropdown"
                style="position: absolute; z-index: 99999;"
                @click.stop="show = autoClose ? false : true"
              >
                <slot name="dropdown" />
              </div>
            </div>
          </portal>
        </button>
      `
    })
    
    new Vue({
      el: "#app",
      template: `
        <div>
          <drop-down>
            <template
              v-slot:default
            >
              Dropdown default slot
            </template>
            <template
              v-slot:dropdown
            >
              This is the dropdown slot content
            </template>
          </drop-down>
          <portal-target
            name="dropdown"
          />
        </div>
      `
    })
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>
    <script src="http://unpkg.com/portal-vue"></script>
    <script src="https://unpkg.com/@popperjs/core@2"></script>
    <div id="app"></div>