image/svg+xml

Gaurav Koley Internet Lurker, Poet, Night Owl @ IIIT Bangalore

Vue components in Multi-Page Apps

Over the past year, I have been working on Gratiato, a social networking site for students, teachers and researchers to share papers, find relevant research and collaborators.

Gratiato is built on top of Ruby on Rails with sprinkles of Vue for a very dynamic and interactive UI. Thus, the website is a multi-page Rails app interspersed with Vue components. These components are written in .vue Single File Components and compiled with Webpacker and the Rails Webpacker gem. I wrote a post about that here.

This allows me to use Vue components within Rails .html.erb layouts as well as pass Rails data to the components by sprinkling some wrapper code which I would talk about subsequently.

So a typical Rails layout file looks like:

<!-- app/resources/show.html.erb -->

<div class="ui divided items">
  <div class="item">
    <div class="content">
      <%= link_to @resource.title, @resource, class: "header" %>
      <div class="vue-container">
        <!-- Vue Component -->
        <gratia-count />
      </div>
      <div class='vue-container'>
        <!-- Vue Component -->
        <get :resource="<%= @resource.to_json %>"></get> <!-- passing Rails data as JSON using to_json -->
      </div>
    </div>
  </div>
  <div class="vue-container">
    <!-- Vue Component -->
    <show-pdf :resource="<%= @resource.to_json %>"></show-pdf>
  </div>
</div>

So there may be multiple .vue-container containers which wrap our Vue components. I then initialize a Vue app on these containers and then do the same for all the pages that need Vue components.

In the above file gratia-count, get and show-pdf are three Vue components which I use.

Then, I have a JS file which is imported for all the pages within which I have the following code:

// app/javascript/packs/app.js
import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks'

import store from './store'
import ShowPdf from './ShowPdf.vue'
import Get from './Get.vue'
import GratiaCount from './GratiaCount.vue'

Vue.use(TurbolinksAdapter)

['turbolinks:load', 'DOMContentLoaded'].map(e =>
  document.addEventListener(e, () => {

    if(window.vueapp == null){
      window.vueapp = []
    }
    if(window.vueapp != null){
      for(var i=0, len=vueapp.length; i < len; i++){
        vueapp[i].$destroy();
      }
      window.vueapp = []
    }
    var myNodeList = document.querySelectorAll('.vue-container');
    forEach(myNodeList, function (index, element) {
      if (element != null) {
        var vueapp = new Vue({
          el: element,
          store,
          components: {
            ShowPdf, Get, GratiaCount  // my components
          }
        })
        window.vueapp.push(vueapp);
      }
    });
  })
)

This creates and initializes a Vue instance on every .vue-container DOM element with all the components registered and everything works! An important thing to note is that all the components are being registered with all the Vue apps being created here.

The code in app.js also cleans any remnants of previous Vue instances created by itself and reinitializes everything on page load. This is particularly useful when using this in conjuction with Turbolinks which is fairly common in Rails.

['turbolinks:load', 'DOMContentLoaded'].map(e =>
  document.addEventListener(e, () => {

This piece of code ensures that the Vue instances are cleaned and initialized on page load for both turbolinks and regular page load. Vue.use(TurbolinksAdapter) ensures that Vue works seamlessly with Turbolinks.

Sharing data

If you need to share data among the different components, across the different containers, any of the standard data sharing techniques like an Event Bus or Vuex would work just fine. In my use, I use a Vuex store as shown by store in the code above.

Note: The Vue components may be defined in any manner which the developer sees fit. The mentioned approach works irrespective of component being a SFC or a global component such as:

<div id="fruit-template" class="vue-template">
  <div class="fruit">
    <h3></h3>
  </div>
</div>

<script>
  Vue.component('my-fruit', {
    template: '#fruit-template',
    props: ['fruit-name']
  });
</script>