Dynamically Switching From One HTML Element to Another in Vue

A friend once contacted me asking if I had a way to dynamically change one HTML element into another within Vue’s template block. For instance, shifting a <div> element to a <span> element based on some criteria. The trick was to do this without relying on a series of v-if and v-else code.

I didn’t think much of it because I couldn’t see a strong reason to do such a thing; it just doesn’t come up that often. Later that same day, though, he reached out again and told me he learned how to change element types. He excitedly pointed out that Vue has a built-in component that can be used as a dynamic element in the very way that he needed.

This small feature can keep code in the template nice and tidy. It can reduce v-if and v-else glut down to a smaller amount of code that’s easier to understand and maintain. This allows us to use methods or computed methods to create nicely-coded, and yet more elaborate, conditions in the script block. That’s where such things belong: in the script, not the template block.

I had the idea for this article mainly because we use this feature in several places in the design system where I work. Granted it’s not a huge feature and it is barely mentioned in the documentation, at least as far as I can tell. Yet it has potential to help render specific HTML elements in components.

Vue’s built-in <component> element

There are several features available in Vue that allow for easy dynamic changes to the view. One such feature, the built-in <component> element, allows components to be dynamic and switched on demand. In both the Vue 2 and the Vue 3 documentation, there is a small note about using this element with HTML elements; that is the part we shall now explore.

The idea is to leverage this aspect of the <component> element to swap out common HTML elements that are somewhat similar in nature; yet with different functionality, semantics, or visuals. The following basic examples will show the potential of this element to help with keeping Vue components neat and tidy.

Buttons and links are often used interchangeably, but there are big differences in their functionality, semantics, and even visuals. Generally speaking, a button (<button>) is intended for an internal action in the current view tied to JavaScript code. A link (<a>), on the other hand, is intended to point to another resource, either on the host server or an external resource; most often web pages. Single page applications tend to rely more on the button than the link, but there is a need for both.

Links are often styled as buttons visually, much like Bootstrap’s .btn class that creates a button-like appearance. With that in mind, we can easily create a component that switches between the two elements based on a single prop. The component will be a button by default, but if an href prop is applied, it will render as a link.

Here is the <component> in the template:

<component
  :is="element"
  :href="href"
  class="my-button"
>
  <slot />
</component>

This bound is attribute points to a computed method named element and the bound href attribute uses the aptly named href prop. This takes advantage of Vue’s normal behavior that the bound attribute does not appear in the rendered HTML element if the prop has no value. The slot provides the inner content regardless whether the final element is a button or a link.

The computed method is simple in nature:

element () {
  return this.href ? 'a' : 'button';
}

If there’s an href prop,. then an <a> element is applied; otherwise we get a <button>.

<my-button>this is a button</my-button>
<my-button href="https://www.css-tricks.com">this is a link</my-button>

The HTML renders as so:

<button class="my-button">this is a button</button>
<a href="https://www.css-tricks.com" class="my-button">this is a link</a>

In this case, there could be an expectation that these two are similar visually, but for semantic and accessibility needs, they are clearly different. That said, there’s no reason the two outputted elements have to be styled the same. You could either use the element with the selector div.my-button in the style block, or create a dynamic class that will change based on the element.

The overall goal is to simplify things by allowing one component to potentially render as two different HTML elements as needed — without v-if or v-else!

Ordered or unordered list?

A similar idea as the button example above, we can have a component that outputs different list elements. Since an unordered list and an ordered list make use of the same list item (<li>) elements as children, then that’s easy enough; we just swap <ul> and <ol>. Even if we wanted to have an option to have a description list, <dl>, this is easily accomplished since the content is just a slot that can accept <li> elements or <dt>/<dd>combinations.

The template code is much the same as the button example:

<component
  :is="element"
  class="my-list"
>
  <slot>No list items!</slot>
</component>

Note the default content inside the slot element, I’ll get to that in a moment.

There is a prop for the type of list to be used that defaults to <ul>:

props: {
  listType: {
    type: String,
    default: 'ul'
  }
}

Again, there is a computed method named element:

element () {
  if (this.$slots.default) {
    return this.listType;
  } else {
    return 'div';
  }
}

In this case, we are testing if the default slot exists, meaning it has content to render. If it does, then the the list type passed through the listType prop is used. Otherwise, the element becomes a <div> which would show the “No list items!” message inside the slot element. This way, if there are no list items, then the HTML won’t render as a list with one item that says there are no items. That last aspect is up to you, though it is nice to consider the semantics of a list with no apparent valid items. Another thing to consider is the potential confusion of accessibility tools suggesting this is a list with one item that just states there are no items.

Just like the button example above, you could also style each list differently. This could be based on selectors that target the element with the class name, ul.my-list. Another option is to dynamically change the class name based on the chosen element.

This example follows a BEM-like class naming structure:

<component
  :is="element"
  class="my-list"
  :class="`my-list__${element}`"
>
  <slot>No list items!</slot>
</component>

Usage is as simple as the previous button example:

<my-list>
  <li>list item 1</li>
</my-list>

<my-list list-type="ol">
  <li>list item 1</li>
</my-list>

<my-list list-type="dl">
  <dt>Item 1</dt>
  <dd>This is item one.</dd>
</my-list>

<my-list></my-list>

Each instance renders the specified list element. The last one, though, results in a <div> stating no list items because, well, there’s no list to show!

One might wonder why create a component that switches between the different list types when it could just be simple HTML. While there could be benefits to keeping lists contained to a component for styling reasons and maintainability, other reasons could be considered. Take, for instance, if some forms of functionality were being tied to the different list types? Maybe consider a sorting of a <ul> list that switches to a <ol> to show sorting order and then switching back when done?

Now we’re controlling the elements

Even though these two examples are essentially changing the root element component, consider deeper into a component. For instance, a title that might need to change from an <h2> to an <h3> based on some criteria.

If you find yourself having to use ternary solutions to control things beyond a few attributes, I would suggest sticking with the v-if. Having to write more code to handle attributes, classes, and properties just complicates the code more than the v-if. In those cases, the v-if makes for simpler code in the long run and simpler code is easier to read and maintain.

When creating a component and there’s a simple v-if to switch between elements, consider this small aspect of a major Vue feature.

Expanding the idea, a flexible card system

Consider all that we’ve covered so far and put it to use in a flexible card component. This example of a card component allows for three different types of cards to be placed in specific parts of the layout of an article:

  • Hero card: This is expected to be used at the top of the page and draw more attention than other cards.
  • Call to action card: This is used as a line of user actions before or within the article.
  • Info card: This is intended for pull quotes.

Consider each of these as following a design system and the component controls the HTML for semantics and styling.

In the example above, you can see the hero card at the top, a line of call-to-action cards next, and then — scrolling down a bit — you’ll see the info card off to the right side.

Here is the template code for the card component:

<component :is="elements('root')" :class="'custom-card custom-card__' + type" @click="rootClickHandler">
  <header class="custom-card__header" :style="bg">
    <component :is="elements('header')" class="custom-card__header-content">
      <slot name="header"></slot>
    </component>
  </header>
  <div class="custom-card__content">
    <slot name="content"></slot>
  </div>
  <footer class="custom-card__footer">
    <component :is="elements('footer')" class="custom-card__footer-content" @click="footerClickHandler">
      <slot name="footer"></slot>
    </component>
  </footer>
</component>

There are three of the “component” elements in the card. Each represents a specific element inside the card, but will be changed based on what kind of card it is. Each component calls the elements() method with a parameter identifying which section of the card is making the call.

The elements() method is:

elements(which) {
  const tags = {
    hero: { root: 'section', header: 'h1', footer: 'date' },
    cta: { root: 'section', header: 'h2', footer: 'div' },
    info: { root: 'aside', header: 'h3', footer: 'small' }
  }
  return tags[this.type][which];
}

There are probably several ways of handing this, but you’ll have to go in the direction that works with your component’s requirements. In this case, there is an object that keeps track of HTML element tags for each section in each card type. Then the method returns the needed HTML element based on the current card type and the parameter passed in.

For the styles, I inserted a class on the root element of the card based on the type of card it is. That makes it easy enough to create the CSS for each type of card based on the requirements. You could also create the CSS based on the HTML elements themselves, but I tend to prefer classes. Future changes to the card component could change the HTML structure and less likely to make changes to the logic creating the class.

The card also supports a background image on the header for the hero card. This is done with a simple computed placed on the header element: bg. This is the computed:

bg() {
  return this.background ? `background-image: url(${this.background})` : null;
}

If an image URL is provided in the background prop, then the computed returns a string for an inline style that applies the image as a background image. A rather simple solution that could easily be made more robust. For instance, it could have support for custom colors, gradients, or default colors in case of no image provided. There’s a large number of possibilities that his example doesn’t approach because each card type could potentially have their own optional props for developers to leverage.

Here’s the hero card from this demo:

<custom-card type="hero" background="https://picsum.photos/id/237/800/200">
  <template v-slot:header>Article Title</template>
  <template v-slot:content>Lorem ipsum...</template>
  <template v-slot:footer>January 1, 2011</template>
</custom-card>

You’ll see that each section of the card has its own slot for content. And, to keep things simple, text is the only thing expected in the slots. The card component handles the needed HTML element based solely on the card type. Having the component just expect text makes using the component rather simplistic in nature. It replaces the need for decisions over HTML structure to be made and in turn the card is simply implemented.

For comparison, here are the other two types being used in the demo:

<custom-card type="cta">
  <template v-slot:header>CTA Title One</template>
  <template v-slot:content>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</template>
  <template v-slot:footer>footer</template>
</custom-card>

<custom-card type="info">
  <template v-slot:header>Here's a Quote</template>
  <template v-slot:content>“Maecenas ... quis.”</template>
  <template v-slot:footer>who said it</template>
</custom-card>

Again, notice that each slot only expects text as each card type generates its own HTML elements as defined by the elements() method. If it’s deemed in the future that a different HTML element should be used, it’s a simple matter of updating the component. Building in features for accessibility is another potential future update. Even interaction features can be expanded, based on card types.

The power is in the component that’s in the component

The oddly named <component> element in Vue components was intended for one thing but, as often happens, it has a small side effect that makes it rather useful in other ways. The <component> element was intended to dynamically switch Vue components inside another component on demand. A basic idea of this could be a tab system to switch between components acting as pages; which is actually demonstrated in the Vue documentation. Yet it supports doing the same thing with HTML elements.

This is an example of a new technique shared by a friend that has become s surprisingly useful tool in the belt of Vue features that I’ve used. I hope that this article carries forward the ideas and information about this small feature for you to explore how you might leverage this in your own Vue projects.


The post Dynamically Switching From One HTML Element to Another in Vue appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

How to Build Vue Components in a WordPress Theme

Intrigued by the title and just wanna see some code? Skip ahead.

A few months ago, I was building a WordPress website that required a form with a bunch of fancy conditional fields. Different options and info were required for different choices you could make on the form, and our client needed complete control over all fields 1. In addition, the form needed to appear in multiple places in each page, with slightly different configs.

And the header instance of the form needed to be mutually exclusive with the hamburger menu, so that opening one closes the other.

And the form had text content that was relevant to SEO.

And we wanted the server response to present some cute animated feedback.

(Phew.)

The whole thing felt complex enough that I didn't want to handle all that state manually. I remembered reading Sarah Drasner’s article "Replacing jQuery With Vue.js: No Build Step Necessary” which shows how to replace classic jQuery patterns with simple Vue micro-apps. That seemed like a good place to start, but I quickly realized that things would get messy on the PHP side of WordPress.

What I really needed were reusable components

PHP → JavaScript

I love the static-first approach of Jamstack tools, like Nuxt, and was looking to do something similar here — send the full content from the server, and progressively enhance on the client side.

But PHP doesn’t have a built-in way to work with components. It does, however, support require-ing files inside other files 2. WordPress has an abstraction of require called get_template_part, that runs relative to the theme folder and is easier to work with. Dividing code into template parts is about the closest thing to components that WordPress provides 3.

Vue, on the other hand, is all about components — but it can only do its thing after the page has loaded and JavaScript is running.

The secret to this marriage of paradigms turns out to be the lesser-known Vue directive inline-template. Its great and wonderful powers allow us to define a Vue component using the markup we already have. It’s the perfect middle ground between getting static HTML from the server, and mounting dynamic DOM elements in the client.

First, the browser gets the HTML, then Vue makes it do stuff. Since the markup is built by WordPress, rather than by Vue in the browser, components can easily use any information that site administrators can edit. And, as opposed to .vue files (which are great for building more app-y things), we can keep the same separation of concerns we use for the whole site — structure and content in PHP, style in CSS, and functionality in JavaScript.

To show how this all fits together, we’re going to build a few features for a recipe blog. First, we’ll add a way for users to rate recipes. Then we’ll build a feedback form based on that rating. Finally, we’ll allow users to filter recipes, based on tags and rating.

We’ll build a few components that share state and live on the same page. To get them to play nicely together — and to make it easy to add additional components in the future — we’ll make the whole page our Vue app, and register components inside it.

Each component will live in its own PHP file and be included in the theme using get_template_part.

Laying the groundwork

There are a few special considerations to take into account when applying Vue to existing pages. The first is that Vue doesn't want you loading scripts inside it — it will send ominous errors to the console if you do. The easiest way to avoid this is to add a wrapper element around the content for every page, then load scripts outside of it (which is already a common pattern for all kinds of reasons). Something like this:

<?php /* header.php */ ?>

<body <?php body_class(); ?>>
<div id="site-wrapper">
<?php /* footer.php */ ?> 

</div> <!-- #site-wrapper -->
<?php wp_footer(); ?>

The second consideration is that Vue has to be called at the end of body element so that it will load after the rest of the DOM is available to parse. We’ll pass true as the fifth argument  (in_footer) for the wp_enqueue_script  function. Also, to make sure Vue is loaded first, we’ll register it as a dependency of the main script.

<?php // functions.php

add_action( 'wp_enqueue_scripts', function() {
  wp_enqueue_script('vue', get_template_directory_uri() . '/assets/js/lib/vue.js', null, null, true); // change to vue.min.js for production
  wp_enqueue_script('main', get_template_directory_uri() . '/assets/js/main.js', 'vue', null, true);

Finally, in the main script, we’ll initialize Vue on the site-wrapper element.

// main.js

new Vue({
  el: document.getElementById('site-wrapper')
})

The star rating component

Our single post template currently looks like this:

<?php /* single-post.php */ ?>

<article class="recipe">
  <?php /* ... post content */ ?>

  <!-- star rating component goes here -->
</article>

We’ll register the star rating component and add some logic to manage it:

// main.js

Vue.component('star-rating', {
  data () {
    return {
      rating: 0
    }
  },
  methods: {
    rate (i) { this.rating = i }
  },
  watch: {
    rating (val) {
      // prevent rating from going out of bounds by checking it to on every change
      if (val < 0) 
        this.rating = 0
      else if (val > 5) 
        this.rating = 5

      // ... some logic to save to localStorage or somewhere else
    }
  }
})

// make sure to initialize Vue after registering all components
new Vue({
  el: document.getElementById('site-wrapper')
})

We’ll write the component template in a separate PHP file. The component will comprise six buttons (one for unrated, 5 with stars). Each button will contain an SVG with either a black or transparent fill.

<?php /* components/star-rating.php */ ?>

<star-rating inline-template>
  <div class="star-rating">
    <p>Rate recipe:</p>
    <button @click="rate(0)">
      <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>
    </button>
    <button v-for="(i in 5) @click="rate(i)">
      <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>
    </button>
  </div>
</star-rating>

As a rule of thumb, I like to give a component’s top element a class name that is identical to that of the component itself. This makes it easy to reason between markup and CSS (e.g. <star-rating> can be thought of as .star-rating).

And now we’ll include it in our page template.

<?php /* single-post.php */ ?>

<article class="recipe">
  <?php /* post content */ ?>

  <?php get_template_part('components/star-rating'); ?>
</article>

All the HTML inside the template is valid and understood by the browser, except for <star-rating>. We can go the extra mile to fix that by using Vue’s is directive:

<div is="star-rating" inline-template>...</div>

Now let’s say that the maximum rating isn’t necessarily 5, but is controllable by the website’s editor using Advanced Custom Fields, a popular WordPress plugin that adds custom fields for pages, posts and other WordPress content. All we need to do is inject that value as a prop of the component that we’ll call maxRating:

<?php // components/star-rating.php

// max_rating is the name of the ACF field
$max_rating = get_field('max_rating');
?>
<div is="star-rating" inline-template :max-rating="<?= $max_rating ?>">
  <div class="star-rating">
    <p>Rate recipe:</p>
    <button @click="rate(0)">
      <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>
    </button>
    <button v-for="(i in maxRating) @click="rate(i)">
      <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>
    </button>
  </div>
</div>

And in our script, let’s register the prop and replace the magic number 5:

// main.js

Vue.component('star-rating', {
  props: {
    maxRating: {
      type: Number,
      default: 5 // highlight
    }
  },
  data () {
    return {
      rating: 0
    }
  },
  methods: {
    rate (i) { this.rating = i }
  },
  watch: {
    rating (val) {
      // prevent rating from going out of bounds by checking it to on every change
      if (val < 0) 
        this.rating = 0
      else if (val > maxRating) 
        this.rating = maxRating

      // ... some logic to save to localStorage or somewhere else
    }
  }
})

In order to save the rating of the specific recipe, we’ll need to pass in the ID of the post. Again, same idea:

<?php // components/star-rating.php

$max_rating = get_field('max_rating');
$recipe_id = get_the_ID();
?>
<div is="star-rating" inline-template :max-rating="<?= $max_rating ?>" recipe-id="<?= $recipe_id ?>">
  <div class="star-rating">
    <p>Rate recipe:</p>
    <button @click="rate(0)">
      <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>
    </button>
    <button v-for="(i in maxRating) @click="rate(i)">
      <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>
    </button>
  </div>
</div>
// main.js

Vue.component('star-rating', {
  props: {
    maxRating: { 
      // Same as before
    },
    recipeId: {
      type: String,
      required: true
    }
  },
  // ...
  watch: {
    rating (val) {
      // Same as before

      // on every change, save to some storage
      // e.g. localStorage or posting to a WP comments endpoint
      someKindOfStorageDefinedElsewhere.save(this.recipeId, this.rating)
    }
  },
  mounted () {
    this.rating = someKindOfStorageDefinedElsewhere.load(this.recipeId)    
  }
})

Now we can include the same component file in the archive page (a loop of posts), without any additional setup:

<?php // archive.php

if (have_posts()): while ( have_posts()): the_post(); ?>
<article class="recipe">
  <?php // Excerpt, featured image, etc. then:
  get_template_part('components/star-rating'); ?>
</article>
<?php endwhile; endif; ?>

The feedback form

The moment a user rates a recipe is a great opportunity to ask for more feedback, so let’s add a little form that appears right after the rating is set.

// main.js

Vue.component('feedback-form', {
  props: {
    recipeId: {
      type: String,
      required: true
    },
    show: { type: Boolean, default: false }
  },
  data () {
    return {
      name: '',
      subject: ''
      // ... other form fields
    }
  }
})
<?php // components/feedback-form.php

$recipe_id = get_the_ID();
?>
<div is="feedback-form" inline-template recipe-id="<?= $recipe_id ?>" v-if="showForm(recipe-id)">
  <form class="recipe-feedback-form" id="feedback-form-<?= $recipe_id ?>">
    <input type="text" :id="first-name-<?= $recipe_id ?>" v-model="name">
    <label for="first-name-<?= $recipe_id ?>">Your name</label>
    <?php /* ... */ ?>
  </form>
</div>

Notice that we’re appending a unique string (in this case, recipe-id) to each form element’s ID. This is to make sure they all have unique IDs, even if there are multiple copies of the form on the page.

So, where do we want this form to live? It needs to know the recipe’s rating so it knows it needs to open. We’re just building good ol’ components, so let’s use composition to place the form inside the <star-rating>:

<?php // components/star-rating.php

$max_rating = get_field('max_rating');
$recipe_id = get_the_ID();
?>
<div is="star-rating" inline-template :max-rating="<?= $max_rating ?>" recipe-id="<?= $recipe_id ?>">
  <div class="star-rating">
    <p>Rate recipe:</p>
    <button @click="rate(0)">
      <svg><path d="..." :fill="rating === 0 ? 'black' : 'transparent'"></svg>
    </button>
    <button v-for="(i in maxRating) @click="rate(i)">
      <svg><path d="..." :fill="rating >= i ? 'black' : 'transparent'"></svg>
    </button>
    <?php get_template_part('components/feedback-form'); ?>
  </div>
</div>

If at this point you’re thinking, “We really should be composing both components into a single parent component that handles the rating state,” then please give yourself 10 points and wait patiently.

A small progressive enhancement we can add to make the form usable without JavaScript, is to give it the traditional PHP action and then override it in Vue. We’ll use @submit.prevent to prevent the original action, then run a submit method to send the form data in JavaScript.

<?php // components/feedback-form.php

$recipe_id = get_the_ID();
?>
<div is="feedback-form" inline-template recipe-id="<?= $recipe_id ?>">
  <form action="path/to/feedback-form-handler.php" 
      @submit.prevent="submit"
      class="recipe-feedback-form" 
      id="feedback-form-<?= $recipe_id ?>">
    <input type="text" :id="first-name-<?= $recipe_id ?>" v-model="name">
    <label for="first-name-<?= $recipe_id ?>">Your name</label>
   <!-- ... -->
  </form>
</div>

Then, assuming we want to use fetch, our submit method can be something like this:

// main.js

Vue.component('feedback-form', {
  // Same as before

  methods: {
    submit () {
      const form = this.$el.querySelector('form')
      const URL = form.action
      const formData = new FormData(form)
      fetch(URL, {method: 'POST', body: formData})
        .then(result => { ... })
        .catch(error => { ... })
    }
  }
})

OK, so what do we want to do in .then and .catch? Let’s add a component that will show real-time feedback for the form’s submit status. First let’s add the state to track sending, success, and failure, and a computed property telling us if we’re pending results.

// main.js

Vue.component('feedback-form', {
  // Same as before

  data () {
    return {
      name: '',
      subject: ''
      // ... other form fields
      sent: false,
      success: false,
​​      error: null
    }
  },
  methods: {
    submit () {
      const form = this.$el.querySelector('form')
      const URL = form.action
      const formData = new FormData(form)
      fetch(URL, {method: 'POST', body: formData})
        .then(result => { 
          this.success = true
         })
        .catch(error => { 
          this.error = error
         })
      this.sent = true
    }
  }
})

To add the markup for each message type (success, failure, pending), we could make another component like the others we’ve built so far. But since these messages are meaningless when the server renders the page, we’re better off rendering them only when necessary. To do this we’re going to place our markup in a native HTML <template> tag, which doesn't render anything in the browser. Then we’ll reference it by id as our component’s template.

<?php /* components/form-status.php */ ?>

<template id="form-status-component" v-if="false">
  <div class="form-message-wrapper">
    <div class="pending-message" v-if="pending">
      <img src="<?= get_template_directory_uri() ?>/spinner.gif">
      <p>Patience, young one.</p>
    </div>
    <div class="success-message" v-else-if="success">
      <img src="<?= get_template_directory_uri() ?>/beer.gif">
      <p>Huzzah!</p>
    </div>
    <div class="success-message" v-else-if="error">
      <img src="<?= get_template_directory_uri() ?>/broken.gif">
      <p>Ooh, boy. It would appear that: {{ error.text }}</p>
    </div>
  </div
</template>

Why add v-if="false" at the top, you ask? It’s a tricky little thing. Once Vue picks up the HTML <template>, it will immediately think of it as a Vue <template> and render it. Unless, you guessed it, we tell Vue not to render it. A bit of a hack, but there you have it.

Since we only need this markup once on the page, we’ll include the PHP component in the footer.

<?php /* footer.php */ ?>

</div> <!-- #site-wrapper -->
<?php get_template_part('components/form-status'); ?>
<?php wp_footer(); ?>

Now we’ll register the component with Vue…

// main.js

Vue.component('form-status', {
  template: '#form-status-component'
  props: {
    pending: { type: Boolean, required: true },
    success: { type: Boolean, required: true },
    error: { type: [Object, null], required: true },
  }
})

…and call it inside our form component:

<?php // components/feedback-form.php

$recipe_id = get_the_ID();
?>
<div is="feedback-form" inline-template recipe-id="<?= $recipe_id ?>">
  <form action="path/to/feedback-form-handler.php" 
        @submit.prevent="submit"
        class="recipe-feedback-form" 
        id="feedback-form-<?= $recipe_id ?>">
    <input type="text" :id="first-name-<?= $recipe_id ?>" v-model="name">
    <label for="first-name-<?= $recipe_id ?>">Your name</label>
    <?php // ... ?>
  </form>
  <form-status v-if="sent" :pending="pending" :success="success" :error="error" />
</div>

Since we registered <form-status> using Vue.component, it's available globally, without specifically including it in the parent’s components: { }.

Filtering recipes

Now that users can personalize some bits of their experience on our blog, we can add all kinds of useful functionality. Specifically, let's allow users to set a minimum rating they want to see, using an input at the top of the page.
The first thing we need is some global state to track the minimum rating set by the user. Since we started off by initializing a Vue app on the whole page, global state will just be data on the Vue instance:

// main.js
// Same as before

new Vue({
  el: document.getElementById('site-wrapper'),
  data: {
    minimumRating: 0
  }
})

And where can we put the controls to change this? Since the whole page is the app, the answer is almost anywhere. For instance, at the top of the archive page:

<?php /* archive.php */ ?>

<label for="minimum-rating-input">Only show me recipes I've rated at or above:</label>
<input type="number" id="minimum-rating-input" v-model="minimumRating">

<?php if (have_posts()): while ( have_posts()): the_post(); ?>
<article class="recipe">
  <?php /* Post excerpt, featured image, etc. */ ?>

  <?php get_template_part('components/star-rating'); ?>
</article>
<?php endwhile; endif; ?>

As long as it’s inside our site-wrapper and not inside another component, it’ll just work. If we want, we could also build a filtering component that would change the global state. And if we wanted to get all fancy, we could even add Vuex to the mix (since Vuex can’t persist state between pages by default, we could add something like vuex-persist to use localStorage).

So, now we need to hide or show a recipe based on the filter. To do this, we’ll need to wrap the recipe content in its own component, with a v-show directive. It’s probably best to use the same component for both the single page and the archive page. Unfortunately, neither require nor get_template_part can pass parameters into the called file — but we can use global variables:

<?php /* archive.php */ ?>

<label for="minimum-rating-input">Only show me recipes I've rated at or above:</label>
<input type="number" id="minimum-rating-input" v-model="minimumRating">

<?php 
$is_archive_item = true;
if (have_posts()): while ( have_posts()): the_post();
  get_template_part('components/recipe-content');
endwhile; endif; ?>

We can then use $is_archive_item as a global variable inside the PHP component file to check if it is set and true. Since we won’t need to hide the content on the single post page, we’ll conditionally add the v-show directive.

<?php  // components/recipe-content.php

global $is_archive_item; ?>
<div is="recipe-content">
  <article class="recipe" 
    <?php if ($is_archive_item): ?>
       v-show="show"
    <?php endif; ?>
  >
    <?php
    if ($is_archive_item):
      the_excerpt();
    else
      the_content();
    endif;
    
    get_template_part('components/star-rating');
    ?>
  </article>
</div>

In this specific example, we could have also tested with  is_archive() inside the component, but in most cases we’ll need to set explicit props.

We’ll need to move the rating state and logic up into the <recipe-content> component so it can know if it needs to hide itself. Inside <star-rating>, we’ll make a custom v-model by replacing rating with value, and this.rating = i with  $emit('input', i) as well . So our component registration will now look like this:

// main.js

Vue.component('recipe-content', {
  data () {
    rating: 0
  },
  watch: {
    rating (val) {
      // ...
    }
  },
  mounted () {
    this.rating = someKindOfStorageDefinedElsewhere.load(this.recipeId)    
  }
})

Vue.component('star-rating', {
  props: {
    maxRating: { /* ... */ },
    recipeId: { /* ... */ },
    value: { type: Number, required: true }
  },
  methods: {
    rate (i) { this.$emit('input', i) }
  },
})

We’ll add v-model in star-rating.php and change rating to value. In addition, we can now move the <feedback-form> up into <recipe-content>:

<?php // components/star-rating.php

$max_rating = get_field('max_rating');
$recipe_id = get_the_ID();
?>
<div is="star-rating" 
  inline-template 
  :max-rating="<?= $ max_rating ?>" 
  recipe-id="<?= $recipe_id ?>" 
  v-model="value"
>
  <div class="star-rating">
    <p>Rate recipe:</p>
    <button @click="rate(0)">
      <svg><path d="..." :fill="value === 0 ? 'black' : 'transparent'"></svg>
    </button>
    <button v-for="(i in maxRating) @click="rate(i)">
      <svg><path d="..." :fill="value >= i ? 'black' : 'transparent'"></svg>
    </button>
  </div>
</div>
<?php // components/recipe-content.php

global $is_archive_item; ?>
<div is="recipe-content">
  <article class="recipe" 
    <?php if ($is_archive_item): ?>
       v-show="show"
    <?php endif; ?>
  >
    
    <?php
    if ($is_archive_item):
      the_excerpt();
    else
      the_content();
    endif;
    
    get_template_part('components/star-rating');
    get_template_part('components/feedback-form');
    ?>
  </article>
</div>

Now everything is set up so the initial render shows all recipes, and then the user can filter them based on their rating. Moving forward, we could add all kinds of parameters to filter content. And it doesn’t have to be based on user input — we can allow filtering based on the content itself (e.g. number of ingredients or cooking time) by passing the data from PHP to Vue.

Conclusion

Well, that was a bit of a long ride, but look at what we’ve built: independent, composable, maintainable, interactive, progressively enhanced components in our WordPress theme. We brought together the best of all worlds!

I’ve been using this approach in production for a while now, and I love the way it allows me to reason about the different parts of my themes. I hope I’ve inspired you to try it out too.


  1. Of course, two days before launch, the client’s legal department decided they don't want to collect all that info. Currently the live form is but a shadow of its development self.
  2. Fun fact: Rasmus Lerdorf said that his original intent was for PHP to be templating only, with all business logic handled in C. Let that sink in for a moment. Then clear an hour from your schedule and watch the whole talk.
  3. There are third-party WordPress templating engines that can compile down to optimized PHP. Twig, for example, comes to mind. We’re trying to go the reverse route and send vanilla PHP to be handled by JavaScript.

The post How to Build Vue Components in a WordPress Theme appeared first on CSS-Tricks.

Moving from Vanilla JavaScript to a Reusable Vue Component

I recently wrote an article explaining how you can create a countdown timer using HTML, CSS and JavaScript. Now, let’s look at how we can make that a reusable component by porting it into Vue using basic features that the framework provides.

Why do this at all? Well there are few reasons, but two stand out in particular:

  • Keeping UI in sync with the timer state: If you look at the code from the first post,  it all lives in the timerInterval function, most noticeably the state management. Each time it runs (every second) we need to manually find the proper element on our document — whether it’s the time label or the remaining time path or whatever — and change either its value or an attribute. Vue comes with an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying Vue instance’s data. That takes all the burden of finding and updating proper UI elements so we can rely purely on the component instance’s properties.
  • Having a highly reusable component: The original example works fine when only one timer is present on our document, but imagine that you want to add another one. Oops! We rely the element’s ID to perform our actions and using the same ID on multiple instances would prevent them from working independently. That means we would have to assign different IDs for each timer. If we create a Vue component, all it’s logic is encapsulated and connected to that specific instance of the component. We can easily create 10, 20, 1,000 timers on a single document without changing a single line in the component itself!

Here’s the same timer we created together in the last post, but in Vue.

Template and styles

From the Vue docs:

Vue uses an HTML-based template syntax that allows you to declaratively bind the rendered DOM to the underlying Vue instance’s data. All Vue.js templates are valid HTML that can be parsed by spec-compliant browsers and HTML parsers.

Let’s create our component by opening a new file called BaseTimer.vue. Here’s the basic structure we need for that:

// Our template markup will go here
<template>
// ...
</template>

// Our functional scripts will go here
<script>
// ...
</script>

// Our styling will go here
<style>
// ...
</style>

In this step, we will concentrate on the <template> and <style> sections. Let’s move our timer template to the <template> section and all our CSS to <style> section. The markup mostly consists of SVG and we can use the exact same code we used from the first article.

<template>
  // The wrapper for the timer
  <div class="base-timer">

    // This all comes from the first article
    <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="base-timer__circle">
        <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
        <path
          id="base-timer-path-remaining"
          stroke-dasharray="283"
          class="base-timer__path-remaining ${remainingPathColor}"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>

    // The label showing the remaining time
    <span
      id="base-timer-label"
      class="base-timer__label"
    >
      ${formatTime(timeLeft)}
    </span>

  </div>
</template>

// "scoped" means these styles will not leak out to other elements on the page
<style scoped>
.base-timer {
  position: relative;
  width: 100px;
  height: 100px;
}
</style>

Let’s have a look at the template we just copied to identify where we can use our framework. There are few parts that are responsible for making our timer count down the time and show the remaining time.

  • stroke-dasharray: A value passed to the SVG <path> element that is responsible for holding the remaining time.
  • remainingPathColor: A CSS class responsible for changing the color of the timer’s circular ring, giving is a way to visually indicate that time is running out.
  • formatTime(timeLeft): A value responsible for showing how much time is left inside the timer

We can control our timer by manipulating those values.

Constants and variables

OK, let’s go down to our <script> section and see what Vue gives us out of the box to make our life easier. One thing it lets us do is define our constants up front, which keeps them scoped to the component.

In the last post, we spent a little time tweaking the stroke-dasharray  value to make sure the animation of the timer’s top layer (the ring that animates and changes color as time progresses) is perfectly in line with its bottom layer (the gray ring that indicates past time). We also defined “thresholds” for when the top layer should change colors (orange at 10 remaining seconds and red at five seconds). We also created constants for those colors.

We can move all of those directly into the <script> section:

<script>
// A value we had to play with a bit to get right
const FULL_DASH_ARRAY = 283;
// When the timer should change from green to orange
const WARNING_THRESHOLD = 10;
// When the timer should change from orange to red
const ALERT_THRESHOLD = 5;

// The actual colors to use at the info, warning and alert threshholds
const COLOR_CODES = {
  info: {
    color: "green"
  },
  warning: {
    color: "orange",
    threshold: WARNING_THRESHOLD
  },
  alert: {
    color: "red",
    threshold: ALERT_THRESHOLD
  }
};

// The timer's starting point
const TIME_LIMIT = 20;
</script>

Now, let’s have a look at our variables:

let timePassed = 0;
let timeLeft = TIME_LIMIT;
let timerInterval = null;
let remainingPathColor = COLOR_CODES.info.color;

We can identify two different types of variables here:

  1. Variables in which the values are directly re-assigned in our methods:
    • timerInterval: Changes when we start or stop the timer
    • timePassed: Changes each second when the timer is running
  2. Variables in which the values change when other variables change:
    • timeLeft: Changes when the value of timePassed changes
    • remainingPathColor: Changes when the value of timeLeft breaches the specified threshold

It is essential to identify that difference between those two types as it allows us to use different features of the framework. Let’s go through each of the type separately.

Variables in which values are directly re-assigned

Let’s think what we want to happen when we change the timePassed value. We want to calculate how much time is left, check if we should change the top ring’s color, and trigger re-render on a part of our view with new values. 

Vue comes with its own reactivity system that updates the view to match the new values of specific properties. To add a property to Vue’s reactivity system we need to declare that property on a data object in our component. By doing that,Vue will create a getter and a setter for each property that will track changes in that property and respond accordingly.

<script>
// Same as before

export default {
  data() {
    return {
      timePassed: 0,
      timerInterval: null
    };
  }
</script>

There are two important things we need to remember.

  1. We need to declare all reactive variables in our data object up front. That means if we know that a variable will exist but we don’t know what the value will be, we still need to declare it with some value. If we forgot to declare it in data it will not be reactive, even if it is added later.
  2. When declaring our data option object, we always need to return a new object instance (using return). This is vital because, if we don’t follow this rule, the declared properties will be shared between all instances of the component.

You can see that second issue in action:

Variables in which values change when other variable change

These variables rely on the value of another variable. For example, timeLeft relies purely on timePassed. In our original example that uses vanilla JavaScript, we were calculating that value in the interval that was responsible for changing the value of timePassed. With Vue, we can extract that value to a computed property.

computed property is a function that returns a value. These values are bound to the dependency values and only update when required. Even more importantly, computed properties are cached, meaning they remember the values that the computed property depends on and calculate the new value only if that dependent property value changed. If the value does not change, the previously cached value is returned.

<script>
// Same as before

computed: {
    timeLeft() {
      return TIME_LIMIT - this.timePassed;
    }
  }
}
</script>

The function passed to the computed property must be a pure function. It can’t cause any side effects and must return a value. Also, the output value must only be dependent on the values passed into the function.

Now, we can move more logic to computed properties:

  • circleDasharray: This returns a value previously that is calculated in the setCircleDasharray method.
  • formattedTimeLeft: This returns a value from the formatTime method.
  • timeFraction: This is an abstraction of the calculateTimeFraction method.
  • remainingPathColor: This is an abstraction of the setRemainingPathColor method.
<script>
// Same as before

computed: {
    circleDasharray() {
      return `${(this.timeFraction * FULL_DASH_ARRAY).toFixed(0)} 283`;
    },

    formattedTimeLeft() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      let seconds = timeLeft % 60;
      if (seconds < 10) {
        seconds = `0${seconds}`;
      }
      return `${minutes}:${seconds}`;
    },

    timeLeft() {
      return TIME_LIMIT - this.timePassed;
    },

    timeFraction() {
      const rawTimeFraction = this.timeLeft / TIME_LIMIT;
      return rawTimeFraction - (1 / TIME_LIMIT) * (1 - rawTimeFraction);
    },

    remainingPathColor() {
      const { alert, warning, info } = COLOR_CODES;
      if (this.timeLeft <= alert.threshold) {
        return alert.color;
      } else if (this.timeLeft <= warning.threshold) {
        return warning.color;
      } else {
        return info.color;
      }
    }
  }
</script>

We now have all the values we need! But now we need to put them to use in our template.

Using data and computed properties in the template

Here’s where we left off with our template:


<template>
  <div class="base-timer">
    <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="base-timer__circle">
        <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
        <path
          id="base-timer-path-remaining"
          stroke-dasharray="283"
          class="base-timer__path-remaining ${remainingPathColor}"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    <span
      id="base-timer-label"
      class="base-timer__label"
    >
        ${formatTime(timeLeft)}
    </span>
  </div>
</template>

Let’s start with formatTime(timeLeft). How we can dynamically bind the rendered value to our formattedTimeLeftcomputed property?

Vue uses HTML-based template syntax that allowsus to declaratively bind the rendered DOM to the underlying data of the Vue instance. That means all properties are available in the template section. To render any of them, we use text interpolation using the “Mustache” syntax (double curly braces, or {{ }}).

<span
  id="base-timer-label"
  class="base-timer__label"
>
  {{ formattedTimeLeft }} 
</span>

Next will be stroke-dasharray. We can see we don’t want to render that value. Instead, we want to change the value of the <path> attribute. Mustache cannot be used inside HTML attributes, but fear not! Vue comes with another way: the v-bind directive. We can bind a value to an attribute like this:

<path  v-bind:stroke-dasharray="circleDasharray"></path>

To facilitate the usage of that directive, we can also use a shorthand.

<path  :stroke-dasharray="circleDasharray"></path>

The last one is remainingPathColor, which adds a proper class to an element. We can do that using the same v-bind directive as above, but assign the value to the class attribute of an element.

<path  :class="remainingPathColor"></path>

Let’s have a look at our template after changes.

<template>
  <div class="base-timer">
    <svg class="base-timer__svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="base-timer__circle">
        <circle class="base-timer__path-elapsed" cx="50" cy="50" r="45"></circle>
        <path
          :stroke-dasharray="circleDasharray"
          class="base-timer__path-remaining"
          :class="remainingPathColor"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    <span class="base-timer__label">{{ formattedTimeLeft }}</span>
  </div>
</template>

We have our template ready, we moved all variables to data or computed, and we got rid off most of the methods by creating corresponding computed properties. We are still missing one vital part, though: we need to start our timer.

Methods and component lifecycle hooks

If we look at our startTimer method, we can see that all the calculations, changes in attributes, etc. happen in the interval.

function startTimer() {
  timerInterval = setInterval(() => {
    timePassed = timePassed += 1;
    timeLeft = TIME_LIMIT - timePassed;
    document.getElementById("base-timer-label").innerHTML = formatTime(
      timeLeft
    );
    setCircleDasharray();
    setRemainingPathColor(timeLeft);
    if (timeLeft === 0) {
      onTimesUp();
    }
  }, 1000);
}

Since we’ve already moved all that logic into the computed property, all we need to do in our timerInterval is change the value of timePassed — the rest will happen magically in the computed properties

<script>
// Same as before

methods: {
  startTimer() {
    this.timerInterval = setInterval(() => (this.timePassed += 1), 1000);
  }
}
</script>

We have the method ready, but we still don’t call it anywhere. Each Vue component comes with a series of hooks that allows us to run a specific logic within a specific period of the component’s lifecycle. These are called lifecycle hooks. In our case, as we want to call our method immediately when the component gets loaded. That makes mounted the lifecycle hook what we want.

<script>
// Same as before

mounted() {
  this.startTimer();
},

// Same methods as before
</script> 

That’s it, we just turned our timer into a consistent and reusable component using Vue!

Let's say we now want to use this component in another component. That requires a few things:

  1. First, we import the component.
  2. Next, we register the component.
  3. Finally, we instantiate the component in the template.
// App.vue

import BaseTimer from "./components/BaseTimer"

export default {
  components: {
    BaseTimer
  }
};

That’s a wrap!

This example shows how we can move a component from vanilla JavaScript to a component-based front-end framework, like Vue. 

We can now treat the timer as a standalone component where all the markup, logic and styling is contained in a way that won’t leak out to or conflict with other elements. Components are often children of a larger parent component that assembles multiple components together — like a form or perhaps a card — where the parent’s properties can be accessed and shared. Here’s an example of the timer component where it’s taking orders from a parent component

I hope I got you interested in Vue and the power of components! I’d encourage you to go to Vue docs to get more detailed description of the features we used in our example. There’s so much Vue can do!

The post Moving from Vanilla JavaScript to a Reusable Vue Component appeared first on CSS-Tricks.

Reusable Popovers to Add a Little Pop

A popover is a transient view that shows up on top of a content on the screen when a user clicks on a control button or within a defined area. For example, clicking on an info icon on a specific list item to get the item details. Typically, a popover includes an arrow pointing to the location from which it emerged.

Popovers are great for situations when we want to show a temporary context to get user’s attention when interacting with a specific element on the screen. They provide additional context and instruction for users without having to clutter up a screen. Users can simply close them by clicking the same way they were opened or outside the popover.

We’re going to look at a library called popper.js that allows us to create reusable popover components in the Vue framework. Popovers are the perfect type of component for a component-based system like Vue because they can be contained, encapsulated components that are maintained on their own, but used anywhere throughout an app.

Let’s dig in and get started.

But first: What’s the difference between a popover and tooltip?

Was the name "popover" throwing you for a loop? The truth is that popovers are a lot like tooltips, which are another common UI pattern for displaying additional context in a contained element. There are differences between them, though, so let’s briefly spell them out so we have a solid handle on what we’re building.

Tooltips Popovers
Tooltips are meant to be exactly that, a hint or tip on what a tool or other interaction does. They are meant to clarify or help you use the content that they hover over, not add additional content. Popovers, on the other hand, can be much more verbose, they can include a header and many lines of text in the body.
Tooltips are typically only visible on hover, for that reason if you need to be able to read the content while interacting with other parts of the page then a tooltip will not work. Popovers are typically dismissible, whether by click on other parts of the page or second clicking the popover target (depending on implementation), for that reason you can set up a popover to allow you to interact with other elements on the page while still being able to read it's content.

Popovers are most appropriate on larger screens and we’re most likely to encounter them in use cases such as:

Looking at those use cases, we can glean some requirements that make a good popover:

  1. Reusability: A popover should allow to pass a custom content to the popover.
  2. Dismissibility: A popover should be dismissible by clicking outside of the popover and escape button.
  3. Positioning: A popover should reposition itself when the screen edge is reached.
  4. Interaction: A popover should allow to interact with the content in the popover.

I created an example to refer to as we go through the process of creating a component.

View Demo

OK, now that we’ve got a baseline understanding of popovers and what we’re building, let’s get into the step-by-step details for creating them using popper.js.

Step 1: Create the BasePopover component

Let’s start by creating a component that will be responsible for initializing and positioning the popover. We’ll call this component BasePopover.vue and, in the component template, we’ll render two elements:

  • Popover content: This is the element that will be responsible for rendering the content within the popover. For now we use a slot that will allow us to pass the content from the parent component responsible for rendering our popover (Requirement #1: Reusability).
  • Popover overlay: This is the element responsible for covering the content under the popover and preventing user from interacting with the elements outside the popover. It also allows us to close the popover when clicked (Requirement #2: Dismissibility).
// BasePopover.vue
<template>
  <div>
    <div
      ref="basePopoverContent"
      class="base-popover"
    >
      <slot />
    </div>
    <div
      ref="basePopoverOverlay"
      class="base-popover__overlay"
    />
  </div>
</template>

In the script section of the component:

  • we import popper.js (the library that takes care of the popover positioning), then
  • we receive the popoverOptions props, and finally
  • we set initial popperInstance to null (because initially we do not have any popover).

Let’s describe what the popoverOptions object contains:

  • popoverReference: This is an object in relation to which the popover will be positioned (usually element that triggers the popover).
  • placement: This is a popper.js placement option that specifies the where the popover is displayed in relation to the popover reference element (the thing it is attached to)
  • offset: This is a popper.js offset modifier that allows us to adjust popover position by passing x- and y-coordinates.
import Popper from "popper.js"

export default {
  name: "BasePopover",

  props: {
    popoverOptions: {
      type: Object,
      required: true
    }
  },

  data() {
    return {
      popperInstance: null
    }
  }
}

Why do we need that? The popper.js library allows us to position the element in relation to another element with ease. It also does the magic when the popover gets to the edge of the screen an reposition it to be always in user’s viewport (Requirement #3: Positioning)

Step 2: Initialize popper.js

Now that we have a BasePopover component skeleton, we will add few methods that will be responsible for positioning and showing the popover.

In the initPopper method, we will start by creating a modifiers object that will be used to create a Popper instance. We set the options received from the parent component (placement and offset) to the corresponding fields in the modifiers object. All those fields are optional, which is why we first need to check for their existence.

Then, we initialize a new Popper instance by passing:

  • the popoverReference node (the element to which the popover is pointing: popoverReference ref)
  • the popper content node (the element containing the popover content: basePopoverContent ref)
  • the options object

We also set the preventOverflow option to prevent the popover from being positioned outside of the viewport. After initialization we set the popper instance to our popperInstance data property to have access to methods and properties provided by popper.js in the future.

methods: {
...
  initPopper() {
    const modifiers = {}
    const { popoverReference, offset, placement } = this.popoverOptions
  
    if (offset) {
      modifiers.offset = {
        offset
      }
    }
  
    if (placement) {
      modifiers.placement = placement
    }
  
    this.popperInstance = new Popper(
      popoverReference,
      this.$refs.basePopoverContent,
      {
        placement,
        modifiers: {
          ...modifiers,
          preventOverflow: {
            boundariesElement: "viewport"
          }
        }
      }
    )
  }
...
}

Now that we have our initPopper method ready, we need a place to invoke it. The best place for that is in the mounted lifecycle hook.

mounted() {
  this.initPopper()
  this.updateOverlayPosition()
}

As you can see, we are calling one more method in the mounted hook: the updateOverlayPosition method. This method is a safeguard used to reposition our overlay in case we have any other elements on the page that have absolute positioning (e.g. NavBar, SideBar). The method is making sure the overlay is always covering the full screen and prevent user from interacting with any element except the popover and overlay itself.

methods: {
...
  updateOverlayPosition() {
    const overlayElement = this.$refs.basePopoverOverlay;
    const overlayPosition = overlayElement.getBoundingClientRect();
  
    overlayElement.style.transform = <code>translate(-${overlayPosition.x}px, -${
      overlayPosition.y
    }px)`;
  }
...
}

Step 3: Destroy Popper

We have our popper initialized but now we need a way to remove and dispose it when it gets closed. There’s no need to have it in the DOM at that point.

We want to close the popover when we click anywhere outside of it. We can do that by adding a click listener to the overlay because we made sure that the overlay is always covering the whole screen under our popover

<template>
...
  <div
    ref="basePopoverOverlay"
    class="base-popover__overlay"
    @click.stop="destroyPopover"
  />
...
</template>

Let’s create a method responsible for destroying the popover. In that method we first check if the popperInstance actually exist and if it does we call popper destroy method that makes sure the popper instance is destroyed. After that we clean our popperInstance data property by setting it to null and emit a closePopover event that will be handled in the component responsible for rendering the popover.

methods: {
...
  destroyPopover() {
      if (this.popperInstance) {
        this.popperInstance.destroy();
        this.popperInstance = null;
        this.$emit("closePopover");
      }
    }
...
}

Step 4: Render BasePopover component

OK, we have our popover ready to be rendered. We do that in our parent component, which will be responsible for managing the visibility of the popover and passing the content to it.

In the template, we need to have an element responsible for triggering our popover (popoverReference) and the BasePopover component. The BasePopover component receives a popoverOptions property that will tell the component how we want to display it and isPopoverVisible property bound to v-if directive that will be responsible for showing and hiding the popover.

<template>
  <div>
    <img
      ref="popoverReference"
      width="25%"
      src="./assets/logo.png"
    >
    <BasePopover
      v-if="isPopoverVisible"
      :popover-options="popoverOptions"
    >
      <div class="custom-content">
        <img width="25%" src="./assets/logo.png">
        Vue is Awesome!
      </div>
    </BasePopover>
  </div>
</template>

In the script section of the component, we import our BasePopover component, set the isPopoverVisible flag initially to false and popoverOptions object that will be used to configure popover on init.

data() {
  return {
    isPopoverVisible: false,
    popoverOptions: {
      popoverReference: null,
      placement: "top",
      offset: "0,0"
    }
  };
}

We set popoverReference property to null initially because the element that will be the popover trigger does not exist when our parent component is created. We get that fixed in the mounted lifecycle hook when the component (and the popover reference) gets rendered.

mounted() {
  this.popoverOptions.popoverReference = this.$refs.popoverReference;
}

Now let’s create two methods, openPopover and closePopover that will be responsible for showing and hiding our popover by setting proper value on the isPopoverVisible property.

methods: {
  closePopover() {
    this.isPopoverVisible = false;
  },
  openPopover() {
    this.isPopoverVisible = true;
  }
}

The last thing we need to do in this step is to attach those methods to appropriate elements in our template. We attach the openPopover method to click event on our trigger element and closePopover method to closePopover event emitted from the BasePopover component when the popover gets destroyed by clicking on the popover overlay.

<template>
  <div>
    <img
      ...
      @click="openPopover"
    >
    <BasePopover
      ...
      @closePopover="closePopover"
    >
      ...
    </BasePopover>
  </div>
</template>

Having this in place, we have our popover showing up when we click on the trigger element and disappearing when we click outside of the popover.

Step 5: Create BasePopoverContent component

It does not look like a popover though. Sure, it renders content passed to the BasePopover component, but it does so without the usual popover wrapper and an arrow pointing to the trigger element. We could have included the wrapper component in the BasePopover component, but this would made it less reusable and couple the popover to a specific template implementation. Our solution allows us to render any template in the popover. We also want to make sure that the component is responsible only for positioning and showing the content.

To make it look like a popover, let’s create a BasePopoverContent component. We need to render two elements in the template:

  • an arrow element having a popper.js x-arrow selector needed for the popper.js to properly position the arrow
  • content wrapper that expose a slot element in which our content will be rendered
<template>
  <div class="base-popover-content">
    <div class="base-popover-content__arrow" x-arrow/>
    <div class="base-popover-content__body">
      <slot/>
    </div>
  </div>
</template>

Now let’s use our wrapper component in the parent component where we use BasePopover

<template>
  <div>
    <img
      ref="popoverReference"
      width="25%"
      src="./assets/logo.png"
      @click="openPopover"
    >
    <BasePopover
      v-if="isPopoverVisible"
      :popover-options="popoverOptions"
      @closePopover="closePopover"
    >
      <BasePopoverContent>
        <div class="custom-content">
          <img width="25%" src="./assets/logo.png">
          Vue is Awesome!
        </div>
      </BasePopoverContent>
    </BasePopover>
  </div>
</template>

And, there we go!

You can see the popover animating in and out in the example above. We’ve left animation out of this article for the sake of brevity, but you can check out other popper.js examples for inspiration.

You can see the animation code and working example here.

Let’s look at our requirements and see if we didn’t missed anything:

Pass? Requirement Explanation
Pass Reusability We used a slot in the BasePopover component that decouples the popover implementation from the content template. This allows us to pass any content to the component.
Fail Dismissibility We made it possible to close the popover when clicking outside of it. We still need to make sure we can dismiss the popover by pressing the ESC on the keyboard.
Pass Positioning That’s where popper.js solved an issue for us. It not only gave us positioning superpowers, but also takes care of repositioning the popover when it reaches the edge of the viewport.
Fail Interaction We have a popover popping in and out, but we do not have any interactions with the popover content yet. As for now, it looks more like a tooltip than popover and could actually be used as a tooltip when it comes to showing and hiding the element. Tooltips are usually shown on hover, so that’s the only change we’d have to make.

Darn, we failed interaction requirement. Adding the interaction is a matter of creating a component (or components) that will be placed in the BasePopoverContent slot. In the example, I created a very simple component with a header and text showing a few Vue style guide rules. By clicking on the buttons, we can interact with the popover content and change the rules, when you get to the last rule the button changes its purpose and serve as a close button for the popover. It’s a lot like the new user welcome screens we see in apps.

We also need to fully meet the dismissibility requirement. We want to hit ESC on the keyboard to close the popover in addition to clicking anywhere outside it. For kicks, we’ll also add an event that proceeds to the next Vue style guide rule when pressing Enter.

We can handle that in the component responsible for rendering the popover content using Vue event key modifiers. To make the events work we need to make sure that the popover is focused when mounted. To do that we add a tabindex attribute to the popover content and a ref that will allow us to access the element in the mounted hook and call focus method on it.

// VueTourPopoverContent.vue

<template>
  <div
    class="vue-tour-popover-content"
    ref="vueTourPopoverContent"
    tabindex="-1"
    @keydown.enter="proceedToNextStep"
    @keydown.esc="closePopover"
  >
...
</template
...
<script>
export default {
...
  mounted() {
    this.$refs.vueTourPopoverContent.focus();
  }
...
}
</script>

Wrapping up

And there we have it: a fully functional popover component we can use anywhere in our app. Here are a few things we learned along the way:

  • Use a popover to expose a small amount of information or functionality. Remember that the content will disappear when user is finished with it.
  • Consider using popovers instead of temporary views like sidebars. Popovers leave more space for content and are only temporary.
  • Enable a closure behavior that makes sense based on the popover’s function. A popover should be visible only when needed. If it allows user to make a choice, close the popover as soon as the user makes a decision.
  • Position popovers onscreen with care. A popover’s arrow should always point directly to the element that triggered it and should never cover the trigger element.
  • Display one popover on screen at a time. More than one steals attention.
  • Take care of the popover size. Prevent making it too big but bear in mind that proper use of padding can make things look nice and clean.

If you don't want to dig too deep into the code and you just need the component as it is, you can try out the npm package version of the component.

Hopefully you will find this component useful in your project!

The post Reusable Popovers to Add a Little Pop appeared first on CSS-Tricks.