When and How to Use Vue 3.0 with Craft CMS

Could Vue 3.0 be a good fit for your project's requirements, and if so, what's the best way to incorporate Vue components in Craft?

In the ever-changing landscape of Javascript environments, Vue JS has enjoyed widespread popularity and increasing adoption since its last major release in 2016. And for good reason: it can manipulate the DOM faster than rivals and sports a short learning curve. Now, in 2020, the library shows no signs of slowing down as it continues to scale Top 10 charts around the web.

As a progressive framework, Vue is suitable for projects ranging from adding simple reactivity to a small SSG site to pairing with custom backend frameworks for building large scale web applications, to managing state and routes for single page applications. With 7 different ways to define a component, it's a highly adaptable tool that can flex to the project needs instead of forcing your code into a cramped box.

If you're thinking of incorporating this nimble powerhouse into your latest Craft CMS project, but wondering when it might be appropriate or how to go about it, read on for a few factors to weigh beforehand, what you need to know about 3.0, and a lightning guide to Single File Components in the context of a standard Craft project.

When a framework comes in handy...

Plain JS can do anything a library can and ES6 modules are workhorses that fit the bill for parcelled, reusable code. Including a framework for the sake of it is a road no developer wants to go down, so before further lauding Vue's capabilities, here are some considerations to help guide the decision to Vue, or not to Vue:

Reactivity

  • Your project entails reactive functionality, like a type-ahead search component or index with a compound group of filter options. When you need to handle asynchronous requests and manage the results elegantly, Vue can ease the workload overall by providing structure and speed, and greatly simplify the process of adding polish like list transitions.
  • You're building robust forms that require smart validation, asynchronous submission, and could benefit from conditional elements. Vue's input bindings make it easy to define lean, custom validation and to compute conditional logic quickly, logging all the of the input data away to a central, reactive object for easy access by an asynchronous method later.

Component Libraries

  • You want to streamline recurring patterns that you've identified and establish them as encapsulated but highly configurable components that you can port easily between projects without rifling between your Twig templates and JS files. Vue has a strong set of tools for building CSS-based transitions, so even basic patterns like accordions and menus can benefit. One of the primary reasons that Vue pairs so beautifully with Craft is the same reason it works so well for large scale applications: Single File Components (SFCs). This approach to defining components pulls together markup, JS logic, and styles (if you wish), making your project organization tidier and your components more flexible and easier to reuse.

If one or more of these cases checks a box for you, then Vue could be a great choice for your project. 

What's up with 3.0?

If you've decided Vue is just the thing, which version is best at the moment? Full stability is slated for completion by the end of this year, so "One Piece" is currently distributed under the next tag and some tools and official libraries are still a WIP (like the DevTools extension and Vuex Router integrations). Here's a top-level summary of some of the things I'm most exited about in the new release:

  • Composition API that works similarly to React Hooks increases readability with extended properties like setup for styling and organizing components
  • More intuitive transition component property names
  • Templates may accept multiple root elements and multiple v-models
  • New suspense async component inspired by React Suspense to replace manual v-if conditions
  • Teleport inspired by React Portals
  • Typescript conversion
  • Extended global APIs and helper functions

These enhancements and many more—like the use of the Virtual DOM for added speed and better tree-shaking—make 3.0 an alluring pick that's destined to be around for a while. If your client project enables you to stay close to the core and you have no need for various plugins and the router, then you might consider rolling the latest. However, if the project requirements are robust or not yet fully defined, your best bet is to probably stick with version 2.0 for now and upgrade in the coming months.

For the purposes of this article, though, let's live on the edge for a moment and begin familiarizing ourselves with the new 3.0 syntax.

Let's do this.

— 1. Setup

The first thing we'll need to do is add our framework. If you're just exploring, you could do this simply and directly:

<script src="<https://unpkg.com/vue@next>"></script>

Or, if you're working with build tools and are ready to bundle your modules, use your package manager of choice for installation:

yarn install vue/@next

To gain the full, self-contained benefits of Vue's SFCs, you should also install the SFC compiler and Vue loader:

yarn add @vue/compiler-sfc@^3.0.0 vue-loader@^16.0.0-alpha.3

At a minimum, you'll want to modify webpack.common.js to incorporate the VueLoaderPlugin and resolve the vue keyword to the right place:

const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader',
      },
    ],
  },
  resolve: {
    alias: {
      vue$: 'vue/dist/vue.esm-bundler.js',
    },
  },
  plugins: [new VueLoaderPlugin()],
}

// file: webpack.common.js

— 2. Playing It Safe

If you're familiar with 2.0, the next part is noticeably different. Instead of mounting globally, we'll protect our app from accidental top-level changes by using the new createApp() method:

import { createApp } from 'vue'
const app = createApp({})

Where you'd like to integrate Vue in your Craft template files, add an id attribute to the parent element. It's common to add this in the primary layout template like so:

<main id="app" role="main">
</main>

Finally, mount the app:

app.mount('#app')

— 3. The Cat's Meow

Now, to take advantage of housing markup and logic together in self-contained, discrete single file components. Using HTML attributes and Vue's props method, we can then inject data from our template right into the vue file. But, first, let's start with defining a minimal SFC in a new file:

<template>
  <div>Stop kitten around.</div>
</template>
<script>
export default {
}
</script>

// file: CatsMeow.vue

Back at the root, import the component and register it with the app. Your file should now look something like this:

import { createApp } from 'vue'
import CatsMeow from './modules/CatsMeow.vue'
const app = createApp({
  components: {
    CatsMeow,
  },
})
app.mount('#app')

// file: index.js

Wherever you'd like the Cat's Meow to appear, include it in the templates:

<cats-meow></cats-meow>

You should see a 'Hello World' message greeting you on page refresh. Pretty simple, but easy to see the potential.

— 4. Get Your Data

Next, let's integrate our component more closely with the CMS by incorporating real content from the templates.

Using Vue props, we could easily pass data into our components like so:

<cats-meow 
  :info="
    [
      { meowStyle: 'squawky', name: 'Herman' }, 
      { meowStyle: 'flat', name: 'Maeve' },
    ]
  ">
</cats-meow>

But that's not immensely helpful...unless we could mirror this in Twig to expose the template data to Vue.

If you know that your data will be output as an array, you could pass it directly to the Vue prop:

<cats-meow :info="{{ entry.cats }}"></cats-meow>

Oftentimes, though, that's either not the case, or it's preferable to clean up the data beforehand.

For example, let's say we have a Super Table field with a repeating block that contains child fields describing each item in the set, some of which are superfluous. We can expose only what we need by looping through and defining our own object like so:

{% set clowderOfCats = [] %}
{% for cat in entry.cats %}
  {% set clowderOfCats = clowderOfCats|merge([
    {
      meowStyle: cat.meow,
      name: cat.name,
    },
  ]) %}
{% endfor %}

<cats-meow :info="{{ clowderOfCats }}"></cats-meow>

/* file: cats/_entry.twig */

On refresh...oh, snap! We get into PHP string-to-array-conversion trouble right away when we try to include this plainly:

Thankfully, getting around this is easy-peasy. We simply need to append Twig's handy-dandy json_encode filter to our variable and our object can safely pass through with no notices in sight:

<cats-meow :info="{{ clowderOfCats | json_encode }}"></cats-meow>

Back in our CatsMeow.vue component, we'll add a property definition so that the Vue template is aware of the incoming data and knows what type to expect:

export default {
  props: {
    items: {
      type: Array,
      required: true,
    },
  },
}

// file: CatsMeow.vue

In a very minimal example, we can output this as a list in the SFC template, so that full component example becomes:

<template>
  <ul>
    <li v-for="item in items">
      The cat, {{ item.name }}, meowed with a {{ item.meowStyle }} voice.
    </li>
  </ul>
</template>
<script>
  export default {
    props: {
      items: {
        type: Array,
        required: true,
      },
    },
  }
</script>

// file: CatsMeow.vue

To define additional layers of configuration, you may pass as many properties as you wish, but keep in mind, props are case sensitive; know that your camelCased prop names will need to become kebab-cased HTML attributes.

Some final notes..

— 1. Depending on your project, you might require data directly at the app root, or perhaps you're unable to use SFCs because you don't have support for the template compiler in your build tools. If either of these is true for you, consider assigning your data to the window:

{% set appData %}
  window.appData = {
    locale: {{ craft.locale | json_encode | raw }}
  }
{% endset %}
{% do view.registerJs( appData, POS_END ) %}

— 2. Beware inline methods for defining components directly in the dom: in Twig, it's especially unideal, given that you quickly get into trouble with handlebar conflicts and don't get the full benefits of modular code.

— 3. Consider setting up your app to asynchronously load components for a more performant build (I'll leave that as a topic for another day!).

This concludes a lightning guide to Vue SFCs in Craft and a brief look at Vue 3.0! If you're using Vue + Craft in unique ways, I hope you'll chime in to the comments below.

Thanks for reading!

Sign up for The Viget Newsletter

Nobody likes popups, so we waited until now to recommend our newsletter, a curated periodical featuring thoughts, opinions, and tools for building a better digital world. Read the current issue.