Ví dụ nhỏ về sự kết hợp giữa Vue.js and Rails

Mở đầu

Vue.js đã được áp dụng rộng rãi bởi cộng đồng Laravel, nhưng ở Rails thì rất hiếm. Mình đã sử dụng VueJS được một ít nên xin phép được chia sẻ cách kết hợp Rails và VueJS lại với nhau.

Chúng ta sẽ làm gì

Chúng ta sẽ xây dựng một ứng dụng quản lý nhân viên để có thể thấy được tất cả các action CRUD đơn giản. Mục đích là để sử dụng Vue thao tác với từng actions trong 1 single page. Chúng ta sẽ có thể hiển thị tất cả các nhân viên, tạo/mời nhân viên mới, sửa thông tin của nhân viên, thăng cấp và hạ cấp các nhân viên, hoặc là sa thải. Bài này đòi hỏi bạn phải có kiến thức về Rails.

1. Thêm VueJS vào Rails

Các bạn có thể dùng Bower để lấy VueJS về và cài đặt thêm một ít để dùng, hoặc đơn giản hơn là dùng Gem vuejs-rails. Nếu dùng Gem thì hãy nhớ làm theo Readme trên trang GitHub của nó nhé.

2. Khởi tạo dự án

Chúng ta sẽ tạo ra model Employee kèm theo controller, cùng với đó là file JavaScript và view index của Employee. Như vậy, chúng ta sẽ chỉ cần những files sau:

app/models/employee.rb

app/controllers/employees_controller.rb

app/assets/javascripts/employees.js

app/views/employees/index.html

Và chúng ta sẽ không cần dùng tới 2 actions newedit nên hãy cấu hình cho resource employees trong file config/routes.rb như sau:

resources :employees, except: [:new, :edit]

2 actions trên chúng ta sẽ thực hiện ngay trong trong index. Và tất cả các actions sẽ trả về theo định dạng json.

Chúng ta cũng sẽ tạo file migration cho employee, với các trường name, email,manager như sau:

# 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

Tạo danh sách nhân viên Bước đầu tiên, chúng ta sẽ tạo ra một table để hiển thị danh sách nhân viên. Ta sẽ tạo action index và trả về tất cả nhân viên theo định dạng json:

# 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

Tiếp theo ta sẽ cài đặt view và javascript để load list người dùng từ server:

// 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;
      }
    });
  }
});
<!-- 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>

Đoạn này chúng ta sẽ còn dùng nhiều nên tốt nhất là tách ra 1 component riêng:

<tr>
    <td>{{ employee.name }}</td>
    <td>{{ employee.email }}</td>
    <td>{{ employee.manager }}</td>
</tr>

Tạo thêm component mới

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

// ...

Thêm vào view index template:

<!-- 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>

<!-- ... -->

Chúng ta sẽ sửa lại đoạn này trong view index

<tbody>
    <tr v-for="employee in employees">
        <td>{{ employee.name }}</td>
        <td>{{ employee.email }}</td>
        <td>{{ employee.manager }}</td>
    </tr>
</tbody>

thành

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

<!-- ... -->

Mời nhân viên mới Điều thú vị đã bắt đầu. Để có thể mời mọt nhân viên, chúng ta cần một thêm một row vào table, và nó sẽ bao gồm Name, Email, và Manager status. Component employees sẽ cần một object mới khi nhân viên được tạo, đồng thời một hash errors để handle errors.

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

Tiếp theo, tạo action create trả về object employee nếu như tạo được và errors nếu ngược lại.

# 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

Ta cũng đồng thời thêm method hireEmployee vào Vue instance, và nó sẽ được gọi khi Hire button được clicked. Hàm nãy sẽ tạo một ajax để thêm employee vào hệ thống.

// 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>

<!-- ... -->

Chỉnh sửa nhân viên Ta sẽ thêm button "Edit" vào từng dòng một, và khi click vào, nó sẽ cho chúng ta chỉnh sửa thông tin của nhân viên. Thực hiện update action thì khá giống với create action, ngoại trừ việc chúng ta phải tìm ra employee để sửa chứ không phải là thêm mới vào. update action sẽ được gọi khi click vào "Save" button ở trạng thái chỉnh sửa. Ta cũng sẽ thêm chức năng "Thăng chức/Giáng chức" bằng cách tạo 1 toggle "Promote/Demote".

# 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>

Sa thải nhân viên Một phần nữa là chức năng sa thải nhân viên. Ta sẽ thêm "Fire" button và nó sẽ thực thi method fireEmployee, method này sẽ gọi action destroy

# 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>

Và tất cả đã xong. Nếu bạn có thắc mắc gì, hãy để lại comment bên dưới nhé.