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.
# 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 ? '✔' : '' }}</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
}
)
}
// ...