VueJS Components with CoffeeScript for Rails

Written by: Daniel P. Clark

The components aspect of VueJS is one of the most attractive features VueJS brings to your frontend development. It allows for composable, reusable, and protected scope code, styles, and HTML. Working with protected scopes is the smart way for implementing coherent systems. And with the added benefit of VueJS protecting your style's scope to only affect your specific component, you'll have far fewer headaches with styling your site.

With Webpack support being added to Rails as of Rails 5.1, the ecosystem for documentation on getting started is fairly young and missing many scenarios. So I'm proud to be able to introduce one of the first posts on implementing VueJS Components with CoffeeScript in Rails. This can save you a couple of days of learning the hard way and will likely help existing VueJS/Rails developers to perhaps learn a few new tricks.

We'll be continuing from where we left off in the last blog post, VueJS as a Frontend for Rails. If you'd like a copy of the code state from that, here is the public GitHub repository for that: danielpclark/vue_example.

Adding ERB Support for .vue Files

To allow Ruby code interpolation with .vue.erb files like we have in .html.erb files, we need to first add the rails-erb-loader package with yarn.

yarn add rails-erb-loader

Next we need to update our configuration file for webpack at config/webpack/loaders/vue.js and change these lines:

module.exports = {
  test: /\.vue(\.erb)?$/,
  use: [{
    loader: 'vue-loader',
    options: { extractCSS }
  }]
}

to this:

module.exports = {
  test: /\.vue(\.erb)?$/,
  use: [{
    loader: 'vue-loader',
    options: { extractCSS }
  },
  {
    loader: 'rails-erb-loader',
    options: {
      runner: 'bin/rails runner',
      dependenciesRoot: '../app'
    }
  }]
}

Now we can have our Vue component files evaluate Rails methods and objects if we append the .erb extension to our .vue files.

Moving VueJS Code into a Component

First we can move our form view from app/views/documents/_form.html.erb to our new file app/javascript/form-repository.vue.erb.

<% include ActionView::Helpers::FormOptionsHelper %>
<template>
  <div>
    <label>Subject</label>
    <input type="text" v-model="document.subject" />
    <label>State</label>
    <select v-model="document.state">
      <%= options_for_select(Document.states.keys, "concept") %>
    </select>
    <label>Body</label>
    <textarea v-model="document.body"></textarea>
    <br />
    <button v-on:click="Submit">Submit</button>
  </div>
</template>
<style scoped>
  textarea {
    rows: 20;
    cols: 60;
  }
</style>

And we'll simplify the _form.html.erb page down to:

<% content_for :head do -%>
  <%= javascript_pack_tag 'documents' %>
<% end -%>
<div id="vue-app">
  <form-document
    v-bind:document="<%= document.to_json(except: [:created_at, :updated_at]) %>"
  >
  </form-document>
</div>

Before we finish by moving our CoffeeScript code from app/javascript/packs/documents.coffee into the same component file, let's go over some features of this change.

The HTML is the same for the form, but we do need to additionally wrap it by a single HTML tag for the template section of the component. For the Rails form helper method options_for_select, we needed to include the module that defines it -- ActionView::Helpers::FormOptionsHelper -- directly from Rails on the first line.

And one of the coolest things we've done is moved the styles for the text area from the HTML into a style section that will only ever affect this component, as we've used the scoped attribute for style.

On the _form.html.erb partial page, we've simplified the code greatly. The form-document is a component name we're about to define in the CoffeeScript file. The v-bind:document is a prop we're using to hand our document data directly to our component.

Technically this particular technique is called a non-prop since there isn't a corresponding prop defined. But we'll treat it the same way as a prop for getting data to our component. If we were to try to implement accessing this data from an external source, we'd have to use more complex techniques with props like sync and/or emit, but that's far more work than what our use case requires. The v-bind evaluates what's on the right side of the equal sign to a JavaScript object for our non-prop prop.

Since we've also removed the id we formerly used for a Vue object to initiate on, we'll need to add that to the list of things we change. We'll be stripping our app/javascript/packs/documents.coffee down to just the following.

import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks'
import VueResource from 'vue-resource'
import FormDocument from '../form-document.vue.erb'
Vue.use(VueResource)
Vue.use(TurbolinksAdapter)
Vue.component('form-document', FormDocument)
document.addEventListener('turbolinks:load', ->
  Vue.http.headers.common['X-CSRF-Token'] = document
    .querySelector('meta[name="csrf-token"]')
    .getAttribute('content')
  element = document.getElementById 'vue-app'
  if element?
    app = new Vue(el: element)
)
Turbolinks.dispatch("turbolinks:load")

Here, our component is imported to a FormDocument object, and we make the component usable as a tag with the Vue.component method. This needs to be defined before the first use of new Vue, and any component used in a web page must be within HTML tags that an instance of new Vue refers to. Since we call new Vue with the id reference of #vue-app, the div tag in our HTML page is now our root for this Vue object. This allows us to use the form-document tag in it like we would any other HTML tag.

Now let's look at what our CoffeeScript looks like when moved over to a component in app/javascript/form-repository.vue.erb.

<script lang="coffee">
export default
  props:
    document:
      type: Object
      required: true
  methods: Submit: ->
    ourDocument = @_props.document
    if ourDocument.id == null
      @$http # New action
        .post '/documents', document: @document
        .then (response) ->
            Turbolinks.visit "/documents/#{response.body.id}"
            return
          (response) ->
            @errors = response.data.errors
            return
    else
      @$http # Edit action
        .put "/documents/#{@document.id}", document: @document
        .then (response) ->
            Turbolinks.visit "/documents/#{response.body.id}"
            return
          (response) ->
            @errors = response.data.errors
            return
    return
</script>

Now our VueJS component should be working in our site. Just run rails s and open your web browser to localhost:3000/documents.

If at any time the changes aren't showing up in your browser, you need to refresh the browser's cache with a hard reload. In the Chromium browser you can do this with CTRL-SHIFT-R.

Here our use of props is a form of type checker. I highly encourage that you use this technique for all props as it will give you very helpful information should you not be getting the prop into your component as you expected. You can look into implementing further prop validations at prop validations.

The export default line is a JavaSciprt feature that has the following code all included as the main object when you use the import SomeName from 'source_code' to require it. The other difference in our code here is the @_props.document line. The @_props gives us direct access to the props passed in on this._props.

And with that you now have the ability to easily create and add as many VueJS components as you want to your Rails site. Next, we'll look at how easy it is to just drop in the show resource for our Documents resource as a VueJS component.

Multiple Components Per Rails Resource

Because individual components use their own HTML style tag, you can include as many components in your source code and call them only where you want in your pages. So to change your show-resource for documents in your project, it's pretty simple. Add two lines near the beginning of your app/javascript/packs/documents.coffee file:

import ShowDocument from '../show-document.vue'
Vue.component('show-document', ShowDocument)

We'll be replacing the following HTML from app/views/documents/show.html.erb:

<p>
  <strong>Subject:</strong>
  <%= @document.subject %>
</p>
<p>
  <strong>Body:</strong>
  <%= @document.body %>
</p>
<p>
  <strong>State:</strong>
  <%= @document.state %>
</p>

with:

<% content_for :head do -%>
  <%= javascript_pack_tag 'documents' %>
<% end -%>
<div id="vue-app">
  <show-document
    v-bind:document="<%= @document.to_json(except: [:created_at, :updated_at]) %>"
  >
  </show-document>
</div>

And write the component in app/javascript/show-document.vue as:

<template>
  <div>
    <p>
      <strong>Subject:</strong>
      {{ document.subject }}
    </p>
    <p>
      <strong>Body:</strong>
      {{ document.body }}
    </p>
    <p>
      <strong>State:</strong>
      {{ document.state }}
    </p>
  </div>
</template>
<script lang="coffee">
export default
  props:
    document:
      type: Object
      required: true
</script>

Since we're not evaluating anything with Ruby in this component, we make the file extension .vue. We're still using our prop validation technique. And we're using Vue's interpolation braces to fill out our values for the view.

Now that we have our show page converted to use VueJS, we can further build on it by including components within the component. This is a very useful technique for implementing a more dynamic comment system. Create your own comment components and include them in as many other components in your site as you'd like.

Summary

Protecting the scope of what your code and styles have access to will make your code base a much more sane environment to work in. Both VueJS and CoffeeScript enforce a form of protected scope and help you write better code. With the problems we've inherited from years of backwards compatibility in the frontend technology stack, it's really nice to work with technologies that help enforce good practices.

We've now covered enough to make working with VueJS in Rails a far more pleasant experience. There are still so many more possibilities that branch out from this foundation of what you can do with VueJS, including visually dynamic content. If you need to have an excellent frontend framework of sorts to work with, then I highly recommend you seriously consider VueJS and CoffeeScript.

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.