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:
- # app/controllers/records_controller.rb
- class RecordsController < ApplicationController
- def index
- @records = Record.all
- end
- end
- <%# app/views/records/index.html.erb %>
- <%= react_component 'Records', { data: @records } %>
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
- <div data-react-class="Records" data-react-props="{...}">
- </div>
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.
- # app/assets/javascripts/components/records.js.coffee
- @Records = React.createClass
- render: ->
- React.DOM.div
- className: 'records'
- React.DOM.h2
- className: 'title'
- 'Records'
NOTE: Another way to instantiate ReactComponents inside the render method is through JSX syntax. The following snippet is equivalent to the previous one:
- render: ->
- `<div className="records">
- <h2 className="title"> Records </h2>
- </div>`
You can refresh your browser now.
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:
- # app/assets/javascripts/components/records.js.coffee
- @Records = React.createClass
- getInitialState: ->
- records: @props.data
- getDefaultProps: ->
- records: []
- render: ->
- ...
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:
- # app/assets/javascripts/utils.js.coffee
- @amountFormat = (amount) ->
- '$ ' + Number(amount).toLocaleString()
- # 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)
- # app/assets/javascripts/components/records.js.coffee
- @Records = React.createClass
- ...
- 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
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).
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):
- # app/controllers/records_controller.rb
- 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
- # app/assets/javascripts/components/record_form.js.coffee
- @RecordForm = React.createClass
- getInitialState: ->
- title: ''
- date: ''
- amount: ''
- render: ->
- React.DOM.form
- className: 'form-inline'
- 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'
- # app/assets/javascripts/components/record_form.js.coffee
- @RecordForm = React.createClass
- ...
- handleChange: (e) ->
- name = e.target.name
- @setState "#{ name }": e.target.value
- ...
- Updates the component's state
- Schedules a UI verification/refresh based on the new state
Lets take a look at the submit button, just at the very end of the render method:
- # app/assets/javascripts/components/record_form.js.coffee
- @RecordForm = React.createClass
- ...
- render: ->
- ...
- React.DOM.form
- ...
- React.DOM.button
- type: 'submit'
- className: 'btn btn-primary'
- disabled: !@valid()
- 'Create record'
- # app/assets/javascripts/components/record_form.js.coffee
- @RecordForm = React.createClass
- ...
- valid: ->
- @state.title && @state.date && @state.amount
- ...
- # app/assets/javascripts/components/record_form.js.coffee
- @RecordForm = React.createClass
- ...
- handleSubmit: (e) ->
- e.preventDefault()
- $.post '', { record: @state }, (data) =>
- @props.handleNewRecord data
- @setState @getInitialState()
- , 'JSON'
- render: ->
- React.DOM.form
- className: 'form-inline'
- onSubmit: @handleSubmit
- ...
- Prevent the form's HTTP submit
- POST the new record information to the current URL
- Success callback
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).
- # 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
- ...
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: amount, text and type. Create a new file called amount_box.js.coffee under javascripts/components/ and paste the following code:
- # 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)
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:
- # 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()
- ...
- # app/assets/javascripts/components/records.js.coffee
- @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
- ...
Written by Fernando Villalobos
If you found this post interesting, follow and support us.
Suggest for you:
No comments:
Post a Comment