The next feature in our list is the ability to delete records, we need a new Actions column in our records table, this column will have a Delete button for each record, pretty standard UI. As in our previous example, we need to create the destroy method in our Rails controller:
- # app/controllers/records_controller.rb
- class RecordsController < ApplicationController
- ...
- def destroy
- @record = Record.find(params[:id])
- @record.destroy
- head :no_content
- end
- ...
- end
- # app/assets/javascripts/components/records.js.coffee
- @Records = React.createClass
- ...
- 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
- # 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)
- React.DOM.td null,
- React.DOM.a
- className: 'btn btn-danger'
- 'Delete'
- Detect an event inside the child Record component (onClick)
- Perform an action (send a DELETE request to the server in this case)
- Notify the parent Records component about this action (sending/receiving a handler method through props)
- Update the Record component's state
Re-open the Record component, add a new handleDelete method and an onClick attribute to our "useless" delete button as follows:
- # app/assets/javascripts/components/record.js.coffee
- @Record = React.createClass
- handleDelete: (e) ->
- e.preventDefault()
- # yeah... jQuery doesn't have a $.delete shortcut method
- $.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'
- # 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
We introduced a new way of interacting with the state, replaceState; the main difference between setState and rreplaceState is that the first one will only update one key of the state object, the second one will completely override the current state of the component with whatever new object we send.
After updating this last bit of code, refresh your browser window and try to delete a record, a couple of things should happen:
- The records should disappear from the table and...
- The indicators should update the amounts instantly, no additional code is required
We are almost done with our application, but before implementing our last feature, we can apply a small refactor and, at the same time, introduce a new React feature.
You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
Refactor: State Helpers
At this point, we have a couple of methods where the state gets updated without any difficulty as our data is not what you might call "complex", but imagine a more complex application with a multi-level JSON state, you can picture yourself performing deep copies and juggling with your state data. React includes some fancy state helpers to help you with some of the heavy lifting, no matter how deep your state is, these helpers will let you manipulate it with more freedom using a kind-of MongoDB's query language (or at least that's what React's documentation says).
Before using these helpers, first we need to configure our Rails application to include them. Open your project's config/application.rb file and add config.react.addons = true at the bottom of the Application block:
- # config/application.rb
- ...
- module Accounts
- class Application < Rails::Application
- ...
- config.react.addons = true
- end
- end
For the changes to take effect, restart your rails server, I repeat, restart your rails server. Now we have access to the state helpers through React.addons.update, which will process our state object (or any other object we send to it) and apply the provided commands. The two commands we will be using are $push and $splice (I'm borrowing the explanation of these commands from the official React documentation):
{$push: array}
push()
all the items in array on the target.- {$splice: array of arrays} for each item in arrays call splice() on the target with the parameters provided by the item.
We're about to simplify addRecord and deleteRecord from the Record component using these helpers, as follows:
- # 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
Shorter, more elegant and with the same results, feel free to reload your browser and ensure nothing got broken.
Reactive Data Flow: Editing Records
For the final feature, we are adding an extra Edit button, next to each Delete button in our records table. When this Edit button gets clicked, it will toggle the entire row from a read-only state (wink wink) to an editable state, revealing an inline form where the user can update the record's content. After submitting the updated content or canceling the action, the record's row will return to its original read-only state.
As you might have guessed from the previous paragraph, we need to handle mutable data to toggle each record's state inside of our Record component. This is a use case of what React calls reactive data flow. Let's add an edit flag and a handleToggle method to record.js.coffee:
- # app/assets/javascripts/components/record.js.coffee
- @Record = React.createClass
- getInitialState: ->
- edit: false
- handleToggle: (e) ->
- e.preventDefault()
- @setState edit: !@state.edit
- ...
The edit flag will default to false, and handleToggle will change edit from false to true and vice versa, we just need to trigger handleToggle from a user onClick event.
Now, we need to handle two row versions (read-only and form) and display them conditionally depending on edit. Luckily for us, as long as our render method returns a React element, we are free to perform any actions in it; we can define a couple of helper methods rrecordRow and recordForm and call them conditionally inside of render depending on the contents of @state.edit.
We already have an initial version of recordRow, it's our current render method. Let's move the contents of render to our brand new recordRow method and add some additional code to it:
- # app/assets/javascripts/components/record.js.coffee
- @Record = React.createClass
- ...
- recordRow: ->
- 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-default'
- onClick: @handleToggle
- 'Edit'
- React.DOM.a
- className: 'btn btn-danger'
- onClick: @handleDelete
- 'Delete'
- ...
We only added an additional React.DOM.a element which listens to onClick events to call handleToggle.
Moving forward, the implementation of recordForm will follow a similar structure, but with input fields in each cell. We are going to use a new ref attribute for our inputs to make them accessible; as this component doesn't handle a state, this new attribute will let our component read the data provided by the user through @refs:
- # app/assets/javascripts/components/record.js.coffee
- @Record = React.createClass
- ...
- recordForm: ->
- React.DOM.tr null,
- React.DOM.td null,
- React.DOM.input
- className: 'form-control'
- type: 'text'
- defaultValue: @props.record.date
- ref: 'date'
- React.DOM.td null,
- React.DOM.input
- className: 'form-control'
- type: 'text'
- defaultValue: @props.record.title
- ref: 'title'
- React.DOM.td null,
- React.DOM.input
- className: 'form-control'
- type: 'number'
- defaultValue: @props.record.amount
- ref: 'amount'
- React.DOM.td null,
- React.DOM.a
- className: 'btn btn-default'
- onClick: @handleEdit
- 'Update'
- React.DOM.a
- className: 'btn btn-danger'
- onClick: @handleToggle
- 'Cancel'
- ...
Do not be afraid, this method might look big, but it is just our HAML-like syntax. Notice we are calling @handleEdit when the user clicks on the Update button, we are about to use a similar flow as the one implemented to delete records.
Do you notice something different on how React.DOM.input are being created? We are using defaultValue instead of value to set the initial input values, this is because using just
value
without onChange
will end up creating read-only inputs.
Finally, the render method boils down to the following code:
- # app/assets/javascripts/components/record.js.coffee
- @Record = React.createClass
- ...
- render: ->
- if @state.edit
- @recordForm()
- else
- @recordRow()
You can refresh your browser to play around with the new toggle behavior, but don't submit any changes yet as we haven't implemented the actual update functionality.
To handle record updates, we need to add the update method to our Rails 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
Back to our Record component, we need to implement the handleEdit method which will send an AJAX request to the server with the updated record information, then it will notify the parent component by sending the updated version of the record via the handleEditRecord method, this method will be received through @props, the same way we did it before when deleting records:
- # app/assets/javascripts/components/records.js.coffee
- @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
As we have learned on the previous section, using React.addons.update to change our state might result on more concrete methods. The final link between Records and Records is the method @updateRecord sent through the handleEditRecord property.
Refresh your browser for the last time and try updating some existing records, notice how the amount boxes at the top of the page keep track of every record you change.
You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
Closing thoughts: React.js Simplicity & Flexibility
We have examined some of React's functionalities and we learned that it barely introduces new concepts. I have heard comments of people saying X or Y JavaScript framework has a steep learning curve because of all the new concepts introduced, this is not React's case; it implements core JavaScript concepts such as event handlers and bindings, making it easy to adopt and learn. Again, one of its strengths is its simplicity.
We also learned by the example how to integrate it into the Rails' assets pipeline and how well it plays along with CoffeeScript, jQuery, Turbolinks, and the rest of the Rails' orchestra. But this is not the only way of achieving the desired results. For example, if you don't use Turbolinks (hence, you don't need react_ujs) you can use Rails Assets instead of the react-rails gem, you could use Jbuilder to build more complex JSON responses instead of rendering JSON objects, and so on; you would still be able to get the same wonderful results.
React will definitely boost your frontend abilities, making it a great library to have under your Rails' toobelt.
Written by Fernando Villalobos
If you found this post interesting, follow and support us.
Suggest for you:
No comments:
Post a Comment