Vue.js and Rails

Vue on Rails book now available -- Purchase Vue on Rails

# Getting up and running with Vue.js and Rails

Vue.js has been widely adopted by the Laravel community, but I haven't seen much adoption in the Rails community. I have experimented with Vue.js in a couple different Rails applications and have had a very positive experience. I hope that this tutorial will show how easy it is to incorporate it into a Rails app and hope that the Rails community adopts the library as well. This tutorial will demonstrate building a Vue.js and Rails application from scratch.

# What are we Building?

We will be building an Employee management tool that will demonstrate all the basic CRUD actions. The goal is to use Vue.js to perform all the actions from a single page. We will be able to list current employees, hire new employees, edit employee information, promote and demote employees, and fire an employee (in case they get ornery). This tutorial will require basic knowledge of Rails.

Live Demo Source

# Loading in Vue.js

I typically prefer to use Bower to manage assets, but how you load in Vue.js is up to you. There is also a gem that rolls it into the asset pipeline - vuejs-rails.

# Initial Setup

We will create an Empoyee model and controller along with a single javascript file and a single index file for employees. In the end the only files that will created will be app/models/employee.rb, app/controllers/employees_controller.rb, app/assets/javascripts/employees.js, and app/views/employees/index.html. In the routes file we will add resources :employees, :except => [:new, :edit]. We will not need the new and edit actions, since the actions will be performed from the index page. All of the actions in the controller will respond to json format.

We will also need to create a migration and run it. We will create an employees table with name as a string, email as a string, and manager as a boolean. The manager column basically indicates manager status in which we will create a button that can toggle the status of said employee.

# migration
class CreateEmployees < ActiveRecord::Migration
  def change
    create_table :employees do |t|
      t.string :name
      t.string :email
      t.boolean :manager

      t.timestamps null: false
    end
  end
end

# Listing Employees

The first step will be to seed in some data into the employees table and create the initial view to display the employees in a table. Fairly simple and straight forward. The controller action will render all of the employees in a json format, and we will setup the initial view application to fetch those employees using an ajax call.

# app/controllers/employees_controller.rb
class EmployeesController < ApplicationController
  def index
    @employees = Employee.all
    respond_to do |format|
      format.html
      format.json { render :json => @employees }
    end
  end
end
// app/assets/javascripts/employees.js
var employees = new Vue({
  el: '#employees',
  data: {
    employees: []
  },
  ready: function() {
    var that;
    that = this;
    $.ajax({
      url: '/employees.json',
      success: function(res) {
        that.employees = res;
      }
    });
  }
});

The index action for the controller will also respond to html format to render the Vue.js template which will be built in app/views/employees/index.html

<!-- app/views/employees/index.html -->
<h1>Employees</h1>

<div id="employees">
  <table>
    <thead>
      <tr>
        <th>Name</th>
        <th>Email</th>
        <th>Manager</th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="employee in employees">
        <td>{{ employee.name }}</td>
        <td>{{ employee.email }}</td>
        <td>{{ employee.manager }}</td>
      </tr>
    </tbody>
  </table>
</div>

# Moving the Row for an Employee into a Component

To perform all the actions that are required for the application, it will serve us better to move the employee into a Vue Component. This can be accomplished by passing the employee object as a prop to the component.

// app/assets/javascripts/employees.js
Vue.component('employee-row', {
  template: '#employee-row',
  props: {
    employee: Object
  }
})

// ...

We will also add a template for the component into the index file.

<!-- app/views/employees/index.html -->
<script type="text/x-template" id="employee-row">
  <tr>
    <td>{{ employee.name }}</td>
    <td>{{ employee.email }}</td>
    <td>{{ employee.manager }}</td>
  </tr>
</script>

<!-- ... -->

We will also change the <tr> tag inside the <tbody>.

    <tbody>
      <tr
        is="employee-row"
        v-for="employee in employees"
        :employee="employee">
      </tr>
    </tbody>

<!-- ... -->

# Hiring an Employee

Here's where it starts to get fun. In order to hire an employee we will need to add a row to the table that will include fields to set Name, Email, and Manager status. The data for the main Vue instance will need an object for a new employee as well as an errors object to handle validation errors.

// app/assets/javascripts/employees.js
var employees = new Vue({
  el: '#employees',
  data: {
    employees: [],
    employee: {
      name: '',
      email: '',
      manager: false
    },
    errors: {}
  },
// ...

The controller for the create action will respond with the created employee or the errors if any validations are not met.

# app/controllers/employees_controller.rb

  # ...

  def create
    @employee = Employee.new(employee_params)
    respond_to do |format|
      format.json do
        if @employee.save
          render :json => @employee
        else
          render :json => { :errors => @employee.errors.messages }, :status => 422
        end
      end
    end
  end

  private

  def employee_params
    params.require(:employee).permit(:name, :email, :manager)
  end
end

We will also add a hireEmployee method to the Vue instance, which will be called when the "Hire" button is clicked as shown in the template. This method makes an ajax call to the create action for employees using the values set for the employee object.

// app/assets/javascripts/employees.js

// ...

  methods: {
    hireEmployee: function () {
      var that = this;
      $.ajax({
        method: 'POST',
        data: {
          employee: that.employee,
        },
        url: '/employees.json',
        success: function(res) {
          that.errors = {}
          that.employees.push(res);
        },
        error: function(res) {
          that.errors = res.responseJSON.errors
        }
      })
    }
  }
<!-- app/views/employees/index.html -->

<!-- ... -->

      <tr>
        <td>
          <!-- Input -->
          <input type="text" v-model="employee.name"><br>
          <!-- Validation errors -->
          <span style="color:red">{{ errors.name }}</span>
        </td>
        <td>
          <!-- Input -->
          <input type="text" v-model="employee.email"><br>
          <!-- Validation errors -->
          <span style="color:red">{{ errors.email }}</span>
        </td>
        <td><input type="checkbox" v-model="employee.manager"></td>
        <!-- button click calls hireEmployee -->
        <td><button @click="hireEmployee">Hire</button></td>
      </tr>
    </tbody>

<!-- ... -->

# Editing an Employee

We will add an Edit button to each row which will toggle each column to an input field for that employee. Calling the update action in the controller will be very similar to the create action, except we are finding the employee then calling update instead of save. The update action will be called when the Save button is clicked when in edit mode. We will also add a Promote/Demote toggle which will toggle the manager status without having to edit the other fields. The javascript and html required to make this happen will go inside the Vue component and component's template.

# app/controllers/employees_controller.rb

# ...

  def update
    @employee = Employee.find(params[:id])
    respond_to do |format|
      format.json do
        if @employee.update(employee_params)
          render :json => @employee
        else
          render :json => { :errors => @employee.errors.messages }, :status => 422
        end
      end
    end
  end

# ...
// app/assets
Vue.component('employee-row', {

  //...

  data: function () {
    return {
      editMode: false,
      errors: {}
    }
  },
  methods: {
    // toggle the manager status which also updates the employee in the database
    toggleManagerStatus: function () {
      this.employee.manager = !this.employee.manager
      this.updateEmployee()
    },
    // ajax call for updating an employee
    updateEmployee: function () {
      var that = this;
      $.ajax({
        method: 'PUT',
        data: {
          employee: that.employee,
        },
        url: '/employees/' + that.employee.id + '.json',
        success: function(res) {
          that.errors = {}
          that.employee = res
          that.editMode = false
        },
        error: function(res) {
          that.errors = res.responseJSON.errors
        }
      })
    }
  }
<!-- app/views/employees/index.html -->

<script type="text/x-template" id="employee-row">
  <tr>
    <td>
      <!-- Show input when in edit mode -->
      <div v-if="editMode">
        <input type="text" v-model="employee.name"><br>
        <span style="color:red">{{ errors.name }}</span>
      </div>
      <div v-else>{{ employee.name }}</div>
    </td>
    <td>
      <div v-if="editMode">
        <input type="text" v-model="employee.email"><br>
        <span style="color:red">{{ errors.email }}</span>
      </div>
      <div v-else>{{ employee.email }}</div>
    </td>
    <td>
      <div v-if="editMode">
        <input type="checkbox" v-model="employee.manager">
      </div>
      <div v-else>{{ employee.manager ? '&#10004;' : '' }}</div>
    </td>
    <td>
      <!-- Save button calls updateEmployee -->
      <button v-if="editMode" @click="updateEmployee">Save</button>
      <!-- Edit button puts row into edit mode -->
      <button v-else @click="editMode = true">Edit</button>
      <!-- Promote / Demote based on current status -->
      <button v-if="!editMode" @click="toggleManagerStatus">{{ employee.manager ? 'Demote' : 'Promote' }}</button>
    </td>
  </tr>
</script>

# Firing an Employee

Last but not least, we will need to be able to fire employees. The fire button will call fireEmployee, which will in turn call the destroy action for the controller.

# app/controllers/employees_controller.rb

# ...

  def destroy
    Employee.find(params[:id]).destroy
    respond_to do |format|
      format.json { render :json => {}, :status => :no_content }
    end
  end

# ...
// app/assets/javascripts/employees.js

// Inside the employee component

    fireEmployee: function () {
      var that = this;
      $.ajax({
        method: 'DELETE',
        url: '/employees/' + that.employee.id + '.json',
        success: function(res) {
          that.$remove()
        }
      })
    }

// ...
<!-- the Fire button inside the component template-->
<button v-if="!editMode" @click="fireEmployee" style="color:red">Fire</button>

And that about wraps it up. I would appreciate any comments, and would love to get feedback from others who have worked with Vue.js and Rails. Check out the full source code on Github. I would like to thank Evan You for creating Vue.js. I find it very easy to work with and has allowed me to elimante thousands of lines of Javascript spaghetti code in existing rails applications.

# After Thought: Vue Resource

As Oli commented, using Vue Resource can clean things up a bit as opposed to using Ajax. In order to get Vue Resource to work in Rails, you'll need to configure the X-CSRF-Token Header appropriately. This can be done using the interceptors. For example:

Vue.http.interceptors.push({
  request: function (request) {
    Vue.http.headers.common['X-CSRF-Token'] = $('[name="csrf-token"]').attr('content');
    return request;
  },
  response: function (response) {
    return response;
  }
});

Here's an example of using Vue Resource for the hireEmployee method:

// app/assets/javascripts/employees.js
// ...

    hireEmployee: function () {
      var that = this;
      this.$http.post('/employees.json', { employee: this.employee }).then(
        function(response) {
          that.errors = {};
          that.employee = {};
          that.employees.push(response.data);
        },
        function(response) {
          that.errors = response.data.errors
        }
      )
    }

// ...