Using Vue.js in Your Drupal Site

Posted on Wed, 12/11/2019
10 minutes read

Drupal 8’s front-end theming is so much better than what we had in Drupal 7, but one issue you still run into is there are lots of options on how to handle JavaScript on your site. The standard approach is to use jQuery, which isn’t terrible even though it gets a lot of hate, but isn’t the best answer for us anymore. Luckily jQuery isn’t actually a requirement for your themes in Drupal 8, so you don’t have to use it at all! That brings you to the next question of what should you do if it’s not jQuery? I generally lean towards vanilla JavaScript for all JavaScript when I need it, but then when you get a more complicated form or page that needs quite a lot of JavaScript I pull in Vue.js.

Why Vue.js?

In JavaScript land there are plenty of options for front-end interfaces, so why chose Vue.js over React or some other option? Every site is different and requires different tools but I think for Drupal if you aren’t doing a fully detached site but just want a more elegant way to write JavaScript, Vue.js is the obvious choice for the following reasons:

  • You can almost use it as a drop in replacement for jQuery with little retraining needed.
  • You can use it as much or as little as you want, you can have it add some interaction to a page or go fully detached all with the same library.
  • The syntax for its templates looks very similar to twig so it should look familiar to any developer working in Drupal.
  • It's as simple to include as loading in a script but can get as complicated as you want and rolled up with webpack.
  • Like Drupal it has plenty of community created components that you can grab, want a draggable table? Someone made that for you.

I’m not going to be able to cover every use case, or explain how to use Vue.js in general, but will show you a couple different approaches that can be used to integrate Vue.js into your Drupal site. I break down the couple different approaches you can take to use Vue in your website as follows:

  • Drupal rendered markup and Vue enhanced interactions. EASIEST
  • Drupal rendered with Vue getting more data from the api to render some of the page. MEDIUMEST
  • Completely detached Vue only uses Drupal's APIs to get data and build pages. HARDEST and outside the scope of this blog post, but has been covered lots by others.

Getting Started

The first thing you need to do to get Vue.js into your Drupal theme is define it as a library that can be pulled in by your theme. To do that you edit your THEMENAME.libraries.yml file and add a new library that pulls in the Vue JavaScript, like the following snippet.

vue-js:
  js:
    https://cdn.jsdelivr.net/npm/vue/dist/vue.js: { type: external }

With that you now have the ability to make that a dependency for any of your Vue components and then they’ll have access to all the Vue.js goodness like this.

modal-vue:
  js:
    dist/js/modal-vue.js: {}
  dependencies:
    - THEMENAME/vue-js

As mentioned before, that could be replaced with some sort of webpack rolled up version of your JavaScript that only loads exactly what you want, but I like easy.

Now that we have Vue.js available to your Drupal site we can go over the two different approaches I’m going to tackle today for getting Vue in your site.

Drupal rendered markup and Vue enhanced interactions

This approach is what I think is the jQuery replacement approach. We are letting Drupal render everything, and after that we use Vue to add dynamic interaction to our pages. My example I have here is creating a modal that opens based on a button click. It’s nothing hard to do but should give you the best side by side comparison of how one is done without Vue and one is done with Vue.

Creating a modal without Vue.js

First we define our Library, which is just including our JS and our dependency on jQuery.

# THEMENAME.libraries.yml
modal-non-vue:
  js:
    dist/js/modal-non-vue.js: {}
  dependencies:
    - core/jquery

Next is our Block Template. For this example I created a block and it uses a Drupal twig template to hold the button and the modal.

# block--non-vue-block.html.twig
{{ attach_library('vue_js/modal') }}
{{ attach_library('vue_js/modal-non-vue') }}
{%
  set classes = [
    'block',
    'block-' ~ configuration.provider|clean_class,
    'block-' ~ plugin_id|clean_class,
  ]
%}
<div{{ attributes.addClass(classes) }}>
  {{ title_prefix }}
  {% if label %}
    <h2{{ title_attributes }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}
  {% block content %}
    <div{{ content_attributes.addClass('content') }}>
      <a id="non-vue-modal-button" href="#">Open Non Vue Modal</a>
      <section id="non-vue-modal" class="modal">
          <div class="modal__mask hidden" >
            <div class="modal__modal">
              <div class="modal__content">
                <div class="content">This is your modal!</div>
                <div class="modal__button modal__close">
                  X
                </div>
              </div>
            </div>
          </div>
      </section>
    </div>
  {% endblock %}
</div>

Finally we come the the JS, in here is the jQuery and non jQuery code (the commented out code) to handle this modal interaction.

# modal-non-vue.js
!((document, Drupal, $) => {
  'use strict';


  Drupal.behaviors.drupalNonVueModal = {
    attach: function(context) {


      $('#non-vue-modal-button', context).on('click', function (e) {
        e.preventDefault();
        $('#non-vue-modal .modal__mask').removeClass('hidden');
      });


      $('#non-vue-modal .modal__close', context).on('click', function (e) {
        e.preventDefault();
        $('#non-vue-modal .modal__mask').addClass('hidden');
      });


      // vanillaJS for fun too.
      // const modal = context.querySelector('#non-vue-modal');
      // const modalButton = context.querySelector('#non-vue-modal-button');
      // const modalCloseButton = context.querySelector('#non-vue-modal .modal__close');
      //
      // if (modalButton !== null) {
      //   modalButton.addEventListener('click', (e)=> {
      //     e.preventDefault();
      //     modal.querySelector('.modal__mask').classList.remove('hidden');
      //   });
      //
      //   modalCloseButton.addEventListener('click', (e)=> {
      //     e.preventDefault();
      //     modal.querySelector('.modal__mask').classList.add('hidden');
      //   });
      // }
    }
  };


})(document, Drupal, jQuery);

Nothing is wrong with doing it that way, it totally works and the JS is only 19 lines long, but this is a very simple example and we all know JS files can get large fast. Here is our working modal on our site:

Creating a modal with Vue.js

Same as before but now our library includes the dependency on Vue.js instead of jQuery.

# THEMENAME.libraries.yml
modal-vue:
  js:
    dist/js/modal-vue.js: {}
  dependencies:
    - vue_js/vue-js

Our block template is mostly the same as before but now we programmed in some of our interaction into the markup itself. This code is using @click and a v-if to handle all the interaction needed for this modal, so when we get to the JS we actually write it will be pretty small as Vue will see those and know what to do.

Here also shows a very important tag needed to integrate Vue in Drupal. The twig tag of {% verbatim %}, like mentioned before, Twig and Vue templates are very similar. Unfortunately, that means Twig can actually try to render out out Vue template which would break Drupal. So with verbatim Drupal prints out the code exactly as written, meaning it will print {{ content }} into the modal HTML on the page instead of trying to render out what the variable of content is at the time of render.

# block--vue-block.html.twig
{{ attach_library('vue_js/modal') }}
{{ attach_library('vue_js/modal-vue') }}
{%
  set classes = [
    'block',
    'block-' ~ configuration.provider|clean_class,
    'block-' ~ plugin_id|clean_class,
  ]
%}
<div{{ attributes.addClass(classes) }}>
  {{ title_prefix }}
  {% if label %}
    <h2{{ title_attributes }}>{{ label }}</h2>
  {% endif %}
  {{ title_suffix }}
  {% block content %}
    <div{{ content_attributes.addClass('content') }}>
      {% verbatim %}
      <section id="vue-modal" class="modal">
        <a id="vue-modal=button" href="#" @click="showModal = true">Open Vue Modal</a>
        <transition name="modal" v-if="showModal" @close="showModal = false">
          <div class="modal__mask hidden"
            :style="{'display': 'block'}"
          >
            <div class="modal__modal">
              <div class="modal__content">
                <div class="content">{{ content }}</div>
                <div class="modal__button modal__close" @click="showModal = false">
                  X
                </div>
              </div>
            </div>
          </div>
        </transition>
      </section>
      {% endverbatim %}
    </div>
  {% endblock %}
</div>

Here in the JS is mostly just boilerplate Vue to get it attached to the template we need. After that we added a method of close which sets the variable of showModal back to false. In here we put in the content of the modal text which isn't needed at all, but I did that just to show off the {% verbatim %} tag, and then you can easily change what that says in your modal by changing the value of content, though that would require more code than what is in my sample.

# modal-vue.js
document.addEventListener('DOMContentLoaded', () => {
  'use strict';


  new Vue({
    el: '#vue-modal',
    data: {
      showModal: false,
      content: 'Default Modal Text',
    },
    methods: {
      close() {
        this.showModal = false;
      }
    }
  });
});

And here is that modal displaying, looks the same as the first one.

Drupal rendered with Vue getting more data from the api to render some of the page

This is the much more powerful thing you can do with Vue and Drupal. Some things I’ve done in the past with this are:

  • A single page order form with every available product and as you change quantities the total cost updates and which products shown in your cart update.
  • A very complicated admin form to build those single page order forms, they needed the ability to drag products around, group, add extra fields on the product per customer, something that would have been a big pain in Drupal was fairly easy in Vue.

For the example here we are going to replace a Drupal View with Vue.js to output a list of Article Titles. That by itself seems silly as Drupal is really good at making views and outputting content, this shows the starting point and from there you could:

  • When articles are clicked auto show the body
  • Have a better ajax load in more content
  • Make the list user sort-able
  • Easily delete items form the list
  • A search that trims down the list as you type

You can do that or really anything else you want that would much harder to do within a Drupal View.

Creating the Article Listing in Drupal without Vue.js

This is really too simple to show, I created a Drupal View that shows 15 article titles and links them to their page. From there I placed that block on a page, done.

Create the Article Listing in Drupal using Vue.js

Here I am creating a custom block, that will give me a template, then I’m creating a library to pull in my JavaScript. I also turned on the JSON:API module so I can pull that data in using the API.

First thing is I added my library, and I also added axios so I can make ajax calls to our Drupal API.

# THEMENAME.libraries.yml
axios:
  js:
    https://unpkg.com/axios/dist/axios.min.js: { type: external }


articles:
  js:
    dist/js/articles.js: {}
  dependencies:
    - vue_js/axios
    - vue_js/vue-js

Then in the block template it looks similar to before but now I'm using a v-for and :href to loop over all the articles and bind the alias value from the API to the link we are creating.

# block--vue-articles-block.html.twig
{%
  set classes = [
  'block',
  'block-' ~ configuration.provider|clean_class,
  'block-' ~ plugin_id|clean_class,


%}
{{ attach_library('vue_js/articles') }}
<div {{ attributes.addClass(classes) }}>
  {{ title_prefix }}
  <h2>Article Titles Vue</h2>
  {{ title_suffix }}
  {% block content %}
    <div id="vue-articles">
      {% verbatim %}
        <div class="articles" v-for="article in articles">
          <div class="article">
            <a :href="article.attributes.path.alias">{{ article.attributes.title }}</a>
          </div>
        </div>
      {% endverbatim %}
    </div>
  {% endblock %}
</div>

Here once again is mostly boilerplate Vue.js, then when the Vue component mounts we call to JSON:API to give us 15 articles and we take the response and set it to that articles array.

# articles.js
document.addEventListener('DOMContentLoaded', () => {
  'use strict';


  drupalSettings.articles = new Vue({ // eslint-disable-line no-unused-vars, no-undef, max-len
    el: '#vue-articles',
    data: {
      articles: []
    },
    mounted() {
      axios
        .get('/jsonapi/node/article?page[limit]=15')
        .then(response => (this.articles = response.data.data))
    }
  });
});

Nothing too crazy there, and here it is in action, looks almost exactly the same but I didn't set a sort on published date.

From there you would most likely want to create a method to fetch more articles triggered by a “View More” button and then they just magically show up below the existing articles.

Wrapping up

There are the two more easy methods of adding Vue.js to your Drupal site. The great thing about it is that you can do it on a component by component page, you don’t need to go fully detached and figure out routing or a bunch of stuff Drupal is already giving you for free. It’s also simple enough if you know JavaScript you should be able to be building things in Vue.js within the day.

Hopefully this was helpful and leads you to start making better JavaScript for your Drupal sites. I created a git repo of all this code and a database so you can spin it up yourself and play with it. If you run into issues or have questions feel free to reach out to me @joshfabean.