

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.replaceStatethe main difference between setStateand replaceStateis 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.
Editafter each Deletebutton to our table. When we click on the button, Editit 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 ... handleToggleedit will change from false to true, and vice versa, we just need to start handleTogglean OnClickevent with the user .recordRowand recordFormcall 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 recordRowand 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 onClickfor a call. handleTogglerecordFormshould be of the following structure but with an input field in each cell. We will use the new refattribute 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' ... @handleEditwhen 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 defaultValuethe 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 handleEditthat 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.updatechange in our state can lead to more specific methods. The final link between Records and Record is the method @updateRecordit is transmitted through the handleEditRecordsettings.
Source: https://habr.com/ru/post/281735/
All Articles