rails new accounts
bootstrap
. The installation process is beyond the scope of this article, but you can install the official bootstrap-sass
using instructions from the git repository .React
. For this guide, I decided to connect it from the official gem because we will use some cool features of this gem, but there is another way to achieve our goal of using Rails or we can download the source code from the official page and paste into our javascripts
folder.react-rails
: Add react-rails
to your Gemfile
gem 'react-rails', '~> 1.0'
bundle install
components.js
file inside the app/assets/javascripts
folder where our React components will live. rails g react:install
application.js
file after launching the installation, you will see 3 new lines: //= require react //= require react_ujs //= require components
ujs
As you might guess from the file name, react-rails
contains an unobtrusive JS driver that will help you install our React components and also handle Turbolinks events.Record
resource, which will consist of the date (date) of the title (title) and the amount (amount).scaffold
'a generation, we will use a resource
generator, rails g resource Record title date:date amount:float
rake db:create db:migrate
rails console Record.create title: 'Record 1', date: Date.today, amount: 500 Record.create title: 'Record 2', date: Date.today, amount: -100
rails s
index
action inside the RecordsController
controller: # app/controllers/records_controller.rb class RecordsController < ApplicationController def index @records = Record.all end end
index.html.erb
file in apps/views/records/
, this file will be the bridge between our Rails application and the React components. To accomplish this task, we will use the helper method react_component
, which gets the name React, the component we want to render along with the data that we pass to it. <%# app/views/records/index.html.erb %> <%= react_component 'Records', { data: @records } %>
localhost:3000/records
. Obviously something is wrong, all because there are no Records (React components). But if you take the generated HTML inside the browser, we can insert something like this. <div data-react-class="Records" data-react-props="{...}"> </div>
react_ujs
will determine, we are trying to render the React component and create an instance of it, including the settings we send via react_component
, in our case, the @records
contentjavascripts/components
directory create a new file: 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'
ReactComponent
class, so when React implements the re-render it will be executed optimally.ReactComponents
inside the render method can be written using JSX syntax. render: -> `<div className="records"> <h2 className="title"> Records </h2> </div>`
React.DOM
syntax because the code will be converted to a hierarchical structure, as in HAML On the other hand, if you try to integrate React into an existing project with ERB, you can reuse existing ERB code and convert it to JSX. # app/assets/javascripts/components/records.js.coffee @Records = React.createClass getInitialState: -> records: @props.data getDefaultProps: -> records: [] render: -> ...
getDefaultProps
method will initialize the settings of our components in the case when we forget to transfer data, when we instantiate it and the getInitialState
method will generate the initial state of our components. Now we generally need to display records using our Rails view.coffee
files. Create a new utils.js.coffee
file in javascripts/
with the following content: # app/assets/javascripts/utils.js.coffee @amountFormat = (amount) -> '$ ' + Number(amount).toLocaleString()
record.js.coffee
file in the javascripts/components
directory and insert the following code: # 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)
null
in React.DOM.*
Calls, this means that we are not passing attributes to the components React.DOM.*
we will update the render method inside the Records components with the following code: # 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
record.id
along with the current record when creating a Record element. If we do not do this, we need to get a warning in our JS browser console (and probably in the near future, sometimes, get a headache).create method
(don't forget to use _strongparams
) 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'
onChange
attribute attaches and processes the method that was called with each keystroke, the handleChange handleChange
method will use the attribute name, which input caused the event and updated the value state.# app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
...
handleChange: (e) ->
name = e.target.name
@setState "#{ name }": e.target.value
@setState title: e.target.value
when the name matches title. But why should we use @setState
? Why can't we just set the desired <co value in state as we usually do in regular> JS objects? Because @setState
must perform 2 actions, this is: # 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'
disabled
attribute along with the value !@valid()
, implying that we are going to implement a valid method to evaluate if the data provided by the user is correct. # app/assets/javascripts/components/record_form.js.coffee @RecordForm = React.createClass ... valid: -> @state.title && @state.date && @state.amount
@state
attribute with empty lines again. Thus, each time the state receives updates, the Create record
button on / off depends on the validation of the data.onSubmit
and a new handleSubmit
method (we did the same with the onChange
event). # 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 ...
@props
)? So, it is. Our current component sends information back to the parent component via @props.handleNewRecord
informing about the creation of a new record.RecordForm
element, we need to pass a handleNewRecord
setting with a link to a method in it, something like React.createElement RecordForm
, handleNewRecord: @addRecord
. Well, the parent Records component is “everywhere” as it has a state with all of the existing records. We need to update this state with the new record created.addRecord
method inside the records.js.coffee
and create a 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
Create record
not surprising, this time the record was added almost immediately and the form became empty after clicking. The update was just completed, of course the backend was filled with new data.jquery
to interact with our backend and Rails jquery_ujs
unobtrusive driver, will include a CSRF token on each of our AJAX requests. Cool.AmountBox
component that will receive the settings: the amount of text and type. Creating a new file will call amount_box.js.coffee
from javascripts/components/
and insert 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)
amountFormat
that read the number of settings and display it in a currency format.Record
component and add the following method: # 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() ...
credits
all entries with a value greater than 0. The sum of debits of all entries with a sum less than 0 and the value of the balance. Now we have in the right place calculating methods. We just need to create the AmountBox
element inside, to render the method (just above the RecordForm
component) # 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 ...
# app/controllers/records_controller.rb class RecordsController < ApplicationController ... def destroy @record = Record.find(params[:id]) @record.destroy head :no_content end ... end
Records React
component and add a column in the actions to the header in the table header. # 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'
RecordForm
component using the list:OnClick
handler to the Record
in the same way, we added a handler for onSubmit
to the RecordForm
to create new records. Fortunately for us, React implements most of the common browser events in a normal way. Therefore, we do not have to worry about cross-browser compatibility (you can look at the full list of events here ).handleDelete
and the 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'
handleDelete
sends an AJAX request to the server.handleDeleteRecord
handler is available through the settings, this means we need to regulate the creation of Record elements in the parent component,handleDeleteRecord
, as well as implement the actual handler method in the ancestors: # 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
deleteRecord
method copies the current record state component, the search index of the record you want to delete. cute standard js operations.replaceState
the main difference between setState
and replaceState
is that the former will only update a state of an object key, and the second will be completely override the current state of the component to any new object that we send.Edit
after each Delete
button to our table. When we click on the button, Edit
it will switch the entire line and readonly state to the editing state by opening the inline form, where the user can update the contents of the records. After submitting the updated content or canceling the action to the line, the record will return to its original read-only state.Record
. This is a case of using what React calls reactive data streams. # app/assets/javascripts/components/record.js.coffee @Record = React.createClass getInitialState: -> edit: false handleToggle: (e) -> e.preventDefault() @setState edit: !@state.edit ...
handleToggle
edit will change from false to true, and vice versa, we just need to start handleToggle
an OnClick
event with the user .recordRow
and recordForm
call them conditionally inside the visualization depending on the content @ state.edit
.recordRow
, this is our current visualization method. Let's move the rendering content to our brand new method recordRow
and add 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' ...
React.DOM
. A member waits for a signal from onClick
for a call. handleToggle
recordForm
should be of the following structure but with an input field in each cell. We will use the new ref
attribute for our inputs, make them available; since this component does not process the state, this new attribute will allow our component to 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' ...
@handleEdit
when the user clicks a button Update
, we are going to use the same thread as one implementation to delete the records.React.DOM.inputs
? We use defaultValue
the default instead of specifying the initial input data, this is because using only the value without OnChange
# app/assets/javascripts/components/record.js.coffee @Record = React.createClass ... render: -> if @state.edit @recordForm() else @recordRow()
# 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
handleEdit
that will send an AJAX request to the server with updated record information, then it notifies the parent component by sending an updated version of the record using the method handleEditRecord
, this method will be received through @props
, just as we did it before when deleting records: # 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
React.findDOMNode
( @ refs.fieldName
) .value and send it literally to the backend. Updating the status to switch to edit mode for success is not mandatory, but the user will certainly thank us for it. # 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
React.addons.update
change in our state can lead to more specific methods. The final link between Records and Record is the method @updateRecord
it is transmitted through the handleEditRecord
settings.Source: https://habr.com/ru/post/281735/
All Articles