Xây dựng giao diện CRUD với React và Ruby on Rails

1. Lời mở đầu

Công nghệ Javascript hiện nay đang ngày càng trở nên huyên náo hơn bao giờ hết. Các framework mới ra đời hằng ngày, các lập trình viên đều đang đắn đo về công cụ mà họ nên chọn, và việc xây dựng các giao diện người dùng cũng đang trải qua rất nhiều sự thay đổi mạnh mẽ. Trên hết, những lập trình viên Rails như chúng ta đều biết rằng thiết kế view bằng .erb rồi sẽ trở nên lỗi thời và sẽ được thay thế bởi các framework phức tạp hơn rất nhiều được dựa trên nền tảng Javascript. Với tất cả những điều trên, việc lựa chọn một công nghệ để ta có thể chuyên sâu nghiên cứu là không hề dễ dàng. May mắn thay, công nghệ React của Facebook cung cấp cho chúng ta một tia hy vọng đầy hứa hẹn về một cách tiếp cận mới trong xây dựng giao diện người dung. Hơn thế nữa, Rails có thể được tích hợp liên tục với React, và rất dễ dàng để cài đặt một ứng dụng sử dụng Rails API và xây dựng tầng giao diện với React. Vậy hãy cùng xem làm thế nào để xây dựng một ứng dụng Rails API đơn giản bao gồm đầy đủ chức năng Thêm, Đọc, Sửa và Xóa cho một Model với React.

2. Thiết lập một ứng dụng Rails API đơn giản

2.1. Thiết lập Model

Chúng ta sẽ sử dụng một cách tiếp cận khác cho mô hình của ứng dụng. Chúng ta chỉ sư dụng Rails để sinh ra dữ liệu dưới dạng JSON trong khi đó React sẽ xử lý tầng giao diện và biểu diễn dữ liệu. Điều này sẽ tách ứng dụng của Rails ra khỏi tầng giao diện.

Đầu tiên, hãy khởi tạo một ứng dụng Rails rỗng:

$ rails new item_cart

Bây giờ, hãy tạo model mà chúng ta muốn xây dựng giao diện CRUD:

$ rails g model Item name:string description:text

Dòng lệnh trên sẽ tạo ra một model với thuộc tính ‘name’ và ‘description’, và một migration cho cơ sở dữ liệu. Giờ chúng ta chỉ cần them model vào trong Schema của cơ sở dữ liệu bằng câu lệnh:

$ rake db:migrate

2.2. Tạo một vài dữ liệu Seed

Đã đến lúc tạo ra một vài dữ liệu mẫu cho cơ sở dữ liệu! Chúng ta cần phải đảm bảo rằng nhưng gì chúng ta đang xây dựng sẽ hoạt động đúng chức năng. Đoạn script sau sẽ sinh ra dữ liệu mẫu cho cơ sở dữ liệu với khoảng 10 bản ghi.

#db/seeds.rb 
10.times { Item.create!(name: "Item", description: "I am a description.") }

$ rake db:seed

2.3. Thiết lập Controller

Đầu tiên chúng ta cần cài đặt gem ‘responders’, cho phép ta ứng dụng respond_to tới tất cả các action trong Controllers, làm cho code gọn gang, tối ưu hơn. Hãy đặt gem vào trong Gemfile rồi chạy bundle: gem 'responders'

$ bundle

Thứ hai, chúng ta sẽ có một điều chỉnh nhỏ tới controller của ứng dụng. Ngoài trừ việc ném ra exception, chúng ta sẽ để controller ném ra một null session bởi chúng ta sẽ gửi yêu cầu nhận JSON, điều này khác với html (mặc định được yêu).

#application_controller.rb 
class ApplicationController < ActionController::Base 
    protect_from_forgery with: :null_session 
end

Vì đây là ứng dụng xây dựng theo nền tảng API, chúng ta sẽ xây dựng các controller mà sử dụng namespaces. Theo quy ước, chúng ta phải đặt controller cho các namespace khác nhau ở các thư mục tương ứng với chúng. Ví dụ, tất cả các controller có namespace api phải được đặt trong thư mục đặt tên là 'api'. Trong thư mục 'controller' của ứng dụng, ta sẽ tạo một thư mục tên 'api' và trong thư mục đó tạo thư mục tên 'v1' :

app/controllers/api/v1

Theo đường dẫn 'app/controllers/api/v1, chúng ta sẽ tạo 2 controller. Base_controller sẽ bao gồm các quy định áp dụng cho toàn bộ các controller API của chúng ta.

#base_controller.rb 
class Api::V1::BaseController < ApplicationController 
    respond_to :json 
end

Phương thức respond_to đảm bảo rằng tất cả các action trong controller kế thừa từ base_controlller sẽ phản hồi lại với JSON. Đây là một cách tiếp cận chuẩn khi xây dựng các API dựa trên JSON. Sau khi hoàn thành base_controller, chúng ta tạo một controller cho model Item. Chúng ta cho controller thừa kế từ base_controller và tạo các action theo tiêu chuẩn: index, create, updatedestroy.

#items_controller.rb 

class Api::V1::ItemsController < Api::V1::BaseController 
    def index 
        respond_with Item.all 
    end 

    def create 
        respond_with :api, :v1, Item.create(item_params) 
    end 

    def destroy 
        respond_with Item.destroy(params[:id]) 
    end 

    def update 
        item = Item.find(params["id"]) 
        item.update_attributes(item_params) 
        respond_with item, json: item 
    end 

    private 
    def item_params 
        params.require(:item).permit(:id, :name, :description) 
    end 
end

Phương thức respond_with là một phần của gem 'responders' và sẽ trả về một đối tượng dạng JSON với kết quả của từng action trong controller. Việc định tuyến cho controller phải được xem xét đặt trong 2 namespace APIV1. Chúng ta sẽ thực hiện bằng cách sử dụng phương thức namespace:

2.4. Định tuyến cho controller

#app/config/routes.rb 

Rails.application.routes.draw do 
    namespace :api do 
        namespace :v1 do 
            resources :items, only: [:index, :create, :destroy, :update] 
        end 
    end 
end

Để xem mọi thứ hoạt động như nào, hãy truy nhập đến:

http://localhost:3000/api/v1/items.json

Nếu bạn thấy một mảng gồm các đối tượng JSON thì mọi thứ đang hoạt động đúng rồi đó!

Chúng ta đã xong với việc tạo API và giờ đến lúc render nó với React. Hãy tạo một giao diện tĩnh cho ứng dụng của chúng ta. Đầu tiên, chúng ta phải tạo một controller chỉ phục vụ cho mục đích render ra giao diện tĩnh.

#app/controllers/site_controller.rb 

class SiteController < ApplicationController 
    def index 
    end 
end

#app/config/routes.rb
Rails.application.routes.draw do
  root to: 'site#index'

2.5. Thêm React vào trong Rails

Thêm gem 'react-rails' vào trong Gemfile gem 'react-rails'

$ bundle
$ rails g react:install

React:install generator sẽ tự động bao gồm thư viện react Javascript vào trong asset pipeline.

$ rails g react:install 
    create app/assets/javascripts/components 
    create app/assets/javascripts/components/.gitkeep 
    insert app/assets/javascripts/application.js 
    insert app/assets/javascripts/application.js 
    insert app/assets/javascripts/application.js 
    create app/assets/javascripts/components.js

Giống như JQuery và các thư viện Javascript khác, reactreact_ujs đã được bao gồm trong asset pipeline. Thư mục ‘components’ trong đường dẫn **** 'assets/javascripts'**** là nơi chứa tất cả các component của chúng ta. Component là các phần xây dựng của framework React. Chúng được dùng để chia giao người dùng thành các phần khác nhau và xây dựng một mối quan hệ parent-child, giống như cách hoạt động của các controller lồng nhau trong AngularJS.

Ví dụ, nếu bạn muốn xây dựng một layout đơn giản với body, header và một list các item, thì cây phân cấp của chúng được biểu diễn như hình dưới đây:

<Main /> render ra <Header /><Body /> và gửi dữ liệu xuóng theo phân cấp . <Header /> có thể nhận được thông tin về người dụng hiện tại và menu, còn <Body /> có thể nhận được một mảng các item. Component <Items /> sẽ nhận dữ liệu và tạo ra danh sách các component <Item /> chứa thông tin về từng object Item đơn lẻ. Component <Attributes /> sẽ chứa thông tin về các item và <Actions /> sẽ chứa nút xóa và sửa. Để tạo ra các component của React, chúng ta cần thêm react_component của view helper vào đường dẫn gốc của chúng ta.

#app/views/site/index.html.erb

<%= react_component 'Main' %>

Ở đây, react_component là một phần của react-rails, nó được dùng để đưa component Main từ thư mục các component trong 'assets' vào trong view.

2.6. Component đầu tiên

Điều đầu tiên chúng ta cần làm là thiết lập 1 file jsx trong thư mục component của chúng ta.

// app/assets/javascripts/components/_main.js.jsx 
var Main = React.createClass({ 
    render() { 
        return ( 
            <div> 
                <h1>Hello, World!</h1>
            </div> 
        ) 
    } 
});

File js.jsx trong các component của React hoạt động giống như html.erb trong Rails, đó là phần đuôi mở rộng dùng để nhận biết các file View của framework. Component này chỉ có duy nhất một phương thức: render(). Trong trường hợp này, chúng được dùng để trả về các dòng html tĩnh cho trang web. Phương thức render() cũng được dùng để gọi đến các hàm render() của tất các component con từ component, cha, và cuối cùng sẽ in tất cả các component lên trang web. Từng component của React chỉ có thể trả về một phần tử, vậy nên tất cả các phần tử jsx trong hàm return cần được gói trong 1 div.

Component <Main /> có 2 component con; <Header /><Body />. Hãy bắt đầu với <Header />

// app/assets/javascripts/components/_header.js.jsx 
var Header = React.createClass({ 
    render() { 
        return ( 
            <div> 
                <h1>Hello, World!</h1> 
            </div> 
        )
    } 
});

Và sau đó thay đổi component <Main /> để nó có thể render ra <Header /> trong hàm render.

// app/assets/javascripts/components/_main.js.jsx 
var Main = React.createClass({ 
    render() { 
        return ( 
            <div> 
                <Header /> 
            </div> 
        ) 
    } 
});

Và vừa rồi chúng ta đã lồng hai component vào với nhau.

2.7. Render ra tất cả các item

Như đã được đề cập trước đó, tất cả các item sẽ được liệt kê trong component <Body />. Component <Body /> sẽ bao gồm form để điền thêm các item mới. Sau đây là danh sách các file cần được tạo:

app/assets/javascripts/components/_body.js.jsx
app/assets/javascripts/components/_all_items.js.jsx
app/assets/javascripts/components/_new_items.js.jsx

Đầu tiên, hãy bắt đầu với việc liệt kê tất cả các item. Việc liệt kê các item sẽ bao gồm gửi một request lên server để lấy tất cả các item về cho component của chúng ta bằng cách sử dụng AJAX request. Chúng ta cần làm điều này khi component được render vào trong cây DOM. React có sẵn rất nhiều phương thức để xử lý các sự kiện xảy ra trong vòng đời của một component. Bao gồm các phương thức được thực thi trước và sau khi component được gắn vào cây DOM hoặc trước và sau khi chúng được gỡ bỏ. Chúng ta sẽ sử dụng phương thức componentDidMount(), được gọi ngay sau khi component được gắn vào DOM. Bạn có thể tìm hiểu thêm về các phương thức khác và cách sử dụng chúng trong tài liệu của React.

// app/assets/javascripts/components/_all_items.js.jsx 
var AllItems = React.createClass({ 
    componentDidMount() { 
        console.log('Component mounted'); 
     },
     
     render() { 
         return ( 
             <div> 
                 <h1>All items component</h1> 
             </div> 
         ) 
     } 
 });

Sau đây là cách mà bạn triển khai phương thức componentDidMount(), Hãy chú ý đến việc các phương thức được tách ra như nào. Nếu nhìn vào hàm React.createClass, chúng ta sẽ thấy chúng được định nghĩa như các thuộc tính của đối tượng, và chúng cần được phân cách bởi các dấu phẩy. Đừng lo nếu bạn không nhìn thấy các message từ console.log() ở trong ứng dụng của bạn - chúng ta vẫn chưa thêm nó vào component cha và nó sẽ không được gắn vào cây DOM.

Trước khi lấy dữ liệu từ server, chúng ta cần biết cách mà dữ liệu được lưu trong component. Khi một component được gắn thì dữ liệu của nó phải được khởi tạo. Điều này được làm bằng cách sử dụng phương thức getInitialState().

// app/assets/javascripts/components/_all_items.js.jsx 
var AllItems = React.createClass({ 
    getInitialState() { 
        return { items: [] } 
    },

Bây giờ, chúng ta cần lấy dữ liệu từ trên server về và gắn chúng vào các object item. Và chúng ta làm như sau:

// app/assets/javascripts/components/_all_items.js.jsx 
getInitialState() { 
    return { items: [] } 
}, 

componentDidMount() { 
    $.getJSON('/api/v1/items.json', (response) => { this.setState({ items: response }) }); 
},

Chúng ta sử dụng phương thức getJSON với URL ‘items.json’ là tham số, và chúng ta dùng hàm setState của component để gán các dữ liệu trả về vào các object item.

Vậy là chúng ta đã lấy được dữ liệu về các item, nhưng giờ chúng ta sẽ render chúng như thế nào? Chúng ta sẽ duyệt mảng các item đó trong phương thức render().

// app/assets/javascripts/components/_all_items.js.jsx 
//getInitialState and componentDidMount 
render() { 
    var items= this.state.items.map((item) => { 
        return ( 
            <div> 
                <h3>{item.name}</h3> 
                <p>{item.description}</p> 
            </div> 
        ) 
    }); 
    
    return( 
        <div> 
            {items} 
         </div> 
     ) 
}

Phương thức map gần giống như phương thức each ở trong template .erb. Nó duyệt từng phần tử của mảng và biểu diễn các thuộc của item sử dụng kí hiệu ngoặc nhọn. Cặp dấu ngoặc này tương đương với cặp thẻ <%= %> của Rails. Chúng được dùng để đưa các thuộc tính của item vào trong html, khiến chúng html trở thành động. Và cuối cùng chúng cũng trả về các biến của item là các phần tử DOM cùng với thuộc tính của item đó được bọc trong các phần tử html.

Nhưng chúng ta chưa xong ở đây. Khi chúng ta duyệt từng phần tử của item trong React, cần phải có cách để phân biệt các item đó ở các component trong DOM. Để làm được điều đó, chúng ta dùng một thuộc tính duy nhất cho từng phần tử item, được biết đến là key. Để thêm key vào cho các item, chúng ta cần dùng thuộc tính key cho từng thẻ div bao quanh nó, như sau:

// app/assets/javascripts/components/_all_items.js.jsx 
    var items= this.state.items.map((item) => { 
        return ( 
            <div key={item.id}> 
                <h3>{item.name}</h3> 
                <p>{item.description}</p> 
            </div> 
        ) 
    }); 
    return( 
        <div> 
            {items} 
        </div> 
    ) 
}

Bạn có để ý các thuộc tính key trong thẻ div được dùng trong phương thức iterator duyệt các biến item? Các key đóng một vai trò quan trọng trong các list hay các phần tử tương tự, chúng cho ta một cách để phân biệt vào tương tác. Để hiểu thêm về keys các bạn hãy đọc thêm tài liệu của React.

Giờ kiểm tra xem mọi thứ có hoạt động hay không. Đầu tiên, <Body />, component cha của <AllItems /><NewItems/> phải được đặt trong component <Main />.

// app/assets/javascripts/components/_main.js.jsx 
var Main = React.createClass({ 
    render() {
        return ( 
            <div> 
                <Header /> 
                <Body /> 
            </div> 
        )
    }
});

Chúng ta phải thêm các component <AllItems /> và <NewItem /> vào trong component body. Trong component <Body />, chúng ta sẽ thêm các component còn lại tương ứng như sau:

// app/assets/javascripts/components/_body.js.jsx 
var Body = React.createClass({ 
    render() { 
        return ( 
            <div> 
                <NewItem /> 
                <AllItems /> 
            </div> 
        )
    } 
});

Và giờ chúng ta đã có thể thấy các item được hiển thị trên giao diện.

2.8. Thêm mới một item

Giờ đến lúc chúng ta chuyển đến một file mà chúng ta đã tạo trước đó:

//app/assets/javascripts/components/_new_item.js.jsx 
var NewItem= React.createClass({ 
    render() { 
        return ( 
            <div> 
                <h1>new item</h1> 
            </div> 
        )
    } 
});

Điều gì cần thiết để tạo được một item? Chúng ta cần tạo ra 2 trường input và gửi chúng lên server thông qua request POST. Khi một item mới được tạo, chúng ta cần tải lại danh sách các item để chúng bao gồm cả item mới được thêm vào.

Hãy thêm các trường của form vào và một nút để xử lý việc submit:

// app/assets/javascripts/components/_new_item.js.jsx 
var NewItem= React.createClass({ 
    render() { 
        return ( 
            <div> 
                <input ref='name' placeholder='Enter the name of the item' /> 
                <input ref='description' placeholder='Enter a description' /> 
                <button>Submit</button> 
            </div> 
            )
        ) 
    }
});

Mọi thứ dường như đã rất quen thuộc, ngoại trừ thuộc tính ref. Thuộc tính ref được dùng để tham chiếu phần tử trong component. Chức năng của nó giống như thuộc tính name trong AngularJS. Thay vì tìm phần tử thông qua id hoặc classs, chúng ta sẽ sử dụng ref. Trong trường hợp cụ thể này, ref sẽ được dùng để lấy giá trị của các trường text và gửi chúng lên server.

Nếu click vào nút submit, bạn sẽ thấy rằng chẳng có chuyện gì xảy ra. Vậy chúng ta cần thêm một phương thức xử lý sự kiện. Để làm được điều này, chúng ta cần thay thế code html của button.

<button onClick={this.handleClick}>Submit</button>

Sau khi thay thế xong, mỗi lần click vào button, component sẽ đi tìm hàm handleClick(). Chúng ta cần định nghĩa nó trong file Javascript.

// app/assets/javascripts/components/_new_item.js.jsx 
// var NewItem = … 
handleClick() { 
    var name = this.refs.name.value; 
    var description = this.refs.description.value; 
    
    console.log('The name value is ' + name + 'the description value is ' + description); 
}, 

//render()..

Lần này nếu bạn gõ text vào trong trường input và click vào button, nó sẽ in các giá trị của ô input vào trong cửa sổ console của Javascript. Ở đây bạn có thể thấy cách thuộc ref được sử dụng để lấy giá trị của các ô input. Thay vì gửi các giá trị đến console, chúng ta sẽ gửi chúng lên server. Và điều đó được làm như sau:

// app/assets/javascripts/components/_new_item.js.jsx 
var NewItem= React.createClass({ 
    handleClick() { 
        var name = this.refs.name.value; 
        var description = this.refs.description.value; 
        $.ajax({ 
            url: '/api/v1/items', 
            type: 'POST', 
            data: { item: { name: name, description: description } }, 
            success: (response) => { 
                console.log('it worked!', response); 
            } 
        }); 
    }, 
    
    render() { 
        return ( 
            <div> 
                <input ref='name' placeholder='Enter the name of the item' /> 
                <input ref='description' placeholder='Enter a description' /> 
                <button onClick={this.handleClick}>Submit</button> 
            </div> 
        )
    } 
});

Chúng ta gửi một request POST thông qua URL sử dụng $.ajax. Response trả về sẽ bao gồm một object với thuộc tính name và description.

Có một chút vấn đề ở đây. Chúng ta cần phải khởi động lại trang web để có thể thấy được item mới. Vậy làm sao để nó tốt hơn? <NewItem /><AllItems /> không thể tự truyền dữ liệu qua lại cho nhau vì chúng ở cùng một cấp. Như được biết, chúng ta chỉ có thể gửi dữ liệu theo xuống theo chiều cây phân cấp. Điều này có nghĩa chúng ta cần chuyển việc lưu dữ liệu về các item trong state của <AllItems/> lên một bậc, đó là ở component <Body />.

Chuyển hàm getInitialState()componentDidMount() từ <AllItems/> lên <Body />. Bây giờ các item sẽ được lấy khi <Body /> render. Chúng ta có thể gửi các biến xuống các component con với props. Props là bất biến ở component con, và để gọi được nó, ta dùng this.props. Trong trường này, thay vì dùng this.state.items, chúng ta sẽ dùng this.props.items.

Và đây là cách chúng ta gửi dữ liệu của các item từ <Body /> xuống <AllItems />:

// app/assets/javascripts/components/_body.js.jsx

<AllItems items={this.state.items} />

Đây là cách tham chiếu đến chúng trong <AllItems /> :

// app/assets/javascripts/components/_all_items.js.jsx 

var items= this.props.items.map((item) => {

Chúng ta cũng có thể truyền các hàm như một thuộc tính từ component cha đến component con. Hãy làm điều đó với hàm handleSubmit() trong <NewItem />. Giống như mảng các item, chúng ta cũng sẽ chuyển hàm lên cha của nó, component <Body />.

// app/assets/javascripts/components/_body.js.jsx 
// getInitialState() and componentDidMount() 
    handleSubmit(item) { 
        console.log(item); 
    }, 
// renders the AllItems and NewItem component

Sau đó hãy tham chiếu hàm trong component con giống như ta đã làm với mảng các item:

// app/assets/javascripts/components/_body.js.js

<NewItem handleSubmit={this.handleSubmit}/>

Trong component <NewItem />, chúng ta sẽ truyền hàm như một phần của this.props và truyền object từ AJAX request như một tham số của component cha.

// app/assets/javascripts/components/_new_item.js.jsx 

handleClick() { 
    var name = this.refs.name.value; 
    var description = this.refs.description.value; 
    $.ajax({ 
        url: '/api/v1/items', 
        type: 'POST', 
        data: { item: { name: name, description: description } }, 
        success: (item) => { 
            this.props.handleSubmit(item); 
        } 
    }); 
}

Giờ khi bạn click vào nút submit, cửa sổ console của Javascript sẽ ghi log object mà chúng ta vừa tạo. Chúng ta chỉ cần them item mới vào trong mảng các item thay vì ghi ra cửa sổ console.

// app/assets/javascripts/components/_body.js.jsx 
// getInitialState() and componentDidMount() 
    handleSubmit(item) { 
        var newState = this.state.items.concat(item); 
        this.setState({ items: newState }) 
    },
// renders the AllItems and NewItemcomponent

Mọi thứ đến giờ có lẽ đã hoạt động đúng. Do đã có rất nhiều đoạn code được cập nhật lại từ các bước trên, dưới đây là các file đã được cập nhật để bạn có thể kiểm tra lại:

// app/assets/javascripts/components/_all_items.js.jsx 

    var AllItems = React.createClass({ 
        render() { var items= this.props.items.map((item) => {
            return ( 
                <div key={item.id}> 
                    <h3>{item.name}</h3> 
                    <p>{item.description}</p> 
                </div>
            ) 
        }); 

        return( 
            <div> 
                {items} 
            </div> 
        ) 
    } 
});

// app/assets/javascripts/components/_body.js.jsx 
var Body = React.createClass({ 
    getInitialState() { 
        return {
            items: [] 
        } 
    }, 
    
    componentDidMount() { 
        $.getJSON('/api/v1/items.json', (response) => { this.setState({ items: response }) }); 
    }, 
    
    handleSubmit(item) { 
        var newState = this.state.items.concat(item);
        this.setState({ items: newState })
    }, 
    
    render() { 
        return (
            <div> 
                <NewItem handleSubmit={this.handleSubmit}/> 
                <AllItems items={this.state.items} />
            </div> 
        )
    } 
});

// app/assets/javascripts/components/_new_item.js.jsx 
var NewItem= React.createClass({ 
    handleClick() { 
        var name = this.refs.name.value; 
        var description = this.refs.description.value; 
        $.ajax({ 
            url: '/api/v1/items', 
            type: 'POST', 
            data: { item: { name: name, description: description } }, 
            success: (item) => { 
                this.props.handleSubmit(item); 
            } 
        }); 
    }, 
    
    render() { 
        return ( 
            <div> 
                <input ref='name' placeholder='Enter the name of the item' /> 
                <input ref='description' placeholder='Enter a description' /> 
                <button onClick={this.handleClick}>Submit</button> 
            </div> 
        )
    } 
});

2.9. Xóa một item

Xóa một item gần giống như là tạo ra một cái mới. Điều chúng ta cần làm là thêm một button và một hàm để xử lý sự kiện click xóa một item trong component <AllItems />.

// app/assets/javascripts/components/_all_items.js.jsx 
var AllItems = React.createClass({ 
    handleDelete() { 
        console.log('delete item clicked'); 
    }, 
    
    render() { 
        var items= this.props.items.map((item) => { 
            return ( 
                <div key={item.id}> 
                    <h3>{item.name}</h3> 
                    <p>{item.description}</p> 
                    <button onClick={this.handleDelete}>Delete</button> 
                </div> 
            )
        }); 
        
        return( 
            <div> 
                {items} 
            </div> 
        ) 
    } 
});

Thứ hai, chúng ta cần truyền tham chiếu đến một hàm từ component cha <Body />, nơi ta sẽ cập nhật lại danh sách các item khi nút Xóa được click.

// app/assets/javascripts/components/_body.js.jsx 

    handleDelete() { 
        console.log('in handle delete'); 
    }, 
    
    render() { 
        return ( 
            <div> 
                <NewItem handleSubmit={this.handleSubmit}/> 
                <AllItems items={this.state.items} handleDelete={this.handleDelete}/> 
            </div> 
        )
    } 
});

Giờ chúng ta chỉ cần truyền tham chiếu của hàm trong component cha xuống component con thông qua props:

// app/assets/javascripts/components/_all_items.js.jsx 
var AllItems = React.createClass({ 
    handleDelete() { 
        this.props.handleDelete(); 
    }, 
    
    render() { 
        //Phần còn lại của component

Mọi thứ có vẻ đã ổn, nhưng làm thế nào để biết đâu là item chúng ta cần xóa? Chúng ta sẽ sử dụng phương thức bind(). Phương thức bind() sẽ ràng buộc id của item vào this, biến id được gửi đi như một tham số.

// app/assets/javascripts/components/_all_items.js.jsx 

handleDelete(id) { 
    this.props.handleDelete(id);
}, 

render() { 
    var items= this.props.items.map((item) => { 
        return ( 
            <div key={item.id}> 
                <h3>{item.name}</h3> 
                <p>{item.description}</p> 
                <button onClick={this.handleDelete.bind(this, item.id)} >Delete</button> 
            </div> 
        )
    });

Giờ chúng ta có thể gửi id như một tham số, bước 2 đã xong. Điều thứ 3 cần làm là tạo ra một AJAX request để xóa item khỏi cơ sở dữ liệu

// app/assets/javascripts/components/_body.js.jsx 
    handleDelete(id) { 
        $.ajax({ 
            url: `/api/v1/items/${id}`, 
            type: 'DELETE', 
            success(response) { 
                console.log('successfully removed item') 
            } 
        }); 
    },

Mọi thứ đều hoạt động được, nhưng chúng ta lại gặp phải cùng một vấn đề như khi thêm mới một item. Chúng ta cần phải khởi động lại trang web để có thể thấy được kết quả. Bước cuối cùng chúng ta sẽ thay đổi điều này, chúng ta sẽ cập nhật lại state bằng cách xóa item ra khỏi mảng các item khi request xóa đến cơ sở dữ liệu được thực hiện thành công.

// app/assets/javascripts/components/_body.js.jsx 
handleDelete(id) { 
    $.ajax({ 
        url: `/api/v1/items/${id}`, 
        type: 'DELETE', 
        success:() => { 
            this.removeItemClient(id);
        } 
    }); 
}, 

removeItemClient(id) { 
    var newItems = this.state.items.filter((item) => { 
        return item.id != id; 
    });
    
    this.setState({ items: newItems });
},

Nhớ rằng hãy thay cú pháp của hàm success từ success (response) {} sang success: () => {}, nếu không bạn sẽ thiên về việc mong muốn có dữ liệu trả về từ server thay vì xử lý trực tiếp trên component.

2.10. Sửa một item

Điều cuối cùng chúng ta làm đó là sửa và cập nhật một item. Chúng ta sẽ thêm một nút edit và sự kiện cho nó. Khi nút edit được click, item sẽ vào trạng thái edit khi mà các ô hiển thị text sẽ biến thành các trường input Text. Khi các thay đổi được submit, chúng ta sẽ tạo một AJAX request và nếu thành công, item sẽ được lưu với các thuộc tính mới.

Đầu tiên, chúng ta sẽ thêm nút edit và sự kiện của nó.

// app/assets/javascripts/components/_all_items.js.jsx 
handleEdit() { 
}, 

//render() và phần còn lại của template 
<button onClick={this.handleEdit()}> Edit </button>

Khi ở trong trạng thái edit, code cho mỗi một item sẽ trở nên rất nhiều và theo quy tắc phân tách, chúng ta sẽ phải chuyển chúng thành các component riêng biệt và đặt tên cho chúng. Component này sẽ được dùng để chứa thông tin và phương thức cho một item đơn lẻ. Hàm handleEdit()handleDelete() sẽ được tham chiếu đến như một thuộc tính của component và như vậy sẽ được tham chiếu bằng cách sử dụng this.props:

// app/assets/javascripts/components/_all_items.js.jsx 
render() { 
    var items= this.props.items.map((item) => { 
        return ( 
            <div key={item.id}> 
                <Item item={item} 
                    handleDelete={this.handleDelete.bind(this, item.id)} 
                    handleEdit={this.handleEdit}/> 
            </div> 
        )
    });
    

Template <Item /> sẽ như sau:

// app/assets/javascripts/components/_item.js.jsx 

var Item = React.createClass({ 
    render() { 
        return ( 
            <div> 
                <h3>{this.props.item.name}</h3> 
                <p>{this.props.item.description}</p> 
                <button onClick={this.props.handleDelete} >Delete</button> 
                <button onClick={this.props.handleEdit}> Edit </button> 
            </div> 
        ) 
    }
});

Code html giống như ta đã dùng trước đó, nhưng các thuộc tính và hàm được truy cập đến thông qua this.props vì chúng đã được tham chiếu đến như một thuộc tính của template. Bây giờ hãy kiểm tra lại chức năng để chắc chắn mọi thứ sẽ hoạt động.

Tiếp đến, chúng ta sẽ chuyển hàm handleEdit() vào trong template <Item />. Chúng ta sẽ sử dụng một biến Boolean để bật tắt mỗi khi click vào button. Nếu biến được gán bằng true, các ô text sẽ biến thành các trường input và ngược lại.

Hãy chuyển hàm handleEdit() vào <Item />. Chúng ta có thể làm nó một cách đơn giản bằng cách viết phương thức vào trong component và thay đổi thuộc tính của button từ this.props.handleEdit thành this.handleEdit

// _app/assets/javascripts/components/_item.js.jsx
<button onClick={this.handleEdit}> Edit </button>

Và sau đó khởi tạo method trong component.

// app/assets/javascripts/components/_item.js.jsx 
var Item = React.createClass({ 
    handleEdit() { 
        console.log('edit button clicked')
    },

Giờ chúng ta sẽ có biến state tên là editable this.state.editable mà ban đầu chúng ta sẽ gán bằng false và gán bằng true khi nút edit được click.

// app/assets/javascripts/components/_item.js.jsx 
var Item = React.createClass({ 
getInitialState() { 
    return {editable: false} }, 
    handleEdit() { 
    this.setState({editable: !this.state.editable}) 
}, 

render() {

Ở đây chúng ta sử dụng toán tử 3 ngôi, chúng ta sẽ render các phần tử khác nhau dựa trên giá trị của this.state.editable. Chúng ta làm nó như sau:

// app/assets/javascripts/components/_item.js.jsx 
render() { 
    var name = this.state.editable ? <input type='text' defaultValue={this.props.item.name} /> : <h3>{this.props.item.name}</h3>; 
    var description = this.state.editable ? <input type='text' defaultValue={this.props.item.description} />: <p>{this.props.item.description}</p>; 
    return ( 
        <div>
            {name}
            {description} 
            <button onClick={this.props.handleDelete} >Delete</button> 
            <button onClick={this.handleEdit}> Edit </button> 
            
            …
            

Biến name và description đã trở thành động, chúng sẽ thay đổi tùy theo trạng thái của this.state.editable. Hãy làm điều đó với nút edit:

// app/assets/javascripts/components/_item.js.jsx 

<button onClick={this.handleEdit}> {this.state.editable ? 'Submit' : 'Edit' } </button>

Có điều gì đó thiếu ở trường input, chúng ta sẽ tham chiếu chúng trong phương thức của component như ta đã làm trong <NewItem />. Cần phải có thuộc tính ref ở trường input. Hãy thêm thuộc tính này vào trường input và tham chiếu chúng đến hàm handleEdit().

// app/assets/javascripts/components/_item.js.jsx 

render() { 
   var name = this.state.editable ? <input type='text' ref='name' defaultValue={this.props.item.name} /> : <h3>{this.props.item.name}</h3>; 
   var description = this.state.editable ? <input type='text' ref='description' defaultValue={this.props.item.description}/>: <p>this.props.item.description}</p>;

Bây giờ, hãy thêm code để tham chiếu các trường input ở trong phương thức

// app/assets/javascripts/components/_item.js.jsx 

handleEdit() { 
    if(this.state.editable) { 
        var name = this.refs.name.value; 
        var description = this.refs.description.value;
        console.log('in handleEdit', this.state.editable, name, description); 
    } 
    this.setState({ editable: !this.state.editable }) },
    

Hãy thử mở cửa sổ console của Javascript và thử nhập vào giá trị cho một item. Khi bạn click vào nút submit, bạn sẽ thấy các giá trị được hiển thị lên màn hình. Chúng ta có giá trị trong <Items /> và chúng ta phải gửi chúng lên <Body /> nơi mảng các item được lưu và đó cũng là nơi chúng ta gọi lên server. Điều này có nghĩa chúng ta sẽ dùng hàm props để truyền dữ liệu lên từ <Item /> qua <AllItems/> và kết thúc ở <Body/>. Hãy bắt đầu từ <Item /> lên <AllItems/>:

// app/assets/javascripts/components/_item.js.jsx 
handleEdit() { 
    if(this.state.editable) { 
        var name = this.refs.name.value; 
        var id = this.props.item.id; 
        var description = this.refs.description.value; 
        var item = {id: id , name: name , description: description};
        this.props.handleUpdate(item); 
    } 
    this.setState({ editable: !this.state.editable }) },
    

Trong <AllItems/> chúng ta sẽ thêm hàm handleUpdate để lấy các thuộc tính trong sự kiện onUpdate và truyền chúng lên thông qua chính thuộc tính onUpdate của nó.

// app/assets/javascripts/components/_all_items.js.jsx 

onUpdate(item) { 
    this.props.onUpdate(item); 
}, 

render() { 
    var items= this.props.items.map((item) => { 
        return ( 
            <div key={item.id}> 
                <Item item={item} handleDelete={this.handleDelete.bind(this, item.id)} handleUpdate={this.onUpdate}/> 
            </div> 
        )
    });

Cuối cùng, <Body /> sẽ lấy thuộc tính onUpdate của <AllItems /> với item trong đó:

// app/assets/javascripts/components/_body.js.jsx 

handleUpdate(item) {
    $.ajax({
        url: `/api/v1/items/${item.id}`, 
        type: 'PUT', data: { item: item }, 
        success: () => { 
            console.log('you did it!!!'); 
            //this.updateItems(item); 
            // callback to swap objects } } )
        }, 
        
render() { 
    return ( 
        <div> 
            <NewItem handleSubmit={this.handleSubmit}/> 
            <AllItems items={this.state.items} handleDelete={this.handleDelete} onUpdate={this.handleUpdate}/> 
        </div>
    ) 
} 
    
    )},

Như bạn đã thấy, chúng ta đã có thể gửi item lên các component. Hãy đảm bảo rằng các tên hàm trong các thuộc tính của component là đúng để mọi thứ có thể hoạt động tốt. Điều cuối cùng cần phải làm để có thể hoàn thành chức năng là thêm một phương thức trong <Body /> để thay thế các item vừa được cập nhật vào các item cũ trong mảng.

// app/assets/javascripts/components/_body.js.jsx 

handleUpdate(item) { 
    $.ajax({ 
        url: `/api/v1/items/${item.id}`,
        type: 'PUT', data: { item: item }, 
        success: () => { 
            this.updateItems(item); 
        }
    } 
)}, 

updateItems(item) { 
    var items = this.state.items.filter((i) => { return i.id != item.id }); 
        items.push(item); 
        
        this.setState({items: items } );
    },
    

Vậy là xong! Chúng ta đã thực hiện xong các chức năng CRUD cho một model. Giờ việc bạn có thể tự làm cho project của mình là thêm CSS hoặc thêm các thông tin cho item.


All Rights Reserved