VueJS Components with CoffeeScript for Rails

Development

Reading Time: 7 minutes

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.

Subscribe via Email

Over 60,000 people from companies like Netflix, Apple, Spotify and O'Reilly are reading our articles.
Subscribe to receive a weekly newsletter with articles around Continuous Integration, Docker, and software development best practices.



We promise that we won't spam you. You can unsubscribe any time.

Join the Discussion

Leave us some comments on what you think about this topic or if you like to add something.

  • Pingback: 週刊Railsウォッチ(20180406)ruby-sass gemが非推奨に、Roda gem、paiza.ioは便利、Linuxha/procで遊ぼうほか()

  • Pingback: Railsのフロントエンドのノウハウ#1: システムテスト編(翻訳)()

  • Leonardo Brito

    Hi Daniel, great post!
    I’ve been experimenting with VueJS and Webpacker for a while on a quite big Rails project.

    There’s a situation I keep stumbling into and can’t quite see an easy solution to: suppose you’re transforming a legacy view with separate JS into a VueJS SFC component.

    Now suppose that view has some erb that accesses or modifies a resource sent by the controller. For instance:

    Sure, we could use props or similar tricks when content is simple enough, e.g. if we just wanted to output `@product`. But when we need to access methods in controller-passed objects or even use very common gems (think Kaminari, Paperclip — all of them depend on the resource _object_, not some serialized representation of it), it seems we’re out of luck.

    In short, Webpack compiles everything beforehand — including erb — and in many, many cases ERB needs to be compiled only at _request time_.

    I’m really struggling to find a solution to this. At least to me, I think this kind of problem is really common. The only workarounds I could find or think of were rewriting the views in such a way to extract these dependencies from the SFC template, which is not really viable in larger projects.

    What do you think, have you ever faced similar problems?

  • Luzio Luna

    Hey Daniel, I love this stuff. Im a super novice and im trying to wrap my head around this to see if I can use it in my project. I wonder if you could answer a very basic question for me.
    I want to include a link_to tag to a user sign in inside a template of a single file component in a vue-bootstrap navbar. when I put just a string inside the erb tag it renders, but if i put the link_to tag in front of it i get a failed to compile error. I tried putting at the top with no luck. I get a Error: rails-erb-loader failed with code: 1
    at ChildProcess.
    any hints on how to solve this would be amazing. Ill keep plugging away on it! Great work!

    • If you check out Leonardo’s comment here you’ll find that only data that can be precompiled with webpacker will work in that ERB execution. Anything that needs to be loaded dynamically during a user session should be brought in through a prop, or a separate call to the server to retrieve the data. The prop is the simplest solution for a link.