Vue components in Multi-Page Apps
by Gaurav Koley
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>