Monday, August 8, 2016

React.js - A guide for Rails developers_part 2


Nesting Components: Listing Records

For our first task, we need to render any existing record inside a table. First of all, we need to create an index action inside of our RecordsController:
  1.  # app/controllers/records_controller.rb
  2.   class RecordsController < ApplicationController
  3.     def index
  4.       @records = Record.all
  5.     end
  6.   end
Next, we need to create a new file index.html.erb under apps/views/records/, this file will act as a bridge between our Rails app and our react_component. To achieve this task, we will use the helper method react_component, which receives the name of the React component we want to render along with the data we want to pass into it.
  1.   <%# app/views/records/index.html.erb %>
  2.   <%= react_component 'Records', { data: @records } %>
It is worth mentioning this helper is provided by the react-rails gem, if you decide to use other React integration method, this helper will not be available.

You can now navigate to localhost:3000/records. Obviously, this won't work yet because of the lack of a Records React component, but if we take a look at the generated HTML inside the browser window, we can spot something like the following code
  1.   <div data-react-class="Records" data-react-props="{...}">
  2.   </div>
With this markup present, react_ujs will detect we are trying to render a React component and will instantiate it, including the properties we sent through react_component, in our case, the contents of @records.

The time has come for us to build our First React component, inside the javascripts/components directory, create a new file called records.js.coffee, this file will contain our Records component.
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     render: ->
  4.       React.DOM.div
  5.         className: 'records'
  6.         React.DOM.h2
  7.           className: 'title'
  8.           'Records'
Each component requires a render method, which will be in charge of rendering the component itself. The render method should return an instance of ReactComponent, this way, when React executes a re-render, it will be performed in an optimal way (as React detects the existence of new nodes through building a virtual DOM in memory). In the snippet above we created an instance of h2, a built-in ReactComponent.

NOTE: Another way to instantiate ReactComponents inside the render method is through JSX syntax. The following snippet is equivalent to the previous one:
  1.   render: ->
  2.     `<div className="records">
  3.       <h2 className="title"> Records </h2>
  4.     </div>`
Personally, when I am working with CoffeeScript, I prefer using the React.DOM syntax over JSX because the code will arrange in a hierarchical structure by itself, similar to HAML. On the other hand, if you are trying to integrate React into an existing application built with erb, you have the option to reuse your existing erb code and convert it into JSX.

You can refresh your browser now.

Perfect! We have rendered our first React Component. Now, it's time to display our records.

Besides the render method, React components rely on the use of properties to communicate with other components and states to detect whether a re-render is required or not. We need to initialize our component's state and properties with the desired values:
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     getInitialState: ->
  4.       records: @props.data
  5.     getDefaultProps: ->
  6.       records: []
  7.     render: ->
  8.       ...
The method getDefaultProps will initialize our component's properties in case we forget to send any data when instantiating it, and the getInitialState method will generate the initial state of our component. Now we need to actually display the records provided by our Rails view.

It looks like we are going to need a helper method to format amount strings, we can implement a simple string formatter and make it accesible to all of our coffee files. Create a new utils.js.coffee file under javascripts/ with the following contents:
  1.   # app/assets/javascripts/utils.js.coffee
  2.   @amountFormat = (amount) ->
  3.     '$ ' + Number(amount).toLocaleString()
We need to create a new Record component to display each individual record, create a new file record.js.coffee under the javascripts/components directory and insert the following contents:
  1.   # app/assets/javascripts/components/record.js.coffee
  2.   @Record = React.createClass
  3.     render: ->
  4.       React.DOM.tr null,
  5.         React.DOM.td null, @props.record.date
  6.         React.DOM.td null, @props.record.title
  7.         React.DOM.td null, amountFormat(@props.record.amount)
The Record component will display a table row containing table cells for each record attribute. Don't worry about those null in the React.DOM.* calls, it means we are not sending attributes to the components. Now update the render method inside the Records component with the following code:
  1. # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     render: ->
  5.       React.DOM.div
  6.         className: 'records'
  7.         React.DOM.h2
  8.           className: 'title'
  9.           'Records'
  10.         React.DOM.table
  11.           className: 'table table-bordered'
  12.           React.DOM.thead null,
  13.             React.DOM.tr null,
  14.               React.DOM.th null, 'Date'
  15.               React.DOM.th null, 'Title'
  16.               React.DOM.th null, 'Amount'
  17.           React.DOM.tbody null,
  18.             for record in @state.records
  19.               React.createElement Record, key: record.id, record: record
Did you see what just happened? We created a table with a header row, and inside of the body table we are creating a Record element for each existing record. In other words, we are nesting built-in/custom React components. Pretty cool, huh?

When we handle dynamic children (in this case, records) we need to provide a key property to the dynamically generated elements so React won't have a hard time refreshing our UI, that's why we send key: record.id along with the actual record when creating Record elements. If we don't do so, we will receive a warning message in the browser's JS console (and probably some headaches in the near future).

You can take a look at the resulting code of this section here, or just the changes introduced by this section here.

Parent-Child communication: Creating Records

Now that we are displaying all the existing records, it would be nice to include a form to create new records, let's add this new feature to our React/Rails application.

First, we need to add the create method to our Rails controller (don't forget to use _strongparams):
  1.  # app/controllers/records_controller.rb
  2.   class RecordsController < ApplicationController
  3.     ...
  4.     def create
  5.       @record = Record.new(record_params)
  6.       if @record.save
  7.         render json: @record
  8.       else
  9.         render json: @record.errors, status: :unprocessable_entity
  10.       end
  11.     end
  12.     private
  13.       def record_params
  14.         params.require(:record).permit(:title, :amount, :date)
  15.       end
  16.   end
Next, we need to build a React component to handle the creation of new records. The component will have its own state to store datetitle and amount. Create a new record_form.js.coffee file under javascripts/components with the following code:
  1.  # app/assets/javascripts/components/record_form.js.coffee
  2.   @RecordForm = React.createClass
  3.     getInitialState: ->
  4.       title: ''
  5.       date: ''
  6.       amount: ''
  7.     render: ->
  8.       React.DOM.form
  9.         className: 'form-inline'
  10.         React.DOM.div
  11.           className: 'form-group'
  12.           React.DOM.input
  13.             type: 'text'
  14.             className: 'form-control'
  15.             placeholder: 'Date'
  16.             name: 'date'
  17.             value: @state.date
  18.             onChange: @handleChange
  19.         React.DOM.div
  20.           className: 'form-group'
  21.           React.DOM.input
  22.             type: 'text'
  23.             className: 'form-control'
  24.             placeholder: 'Title'
  25.             name: 'title'
  26.             value: @state.title
  27.             onChange: @handleChange
  28.         React.DOM.div
  29.           className: 'form-group'
  30.           React.DOM.input
  31.             type: 'number'
  32.             className: 'form-control'
  33.             placeholder: 'Amount'
  34.             name: 'amount'
  35.             value: @state.amount
  36.             onChange: @handleChange
  37.         React.DOM.button
  38.           type: 'submit'
  39.           className: 'btn btn-primary'
  40.           disabled: !@valid()
  41.           'Create record'
Nothing too fancy, just a regular Bootstrap inline form. Notice how we are defining the value attribute to set the input's value and the onChange attribute to attach a handler method which will be called on every keystroke; the handleChange handler method will use the name attribute to detect which input triggered the event and update the related state value:
  1.   # app/assets/javascripts/components/record_form.js.coffee
  2.   @RecordForm = React.createClass
  3.     ...
  4.     handleChange: (e) ->
  5.       name = e.target.name
  6.       @setState "#{ name }": e.target.value
  7.     ...
We are just using string interpolation to dynamically define object keys, equivalent to @setState title: e.target.value when name equals title. But why do we have to use @setState? Why can't we just set the desired value of @state as we usually do in regular JS Objects? Because @setState will perform 2 actions, it:
  1. Updates the component's state
  2. Schedules a UI verification/refresh based on the new state
It is very important to have this information in mind every time we use state inside our components.

Lets take a look at the submit button, just at the very end of the render method:
  1.   # app/assets/javascripts/components/record_form.js.coffee
  2.   @RecordForm = React.createClass
  3.     ...
  4.     render: ->
  5.       ...
  6.       React.DOM.form
  7.         ...
  8.         React.DOM.button
  9.           type: 'submit'
  10.           className: 'btn btn-primary'
  11.           disabled: !@valid()
  12.           'Create record'
We defined a disabled attribute with the value of !@valid(), meaning that we are going to implement a valid method to evaluate if the data provided by the user is correct.
  1.   # app/assets/javascripts/components/record_form.js.coffee
  2.   @RecordForm = React.createClass
  3.     ...
  4.     valid: ->
  5.       @state.title && @state.date && @state.amount
  6.     ...
For the sake of simplicity we are only validating @state attributes against empty strings. This way, every time the state gets updated, the Create record button is enabled/disabled depending on the validity of the data.

Now that we have our controller and form in place, it's time to submit our new record to the server. We need to handle the form's submit event. To achieve this task, we need to add an onSubmit attribute to our form and a new handleSubmit method (the same way we handled onChange events before):
  1.   # app/assets/javascripts/components/record_form.js.coffee
  2.   @RecordForm = React.createClass
  3.     ...
  4.     handleSubmit: (e) ->
  5.       e.preventDefault()
  6.       $.post '', { record: @state }, (data) =>
  7.         @props.handleNewRecord data
  8.         @setState @getInitialState()
  9.       , 'JSON'
  10.     render: ->
  11.       React.DOM.form
  12.         className: 'form-inline'
  13.         onSubmit: @handleSubmit
  14.       ...
Let's review the new method line by line:
  1. Prevent the form's HTTP submit
  2. POST the new record information to the current URL
  3. Success callback
The success callback is the key of this process, after successfully creating the new record someone will be notified about this action and the state is restored to its initial value. Do you remember early in the post when I mentioned that components communicate with other components through properties (or @props)? Well, this is it. Our current component sends data back to the parent component through @props.handleNewRecord to notify it about the existence of a new record.

As you might have guessed, wherever we create our RecordForm element, we need to pass a handleNewRecord property with a method reference into it, something like React.createElement RecordForm, handleNewRecord: @addRecord. Well, the parent Records component is the "wherever", as it has a state with all of the existing records, we need to update its state with the newly created record.

Add the new addRecord method inside records.js.coffee and create the new RecordForm element, just after the h2 title (inside the render method).
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     addRecord: (record) ->
  5.       records = @state.records.slice()
  6.       records.push record
  7.       @setState records: records
  8.     render: ->
  9.       React.DOM.div
  10.         className: 'records'
  11.         React.DOM.h2
  12.           className: 'title'
  13.           'Records'
  14.         React.createElement RecordForm, handleNewRecord: @addRecord
  15.         React.DOM.hr null
  16.       ...
Refresh your browser, fill in the form with a new record, click Create record... No suspense this time, the record was added almost immediately and the form gets cleaned after submit, refresh again just to make sure the backend has stored the new data.

If you have used other JS frameworks along with Rails (for example, AngularJS) to build similar features, you might have run into problems because your POST requests don't include the CSRF token required by Rails, so, why didn't we run into this same issue? Easy, because we are using jQuery to interact with our backend, and Rails' jquery_ujs unobtrusive driver will include the CSRF token on every AJAX request for us. Cool!

You can take a look at the resulting code of this section here, or just the changes introduced by this section here.

Reusable Components: Amount Indicators

What would an application be without some (nice) indicators? Let's add some boxes at the top of our window with some useful information. We goal for this section is to show 3 values: Total credit amount, total debit amount and Balance. This looks like a job for 3 components, or maybe just one with properties?
We can build a new AmountBox component which will receive three properties: amounttext and type. Create a new file called amount_box.js.coffee under javascripts/components/ and paste the following code:
  1.   # app/assets/javascripts/components/amount_box.js.coffee
  2.   @AmountBox = React.createClass
  3.     render: ->
  4.       React.DOM.div
  5.         className: 'col-md-4'
  6.         React.DOM.div
  7.           className: "panel panel-#{ @props.type }"
  8.           React.DOM.div
  9.             className: 'panel-heading'
  10.             @props.text
  11.           React.DOM.div
  12.             className: 'panel-body'
  13.             amountFormat(@props.amount)
We are just using Bootstrap's panel element to display the information in a "blocky" way, and setting the color through the type property. We have also included a really simple amount formatter method called amountFormat which reads the amount property and displays it in currency format.

In order to have a complete solution, we need to create this element (3 times) inside of our main component, sending the required properties depending on the data we want to display. Let's build the calculator methods first, open the Records component and add the following methods:
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     credits: ->
  5.       credits = @state.records.filter (val) -> val.amount >= 0
  6.       credits.reduce ((prev, curr) ->
  7.         prev + parseFloat(curr.amount)
  8.       ), 0
  9.     debits: ->
  10.       debits = @state.records.filter (val) -> val.amount < 0
  11.       debits.reduce ((prev, curr) ->
  12.         prev + parseFloat(curr.amount)
  13.       ), 0
  14.     balance: ->
  15.       @debits() + @credits()
  16.     ...
credits sums all the records with an amount greater than 0, debits sums all the records with an amount lesser than 0 and balance is self-explanatory. Now that we have the calculator methods in place, we just need to create the AmountBox elements inside the render method (just above the RecordForm component):
  1.   # app/assets/javascripts/components/records.js.coffee
  2.   @Records = React.createClass
  3.     ...
  4.     render: ->
  5.       React.DOM.div
  6.         className: 'records'
  7.         React.DOM.h2
  8.           className: 'title'
  9.           'Records'
  10.         React.DOM.div
  11.           className: 'row'
  12.           React.createElement AmountBox, type: 'success', amount: @credits(), text: 'Credit'
  13.           React.createElement AmountBox, type: 'danger', amount: @debits(), text: 'Debit'
  14.           React.createElement AmountBox, type: 'info', amount: @balance(), text: 'Balance'
  15.         React.createElement RecordForm, handleNewRecord: @addRecord
  16.     ...
We are done with this feature! Refresh your browser, you should see three boxes displaying the amounts we've calculated earlier. But wait! There's more! Create a new record and see the magic work...

You can take a look at the resulting code of this section here, or just the changes introduced by this section here.
Written by Fernando Villalobos

If you found this post interesting, follow and support us.
Suggest for you:



No comments:

Post a Comment