0

React.js - Hướng dẫn dùng react.js với rails

1. Giới thiệu

React.js là một Javascript Framework mới khá là phổ biến được tạo ra bởi Facebook. Trong bài viết chúng ta sẽ thử sử dụng nó để viết một ứng dụng giả lập theo dõi chi phí trong rails như hình 1.png

  • Mỗi bản ghi sẽ có tiêu đề, thời gian, số tiền

  • Debit: Tổng các giá trị nhỏ hơn 0

  • Credit: tổng các giá trị lớn hơn 0

  • Balance: Tổng các giá trị của tất cả bản ghi

  • Khi người dùng tạo một bản ghi, nó sẽ được thêm vào bảng

  • Người dùng có thể sửa các bản ghi, và xóa nó bằng các nut edit, delete ứng với mỗi bản ghi

2. Hướng dẫn cài đặt để tạo ứng dụng

Bước đầu tiên, ta sẽ tạo một project trong rails

    rails new sample_app

Đối với giao diện người dùng trong project này, ta sử dụng bootstrap gem làm theo hướng dẫn như sau Trong Gemfile: cài đặt 2 gem

    gem "bootstrap-sass"
    gem "sass-rails", "~> 5.0"

Trong app/assets/stylesheets/custom.css.scss

    @import "bootstrap-sprockets";
    @import "bootstrap";

Cài đặt gem "react-rails" trong Gemfile để sử dụng thư viện react.js

    gem 'react-rails', '~> 1.0'

Sau đó chạy lệnh

rails g react:install

Nó sẽ tạo cho bạn 1 file components.js ở trong thư mục app/assets/javascripts/components/. Sau khi chạy xong lệnh trên, thì trong file application.js xuất hiện 3 dòng

//= require react
//= require react_ujs
//= require components

3. Tạo Resource

Chúng ta sẽ tạo một record resource gồm có date,title và amount

rails g resource Record title date:date amount:float

Chạy cơ sở dữ liệu:

rake db:create db:migrate

Tạo 2 record trong rails console, và không quên lệnh rails s

Record.create title: 'Record 1', date: Date.today, amount: 500
Record.create title: 'Record 2', date: Date.today, amount: -100

4. Tạo danh sách các records

Trong app/controllers/records_controller.rb ta có

    class RecordsController < ApplicationController
      def index
        @records = Record.all
      end
    end

Trong file apps/views/records/index.html.erb

    <%= react_component "Records", { data: @records } %>

file này sẽ là cầu nối giữa Rails app và React Components

Ta có một list các record được viết như sau, trong app/assets/javascripts/components/records.js.coffee

    @Records = React.createClass
      getInitialState: ->
        records: @props.data
      getDefaultProps: ->
        records: []
      render: ->
        React.DOM.div
          className: 'records'
          React.DOM.h2
            className: 'title'
            'Records'
          React.DOM.table
            className: 'table table-bordered'
            React.DOM.thead null,
              React.DOM.tr null,
                React.DOM.th null, 'Date'
                React.DOM.th null, 'Title'
                React.DOM.th null, 'Amount'
            React.DOM.tbody null,
              for record in @state.records
                React.createElement Record, key: record.id, record: record

Chúng ta cần tạo mới Record component để hiện thị cho mỗi Record, trong app/assets/javascripts/components/record.js.coffee

    @Record = React.createClass
      render: ->
        React.DOM.tr null,
          React.DOM.td null, @props.record.date
          React.DOM.td null, @props.record.title
          React.DOM.td null, amountFormat(@props.record.amount)

Thêm "$" vào trước giá trị amount, app/assets/javascripts/utils.js.coffee

    @amountFormat = (amount) ->
      '$ ' + Number(amount).toLocaleString()

Kết quả: 2.png

tham khảo tại https://github.com/vietanhbkaaa/React.js-with-rails/pull/2/files

5. Create Records

Ta cần tạo một method Create trong controller, trong app/controllers/records_controller.rb ta viết

class RecordsController < ApplicationController
    ...

  def create
    @record = Record.new(record_params)

    if @record.save
      render json: @record
    else
      render json: @record.errors, status: :unprocessable_entity
    end
  end

  private
  def record_params
    params.require(:record).permit(:title, :amount, :date)
  end
end

Sau đó, trong file app/assets/javascripts/components/record_form.js.coffee ta sẽ viết như sau

@RecordForm = React.createClass
  getInitialState: ->
    title: ''
    date: ''
    amount: ''

  valid: ->
    @state.title && @state.date && @state.amount

  handleChange: (e) ->
    name = e.target.name
    @setState "#{ name }": e.target.value

  handleSubmit: (e) ->
    e.preventDefault()
    $.post '', { record: @state }, (data) =>
      @props.handleNewRecord data
      @setState @getInitialState()
    , 'JSON'

  render: ->
    React.DOM.form
      className: 'form-inline'
      onSubmit: @handleSubmit
      React.DOM.div
        className: 'form-group'
        React.DOM.input
          type: 'text'
          className: 'form-control'
          placeholder: 'Date'
          name: 'date'
          value: @state.date
          onChange: @handleChange
      React.DOM.div
        className: 'form-group'
        React.DOM.input
          type: 'text'
          className: 'form-control'
          placeholder: 'Title'
          name: 'title'
          value: @state.title
          onChange: @handleChange
      React.DOM.div
        className: 'form-group'
        React.DOM.input
          type: 'number'
          className: 'form-control'
          placeholder: 'Amount'
          name: 'amount'
          value: @state.amount
          onChange: @handleChange
      React.DOM.button
        type: 'submit'
        className: 'btn btn-primary'
        disabled: !@valid()
        'Create record'

Ta có 3.png Khi điền đầy đủ thông tin, button Create Record sẽ được enable 5.png

Khi tạo ra một record, để hiển thị record đó vào danh sách các record, ta viết thêm một phương thức addRecord trong app/assets/javascripts/components/records.js.coffee

  @Records = React.createClass
    ...
    addRecord: (record) ->
      records = @state.records.slice()
      records.push record
      @setState records: records
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'
        React.createElement RecordForm, handleNewRecord: @addRecord
        React.DOM.hr null
      ...

thao khảo tại: https://github.com/vietanhbkaaa/React.js-with-rails/pull/3/files

6. Xử lý đối với các chỉ số lượng (Amount)

Để tạo các box như hình 6.png với những nhiệm vụ đã được đề cập ở đầu bài viết.

Ta làm như sau:

Trong file # app/assets/javascripts/components/amount_box.js.coffee

 @AmountBox = React.createClass
    render: ->
      React.DOM.div
        className: 'col-md-4'
        React.DOM.div
          className: "panel panel-#{ @props.type }"
          React.DOM.div
            className: 'panel-heading'
            @props.text
          React.DOM.div
            className: 'panel-body'
            amountFormat(@props.amount)

Trong file # app/assets/javascripts/components/records.js.coffee

@Records = React.createClass
    ...
    credits: ->
      credits = @state.records.filter (val) -> val.amount >= 0
      credits.reduce ((prev, curr) ->
        prev + parseFloat(curr.amount)
      ), 0
    debits: ->
      debits = @state.records.filter (val) -> val.amount < 0
      debits.reduce ((prev, curr) ->
        prev + parseFloat(curr.amount)
      ), 0
    balance: ->
      @debits() + @credits()
    ...

phần render của records.js.coffee để hiển thị 3 box trên ta viết

@Records = React.createClass
    ...
    render: ->
      React.DOM.div
        className: 'records'
        React.DOM.h2
          className: 'title'
          'Records'
        React.DOM.div
          className: 'row'
          React.createElement AmountBox, type: 'success', amount: @credits(), text: 'Credit'
          React.createElement AmountBox, type: 'danger', amount: @debits(), text: 'Debit'
          React.createElement AmountBox, type: 'info', amount: @balance(), text: 'Balance'
        React.createElement RecordForm, handleNewRecord: @addRecord
    ...

Kết quả 7.png tham khảo tại: https://github.com/vietanhbkaaa/React.js-with-rails/pull/4

7. Deleting Records

Trước hết ta tạo một phương thức delete trong controller app/controllers/records_controller.rb

class RecordsController < ApplicationController
    ...

    def destroy
      @record = Record.find(params[:id])
      @record.destroy
      head :no_content
    end

    ...
  end

trong mỗi record, ta tạo một button Delete, và method handleDelete

# app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
    handleDelete: (e) ->
      e.preventDefault()
      $.ajax
        method: 'DELETE'
        url: "/records/#{ @props.record.id }"
        dataType: 'JSON'
        success: () =>
          @props.handleDeleteRecord @props.record
    render: ->
      React.DOM.tr null,
        React.DOM.td null, @props.record.date
        React.DOM.td null, @props.record.title
        React.DOM.td null, amountFormat(@props.record.amount)
        React.DOM.td null,
          React.DOM.a
            className: 'btn btn-danger'
            onClick: @handleDelete
            'Delete'

Trong app/assets/javascripts/components/records.js.coffee

@Records = React.createClass
    ...
    deleteRecord: (record) ->
      records = @state.records.slice()
      index = records.indexOf record
      records.splice index, 1
      @replaceState records: records
    render: ->
      ...
      # almost at the bottom of the render method
      React.DOM.table
        React.DOM.thead null,
          React.DOM.tr null,
            React.DOM.th null, 'Date'
            React.DOM.th null, 'Title'
            React.DOM.th null, 'Amount'
            React.DOM.th null, 'Actions'
        React.DOM.tbody null,
          for record in @state.records
            React.createElement Record, key: record.id, record: record, handleDeleteRecord: @deleteRecord

kết quả 1.png tham khảo tại https://github.com/vietanhbkaaa/React.js-with-rails/pull/5

8. Editing Records

Trong phần này ta sẽ tối ưu một số đoạn code bằng cách sử dụng đoạn lệnh sau trong config/application.rb

...
  module Accounts
    class Application < Rails::Application
      ...
      config.react.addons = true
    end
  end

restart sever, sau đó tối ưu một số đoạn code như sau:

# app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
  ...
  addRecord: (record) ->
    records = React.addons.update(@state.records, { $push: [record] })
    @setState records: records
  deleteRecord: (record) ->
    index = @state.records.indexOf record
    records = React.addons.update(@state.records, { $splice: [[index, 1]] })
    @replaceState records: records

Thêm method update trong controller # app/controllers/records_controller.rb

class RecordsController < ApplicationController
    ...
    def update
      @record = Record.find(params[:id])
      if @record.update(record_params)
        render json: @record
      else
        render json: @record.errors, status: :unprocessable_entity
      end
    end
    ...
  end

Trong # app/assets/javascripts/components/record.js.coffee

@Record = React.createClass
    ...
    handleEdit: (e) ->
      e.preventDefault()
      data =
        title: React.findDOMNode(@refs.title).value
        date: React.findDOMNode(@refs.date).value
        amount: React.findDOMNode(@refs.amount).value
      # jQuery doesn't have a $.put shortcut method either
      $.ajax
        method: 'PUT'
        url: "/records/#{ @props.record.id }"
        dataType: 'JSON'
        data:
          record: data
        success: (data) =>
          @setState edit: false
          @props.handleEditRecord @props.record, data
    ...

Trong # app/assets/javascripts/components/records.js.coffee viết

@Records = React.createClass
    ...
    updateRecord: (record, data) ->
      index = @state.records.indexOf record
      records = React.addons.update(@state.records, { $splice: [[index, 1, data]] })
      @replaceState records: records
    ...
    render: ->
      ...
      # almost at the bottom of the render method
      React.DOM.table
        React.DOM.thead null,
          React.DOM.tr null,
            React.DOM.th null, 'Date'
            React.DOM.th null, 'Title'
            React.DOM.th null, 'Amount'
            React.DOM.th null, 'Actions'
        React.DOM.tbody null,
          for record in @state.records
            React.createElement Record, key: record.id, record: record, handleDeleteRecord: @deleteRecord, handleEditRecord: @updateRecord

Kết quả 3.png Tham khảo tại: https://github.com/vietanhbkaaa/React.js-with-rails/pull/6

9. Kết luận

Trong bài viết này, chúng ta nắm được những bước cơ bản của sử dụng react.js trong rails. Cách sử dụng các phương thức create, update, destroy của react.js. Đây là một công nghệ đáng để thử.

10. Tài liệu tham khảo

https://www.airpair.com/reactjs/posts/reactjs-a-guide-for-rails-developers

https://facebook.github.io/react/docs/tutorial.html


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí