How to Make a Popover with Vue

Vue js

Making a popover with Vue is pretty straight froward. It's a great component to make if you're new to Vue and want to learn the basics of making components. They're also a great use case for slots. Slots are a useful feature of Vue that allow you to pass html into components. This post will show you how to make a basic popover component that could be built upon for your use case. If you're already familiar with Vue and just want to see the code you can here.

Even though this is a simple component it demonstrates some important features of Vue: dynamic styles, emitting events, and slots.

Sections


Single file components

The template section

Slots

The script section

The style section

Emitting a custom event

The complete component

Using the popover

Single file components


The most common way to make Vue components is as a single-file component (SFC). This is one of its big differences with Angular and React. It's one of the things I like about working with Vue. A SFC usually consists of three sections: template, script, and style. Below I'll go through each section for our Popover SFC.

The template section


Our markup in the <template> section is pretty simple. We don't really need anything too complex.

<template>
  <div class="popover" :style="position" v-if="show">
    <slot></slot>
  </div>
</template>

There are three attributes attached to the <div>. A normal css class tag and two common vue directives: v-if and :style. The v-if will be used to toggle showing the popover and the :style for binding dynamic styles to our component. In our case will use it to position the component.

Slots


We're also using the special <slot></slot> tag. Slots are a way to distribute content to components.

So when we actually use the component like this:

<Popover>
  <h1>This is a popover!</h1>
</Popover>

whatever markup we put in between the <Popover> tags will be rendered. In this case the <h1> will be rendered in the slots place. If I didn't need the flexibility of rendering different tags I would probably just use a string prop with the text I want to display. Custom components can also be used with slots. That is how I often use them. There's a bunch of stuff you can do with slots like have named slots so you can have more than one slot in a component. and have default content for the slot. Here's the docs for slots for a deeper dive.

The script section


The script section is responsible for much of the functionality. We're keeping it pretty simple but it's a good base to expand upon if you'd like.

<script>
  export default {
    props: {
      show: { type: Boolean, required: true, default: false },
      xCoordinate: { type: Number, required: true },
      yCoordinate: { type: Number, required: true },
    },
    computed: {
      position() {
        return {
          left: `${this.xCoordinate}px`,
          top: `${this.yCoordinate}px`,
        };
      },
    },
  };
</script>

The props property defines the data props we'll pass into the popover. All of the props options: type, required, and default are optional. I always include them though since it indicates what the components expect and will cause the compiler to throw a warning if they're invalid. The show prop will act as a flag to show and hide the component. It's used in the v-if to display the component. We set default: false to hide it by default. We'll use xCoordinate and yCoordinate for the values to position the popover on the page. In the styles section we'll see that we set the components position to absolute which will allow us to position the component we're we want on the page.

The computed property is used to filter, transform or consume data. Usually from data stores, AJAX calls or props. The more components you make you'll find that it is often used. The data in the computed properties is get only (read only) by default and in most cases you don't need to updated them. They will update when the data the are derived from changes. If you do need to update them you will need to define a setter. This is used in more complex use cases though.

We only have one computed property, position which returns an object with a key of left and top. We will then use these as a dynamic styles to position the popover. It's used in the template with the bound style :style=position in the template section. The other styles we will just use as normal in the style section.

The style section


I really like the way styling works with Vue. Using the scoped attribute confines the properties to this component. After making a handful of apps with Vue I've found this to work great in practice. It makes updating styles pretty easy in that you don't have to worry about changing a global style that effects other parts of the application. I usually just end up with a handful of global styles.

Here's our style section:

<style scoped>
  .popover {
    background-color: hsla(0, 0%, 100%, 0.95);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
    padding: 0.5rem;
    position: absolute;
    transition: all 0.3s ease;
    z-index: 999;
  }
</style>

This ia all pretty standard styling. The most important one is position: absolute which in conjunction with the dynamic style positions the popover.

Emitting a custom event


Our component is almost complete but we don't have a way to close the popover. Let's add a custom event @click="$emit('close')" to our template.

<template>
  <div class="popover" :style="position" v-if="show" @click="$emit('close')">
    <slot></slot>
  </div>
</template>

Now when we click the component it will emit a close event. In the parent component we need to define what to do when a close event is fired. So we'll add the action like this: @close="showPopover = false".

Why emit the event at all?

Vue has one way data binding. Emitting an event allows us to communicate with the parent component. Parent communicates to children through props. Children communicate to parents through events. This ensures that your components update smoothly and predictably.

The complete component


<template>
  <div class="popover" :style="position" v-if="show" @click="$emit('close')">
    <slot></slot>
  </div>
</template>
 
<script>
  export default {
    props: {
      show: { type: Boolean, required: true, default: false },
      xCoordinate: { type: Number, required: true },
      yCoordinate: { type: Number, required: true },
    },
    computed: {
      position() {
        return {
          left: `${this.xCoordinate}px`,
          top: `${this.yCoordinate}px`,
        };
      },
    },
    methods: {
      close: function () {
        this.$emit("close");
      },
    },
  };
</script>
<style scoped>
  .popover {
    background-color: hsla(0, 0%, 100%, 0.95);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
    padding: 0.5rem;
    position: absolute;
    transition: all 0.3s ease;
    z-index: 999;
  }
</style>

Using the popover


Let's take a look at how we'd actually use the popover in a simple example. This should be treated as a snippet it might have some code omitted. Take a look at this repo for a full example.

<template>
  <div>
    <h4 @click="displayPopover($event)">click to show popover</h4>
    <Popover
      :xCoordinate="xCoordinate"
      :yCoordinate="yCoordinate"
      :show="showPopover"
      @close="showPopover = false"
    >
      <h3>It's a popover!</h3>
    </Popover>
  </div>
</template>
 
<script>
  import Popover from "./Popover";
 
  export default {
    components: { Popover },
    data: () => ({
      showPopover: false,
      xCoordinate: 0,
      yCoordinate: 0,
    }),
 
    methods: {
      displayPopover(event) {
        this.xCoordinate = event.pageX;
        this.yCoordinate = event.pageY;
        this.showPopover = true;
      },
    },
  };
</script>

To position the popover we're capturing a mouse click event and in displayPopover(event) setting the xCoordinate and yCoordinate with event.pageX and event.pageY.

Going further


I kept this component pretty lean but there are a lot more things that could be added to it. There are many ways we could have positioned it. We could have attached the Popover to an element using the element's id instead of using coordinates or allow for both. Another option would be to define a position component prop that could take the values: top, bottom, left, or right to orientate the popover.

Slots are also a great feature to make use of in your Vuejs components and worth some more time exploring.