Passing your Craft CMS data to Vue.js with Twig

Posted by Jake Dohm on 15 February, 2019

Goodwork-article-image-craftcmsdatavuejstwig

One of the unique challenges of using Vue JS with Craft CMS is how you get your data from Craft into Vue. Vue’s whole model of reactive UI is based on having your data in Vue, and your markup reacting to changes in that data automatically. That means Vue must own” the data required for your templates and components. But, if your data lives in Craft, how do you get that data into Vue?

There are multiple ways to tackle this problem, and choosing the best way depends on how your project is structured. If your project is a Single Page Application (SPA) you’ll (most likely) need to create an API, and make HTTP requests to your API to get your data. But, if your project is a traditional” server-side rendered website, then creating an API and delaying the render of your markup by waiting for an HTTP request to finish is usually not the best idea.

In this article, we’ll cover two ways you can pass your data into Vue using just Twig templates and Vue components.

Method #1: Passing your data into a component using props

Vue components have the ability to receive external data using props, which is convenient when using Vue components on your Craft site. Using this pattern allows you to pass your data from Twig into a component, whether it’s an object, array, string, or anything else. Here’s an example:

<!-- page.twig -->
{% set fruits = ['apple', 'banana'] %}
{% set fruitEmoji = { 'apple': '🍎', 'banana': '🍌' } %}

<emoji-fruit-component :fruits="{{ fruits | json_encode | raw }}" :fruitemoji="{{ fruitEmoji | json_encode | raw }}"></emoji-fruit-component>
/* page.js */

// create a component, and accept our data with props
Vue.component('emoji-fruit-component', {
  props: ['fruits', 'fruitEmoji'],
  template: '...'
})

In the example above, we create two sets of data within our Twig templates: fruits (an array), and fruitEmoji (an object). Then we pass that data into our Vue component using props. Then in our JS, when we define our Vue component, we accept fruit and fruitEmoji both as props. Now the fruit and fruitEmoji data will be accessible within our Vue component.

Tip: Use the json_encode filter to escape your data and make it JS-readable, then use the raw filter to allow HTML entities so characters like quotes are printed correctly.

Method #2: Attaching data to the window, then loading it into Vue

Another way to pass your data into Vue from Craft is to assign it as a global variable in JavaScript, then load that data into your Vue component or instance. Consider the following example.

<!-- page.twig -->
{% set fruits = ['apple', 'banana'] %}
{% set fruitEmoji = { 'apple': '🍎', 'banana': '🍌' } %}

<script>
  window.craftData = {
    fruits: {{ fruits | json_encode | raw }},
    fruitEmoji: {{ fruitEmoji | json_encode | raw }},
  }
</script>
/* fruit-page.js */

// load the data into your root Vue instance
new Vue({
  el: '#vue-app',
  data: window.craftData
})

// load the data into a single Vue component
Vue.component('emoji-fruit-component', {
  template: '...',
  data() {
    return window.craftData
  }
})

Let me explain what I’m doing in the examples above:

  • First we create an object (craftData) that is attached to the global window object (which is globally available to all scripts).
  • The craftData object contains any data that we need access to in Vue (or a specific component).
  • Then when we instantiate our Vue instance (most likely in a JS file), or component, we tell it to pull the craftData object into our Vue instance as local data.

How to choose which method to use

Now you know two methods to accomplish passing your data into Vue, but which one should you use? Well, it depends.

Here are a few thoughts to help you decide which method to use:

Real world examples

The examples that we looked at above are using hard-coded data, but on your website you’ll generally be pulling data from Craft so I wanted to provide some more relevant examples.

Slider (Method #1)

{% set images = [] %}
{% for image in entry.images %}
  {% set images = images|merge([{
    url: image.one.url,
    alt: image.title
  }]) %} 
{% endfor %}

<vue-slider :images="{{ images | json_encode | raw }}"></vue-slider>

Testimonials (Method #1)

{% set testimonials = [] %}
    
{% for block in entry.testimonials %}
  {% set testimonials = testimonials|merge([{
    quote: block.quote,
    name: block.name
  }]) %}
{% endfor %}

<testimonial-slider :testimonials="{{ testimonials| json_encode | raw }}" title="{{ entry.title }}"></testimonial-slider>

Blog post (Method #2)

{% set entries = craft.entries.section('blog').all() %}
  
{% set posts = [] %}
{% for post in entries %}
  {% set posts = posts|merge([{
    url: post.url,
    title: post.title
    content: post.content
  }]) %} 
{% endfor %}

<script>
  window.craftData = {
    posts: {{ posts | json_encode | raw }}
  }
</script>