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ả ...
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 new và edit 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; } }); } });
<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:
<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 } }) } }
<tr> <td> <input type="text" v-model="employee.name"><br> <span style="color:red">{{ errors.name }}</span> </td> <td> <input type="text" v-model="employee.email"><br> <span style="color:red">{{ errors.email }}</span> </td> <td><input type="checkbox" v-model="employee.manager"></td> <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 } }) } }
<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>
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() } }) } // ...
<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é.