На начальном уровне такие переводы — мой вклад в развитие rails сообщества.
Дальше в тексте все что выделено курсивом мои замечания (таких будет не много:) )
Введение в React.js
React.js это новый популярный парень из команды JavaScript фреймворков, он выделяется своей простотой. Когда другие фреймворки реализуют полный MVC (Model View Controller) подход, мы можем сказать React'у реализовать только View (Отображение) (факт — некоторые люди переписывают часть отображения (V) этих фреймворков c помощью React).
Приложения с реактом основаны на 2х основных принципах Компоненты и Состояния. Компоненты могут состоять из более мелких компонентов встроенных или пользовательских. Состояния, что ребята из Facebook называют односторонний реактивный поток данных, подразумевая что наш интерфейс(UI) будет реагировать на каждое изменение состояния.
Одна хорошая особенность React.js это то что он не требует каких-либо дополнительных зависимостей, что обеспечивает ему подключаемость с любой js библиотекой. Пользуясь этим, мы будем включаеть его в наш Rails стек для создания внешнего интерфейса или можно сказать для создания «Rails на стероидах».
Макет для отслеживания расходов приложения
Для этого гайда мы создадим маленькое приложение с нуля что бы отслеживать наши действия. Каждая запись(дальше, тоже самое что и Record) будет состоять из даты, названия и количества. Запись будет рассматриваться как Кредит(Credit) если его сумма больше нуля, в противном случае она будет рассматриваться каr дебет. Вот макет проекта:
Суммарно приложение будет вести себя так:
- Когда пользователь создает новую запись через горизонтальную форму, она будет вставлена в таблицу записей
- Пользователь может редактировать любую существующую запись
- Кликнув на кнопку Delete он удалит ассоциацию из таблицы
- Добавление, редактирование или удаление существующей записи будет обновлять количество боксов в верху страницы
Инициализация React.js в Rails проект
В первую очередь нам нужно создать наш новый проект, назовем его Accounts
rails new accounts
Для пользовательского интерфейса этого проектамы будем использовать bootstrap
. Процесс установки выходит за рамки этой статьи, но вы можете установить официальный гем bootstrap-sass
используя инструкцию из гит репозитория.
Теперь наш проект установлен. Продолжим подключать React
. Для этого гайда я решил подключить его с официального гема потому что мы будем использовать некоторые крутые фичи этого гема, но есть и другой способ достижение нашей цели использывать Rails или можем скачать исходный код с официальной страницы и вставить в нашу javascripts
папку.
Если вы занимались разработкой Rails приложений вы знаете как легко установить гем: Добавте react-rails
в ваш Gemfile
gem 'react-rails', '~> 1.0'
Затем любезно скажите рельсам установить новый гем
bundle install
React-rails идет с установкой скрипта, который создаст файл components.js
внутри папки app/assets/javascripts
где и будут жить наши React компоненты.
rails g react:install
Если вы посмотрите в ваш файл application.js
после запуска установки — вы увидите 3 новые строки:
//= require react
//= require react_ujs
//= require components
По существу, это включает в себя React библиотеку, компоненты и похожие файлы хранятся в ujs
.Как вы могли догадаться по имени файла react-rails
содержит ненавязчивый JS драйвер который поможет установить наши React компоненты а также будет обрабатывать Turbolinks события.
Создание ресурса
Мы будем создавать Record
ресурс, который будет состоять из даты(date) заголовка(title) и количества(amount).
Взамен использования генерацииscaffold
'a, мы будем использовать resource
генератор,
мы не будем использовать все файлы и методы созданные с помощью scaffold генератора. В противном случае можно было бы запустить скафолд и затем удалить неиспользуемые файлы/методы но наш проект в таком случае будет немного грязным. После этого, внутри проекта запустите следующую команду:
rails g resource Record title date:date amount:float
После этой магии мы имеем новую модель(Model) контроллер(Controller) и роуты(routes). Теперь создадим базу данных и запустим миграции:
rake db:create db:migrate
Плюс ко всему вы можете создать пару записей(records) через
rails console
Record.create title: 'Record 1', date: Date.today, amount: 500
Record.create title: 'Record 2', date: Date.today, amount: -100
Не забудьте запустить ваш сервер
rails s
Готово! Мы можем писать код.
Вложенные компоненты: Список Records
Для нашей первой задачи, нам нужно отрендерить любую созданную запись внутри таблицы.
Во-первых нам нужно создать index
экшн внутри контроллера RecordsController
:
# app/controllers/records_controller.rb
class RecordsController < ApplicationController
def index
@records = Record.all
end
end
Теперь нам нужно создать новый файл index.html.erb
вapps/views/records/
, этот файл будет мостом между нашим Rails приложением и компонентами React. Для выполнения этой задачи, мы будем использовать хелпер метод react_component
, который получает имя React, компонент мы хотим отрендерить вместе с данными которые передаем в него.
<%# app/views/records/index.html.erb %>
<%= react_component 'Records', { data: @records } %>
Стоит отметить что этот helper предоставлен react-rails гемом, если вы решите использывать другой интеграционный React метод, этот хелпер не будет доступен.
Теперь можете перейти в localhost:3000/records
. Очевидно что что-то работает не так, все потому что отсутствуют Records (React компоненты). Но если вы возьмете сгенерированный HTML внутри браузера, мы можем вставить что то вроде этого.
<div data-react-class="Records" data-react-props="{...}">
</div>
C этой разметкой react_ujs
определит, мы пытаемся отрендерить React компонент и создать его экземпляр включая настройки мы посылаем через react_component
, в нашем случае контент @records
Пришло время создать наш первый компонент, внутри директории javascripts/components
создайте новый файл: records.js.coffee
, этот файл будет содержать наш Records
компонент.
# app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
render: ->
React.DOM.div
className: 'records'
React.DOM.h2
className: 'title'
'Records'
Каждый компонент требует рендер метод, который будет изменять рендеринг своих компонентов, рендер метод должен возвращать экземпляр класса ReactComponent
, таким образом, когда React реализует ре-рендер он будет выполненятся оптимально.
Замечание. В другом случае экземпляр ReactComponents
внутри рендер метода может быть записан с помощью JSX синтаксиса.
Эквивалент коду выше:
render: ->
`<div className="records">
<h2 className="title"> Records </h2>
</div>`
Лично для меня, когда я работаю с CoffeeScript, я предпочитаю использовать React.DOM
синтаксис JSX'у потому что код будет преобразован к иерархической структуре, как в HAML С другой стороны если вы пробуете интегрировать React в существующий проект с ERB, вы можете повторно использовать существующий ERB код и конвертировать его в JSX.
Обновите браузер
Отлично. Мы отрендерили наш первый React компонент. Теперь пришло время отобразить наши записи.
Кроме того рендер метод React компонентов полагается на использование настроек для обмена с другими компонентами и состояниями что бы понять нужен ре-рендер или нет. Нам необходимо инициализировать состояние и свойства нашего компонента с требуемыми значениями.
# app/assets/javascripts/components/records.js.coffee
@Records = React.createClass
getInitialState: ->
records: @props.data
getDefaultProps: ->
records: []
render: ->
...
Метод getDefaultProps
будет инициалиировать настройки наших компонентов в случае когда мы забываем передать данные, когда инстанцируем его и метод getInitialState
будет генерировать начальное состояние нашиш компонентов. Теперь нам вообще то нужно отобразить records с помощью нашего Rails view.
Похоже что нам нужен хелпер метод для форматирования количества строк, мы можем вставить простое форматирование строк и сделать его доступным для всех coffee
файлов Создадим новый utils.js.coffee
файл в javascripts/
с следующим контентом:
# app/assets/javascripts/utils.js.coffee
@amountFormat = (amount) ->
'$ ' + Number(amount).toLocaleString()
Нам нужно создать новый Record компонент, отобразить каждую отдельную запись, создать новый файл record.js.coffee
в javascripts/components
директории и вставить следующий код:
# 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)
Record компонент должен отобразиться в колонке таблицы содержащей клетку для каждого атрибута записи. Не волнуйтесь на счет этих null
в React.DOM.*
вызовах, это значит что мы не передаем атрибуты компонентам.Теперь обновим рендер метод внутри Records компонентов с следующим кодом:
# 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 элемент для каждой существующей записи. Иначе говоря, мы вложили build-in/custom React компоненты, Круто. правда?
Когда мы имеем динамических наследников (в нашем случае records) мы должны обеспечить ключ настройки к динамическому генирированию елементов, итак React не имеет большого времени обновления нашего UI(пользовательского интерфейса) это потому что мы передаем
ключ: record.id
вместе с настоящей записью когда создаем Record элемент. Если мы не делаетем это, нам нужно получить предупреждение в нашей JS консоли браузера (и вероятно, в ближайшем будущем, иногда, получать головную боль).
Вы можете посмотреть код этой секции тут или вы можете посмотреть измения секции.
Связь между родительскими и дочерними элементами: создание Records
Теперь мы отображаем все созданные записи также было бы здорово включить форму для создания новой записи. Давайте добавим эту фичу в наше React/Rails приложение. Во-первых, нам нужно добавить метод создания (create method
) нашего контроллера (не забудьте про использование _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
Дальше, нам нужно создать React компонент и отслеживать создание новой записи. Компонент будет иметь свое собсвенное состояние что бы хранить дату (date) заголовок (title) и количество (amount).
# 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'
Ничего креативного, просто инлайн форма бутстрапа. Обратите внимание как мы определяем значение атрибута установив значение инпутов(input's), onChange
атрибут прикрепляет и обрабатывает метод который был вызван при каждом нажатии клавиши, метод обрабатчик handleChange
будет использовать имя атрибута, какой инпут вызвал событие и обновил состояние значения.
# 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
когда имя соответствует title. Но почему мы должны использовать @setState
? Почему мы не можем просто установить желаемоеых <co значение в state как мы обычно делаем в регулярнde>JS объектах? Потому что @setState
должен выполнять 2 действия, это:
- Обновлять компоненты состояния
- Планировать UI проверку/обновление на основе нового состояния
Очень важно иметь эту информацию в памяти каждый раз мы используем состояние внутри нашего компонента. Давайте посмотрим на кнопку отправки, только в конце рендер метода
# 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
атрибут вместе с значением !@valid()
, подразумевая что мы собираемся реализовать valid метод для оценки, если данные предоставленные пользователем правильны.
# app/assets/javascripts/components/record_form.js.coffee
@RecordForm = React.createClass
...
valid: ->
@state.title && @state.date && @state.amount
Для простоты мы только валидируем @state
атрибут снова пустыми строками. Таким образом каждый раз состояние получает обновления, Create record
кнопка вкл/выкл зависит от валидации данных.
Сейчас наш контроллер и форма на месте. Пришло время что бы отправить нашу новую запись на сервер. Нам нужно обработать формы представления событий. Для выполнения задачи нам нужно добавить на onSubmit
атрибут нашей формы и новый handleSubmit
метод (похожее мы делали с onChange
событием ).
# 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
...
Просмотрим новый метод построчно:
- Предотвратить форму отправки
- POST новой record информации текущего URL
- Успешный коллбек
Успешный коллбек — это ключ этого процесса, после успешного создания новой записи кто то должен сообщить об этом действии, и состояние обновляется до нового значения. Вы помните когда я упомянул что этот компонент взаимодействует с другими компонентами через настройки (или @props
)? Так вот, так и есть. Наш текущий компонент отправляет информацию назад родительскому компонента через @props.handleNewRecord
сообщая про создание новой записи.
Как вы могли догадаться когда мы создаем наш RecordForm
элемент нам нужно передать handleNewRecord
настройки с ссылыкой на метод в нем, что то вроде React.createElement RecordForm
, handleNewRecord: @addRecord
. Ну, родительский Records компонента «везде» как это имеет состояние со всеми из существующих записей нам нужно обновить это состояние с новой созданной записью
Добавить новый addRecord
метод внутри records.js.coffee
и создать новую RecordForm
элемента, просто после h2 title (внутри рендер метода).
# 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
не удивительно, на этот раз запись была добавлена почти незамедлительно и форма стала пустой после нажатия. Просто выполнилось обновление, конечно бекэнд заполнился новыми данными
Если вы используете другой JS фреймворк вместе c React (ангулар например) и создаете подобные фичи, то вы можете иметь проблемы, потому что ваш POST запрос не включает CSRF токен требуемый Rails, итак, почему мы не столкнулись с этим вопросом? Просто потому что мы используем jquery
для взаимодействия с нашим бекэндом и Rails jquery_ujs
ненавязчивый драйвер, будет включать в себя маркер CSRF на каждом нашем AJAX запросе. Круто.
Вы можете увидеть результат кода в этой секции, или изменить тут
Повторное использование компонентов
Чего бы хотело приложение без некоторых (хороших) показателей? Давайте добавим несколько боксов наверху нашего окна с некоторой используемой информацией. Мы обозначим для боксов 3 значение: Суммарное количество кредитов, суммарное кол-во дебета и баланс.
Это выглядит как работа с тремя компонентами или может быть просто одной из настроек?
Мы можем создать новый AmountBox
компонент который будет получать настройки: количество текст и тип. Создание нового файла вызовет amount_box.js.coffee
из javascripts/components/
и вставит следующий код:
# 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)
Мы просто используем бутстрап панель, элемент отображает информацию в «blocky» методе и устанавливает цвет через тип настройки.
Мы также имеем включенное и очень простое количество форматированных методов вызванных amountFormat
которые читают количество настроек и отображают это в формате валют.
В заказе есть завершенное решение. Нам нужно создать этот элемент 3 раза внутри нашего главного (main) компонента передать обязательные настройки зависимости от данных, которые мы хотим отобразить. Давайте создадим калькулярот методов. Во-первых откройте Record
компонент и добавте следующий метод:
# 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
всех записей с количеством больше 0. Cумма дебетов всех записей количество меньше 0 и значение баланса. Теперь мы имеем в нужном месте калькулято методов. Нам просто нужно создать AmountBox
элемент внутри, отрендерить метод (просто выше RecordForm
компонента)
# 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
...
Мы закончили с этой фичей! Обновим браузер. Вы должно быть увидели 3 отображаемых бокса, суммы мы вычислили раньше. Но подождите! Есть еще! Создадим новую запись и увидим магию в работе.
Вы можете увидеть результат кода
исправления тут
setState/replaceState: удаление записей
Следующая фича в нашем списке — удаление записи. Нам нужнен новая экшн колонка в нашем таблице записей. Эта колонка будет иметь Delete кнопку для каждой записи, симпатичный стандарт UI. Как в нашем предыдущем примере нам нужно создать и удалить метод в нашем Rails контроллере.
# app/controllers/records_controller.rb
class RecordsController < ApplicationController
...
def destroy
@record = Record.find(params[:id])
@record.destroy
head :no_content
end
...
end
Это весь код на стороне сервера, который нам нужен для этой фичи. Теперь откройте ваши Records React
компонент и добавте в экшены колонку справа в хеадер таблицы.
# 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
И наконец откройте Record компонент и добавте дополнительную колонку с Delete ссылкой
# 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
компонента, используя список:
- Удалить событие внути потомка Record компонента (onClick)
- Выполнять экшн (отправить DELETE запрос к серверу в этом случае)
- Уведомить Records родительских компонентов об этом действии (отправка / прием метод обработчика через настройки)
- Обновить состояние Record компонентов
Для реализации первого шага мы можем добавить обработчик OnClick
к Record
таким же образом, мы добавили обработчик для onSubmit
к RecordForm
для создания новых записей. К счастью для нас, React реализует большинство общих событий браузера в нормальном виде. Поэтому мы не должны беспокоиться о кросс-браузерной совместимости (вы можете посмотреть на полный список событий здесь ).
Снова откройте компонент записи, добавьте новый метод handleDelete
и OnClick
атрибут нашей «бесполезной» кнопке удаления следующим образом:
# 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
отправляет AJAX запрос к серверу
чтобы удалить запись на бекэнде и после этого сообщит родительскому компоненту про это действие через handleDeleteRecord
обработчик доступен через настройки, это значит нам нужно регулировать создание Record элементов в родительском компоненте,
чтобы включить дополнительное свойство handleDeleteRecord
, а также осуществлять фактический метод обработчика в предках:
# 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
метод копирует текущий компонент состояния записей, выполнение поиска индекса записи которую нужно удалить. милый стандарт JS операций.
Мы ввели новый способ взаимодействия с состоянием replaceState
главное отличие между setState
и replaceState
это то что первый будет только обновлять один ключ состояния объекта, а второй будет полностью переопределять текущее состояние компонента с любым новым объектом который мы посылаем.
После обновления последнего бита кода обновите окно браузера и пробуйте удалить запись, должно произойти две вещи:
- Запись должна пропасть из таблицы
- Индикатор должен мгновенно обновить кол-во (для этого ненужен никакой другой код).
Мы почти закончили но перед установкой последней фичи мы можем применить маленький рефакторинг в то же время, ввести новую функцию React.
Refactor: State Helpers
Последняя фича. Мы добавим дополнительно кнопку Edit
после каждой Delete
кнопки в нашу таблицу. Когда кликнем по кнопке Edit
она будет переключать всю строку и состония «только для чтения» в состояние редактирования открывая инлайн форму, где пользователь может обновить содержание записей. После подачи обновленного содержимого или отмены действия к строке, запись вернется в исходное состояние только для чтения.
Как вы догадались из предыдущей главы нам нужно обрабатывать несколько данных для переключения каждого состояния записей внутри нашего компонента Record
. Это случай использования того, что React называет реактивные потоки данных.
Давайте добавим флаг редактирования и метод handleToggle к record.js.coffee:
# app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
getInitialState: ->
edit: false
handleToggle: (e) ->
e.preventDefault()
@setState edit: !@state.edit
...
Флаг редактирования по умолчанию будет выключен, и handleToggle
изменит редактирование от ложного к истинному, и наоборот, нам просто нужно запустить handleToggle
с пользователем OnClick
событие.
Теперь нам нужно управлять двумя версиями строки читать/читать_и_редактировать и отображать их условно в зависимости от редактирования. К счастью для нас, до тех пор, как наш метод визуализации возвращает React элемент, мы свободны совершать какие-либо действия в нем; мы
можем определить несколько вспомогательных методов recordRow
и recordForm
и вызывать их условно внутри визуализации в зависимости от содержания @ state.edit
.
У нас уже есть первый вариант recordRow
, это наш текущий метод визуализации. Давайте переместим содержимое рендеринга в наш совершенно новый метод recordRow
и добавим дополнительный код к нему:
# 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 элемент ждет сигнала от onClick
для вызова handleToggle
Двигаемся дальше. Реализация recordForm
должна быть следующей структуры но с input полем в каждой клетке. Мы будем использовать новый ref
атрибут для наших input'oв, сделаем их доступными; поскольку этот компонент не обрабатывает состояние, этот новый атрибут позволит нашему компонент считывать данные, предоставленные пользователем через@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'
...
Не волнуйтесь. Этот метод мог быть больше но это просто html синтаксис.
Замечание. Мы вызываем @handleEdit
, когда пользователь нажимает на кнопку Update
, мы собираемся использовать аналогичный поток в качестве одной реализации для удаления записей.
Заметили ли вы отличие в том, как создаются React.DOM.inputs
? Мы используем defaultValue
по умолчанию вместо того что бы задать начальные входные данные, это происходит потому, что, используя только значение без OnChange
будет в конечном итоге создан только для чтения input'ов.
Наконец, метод визуализации сводится к следующему коду:
# app/assets/javascripts/components/record.js.coffee
@Record = React.createClass
...
render: ->
if @state.edit
@recordForm()
else
@recordRow()
Вы можете обновить свой браузер, чтобы поиграть с новым поведением переключения, но оно не представляет никаких изменений, поскольку мы не реализовали реальную возможность обновления.
Для обработки обновлений записей, нам нужно добавить метод обновления к нашему контроллеру Rails:
# 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
, который будет отправлять AJAX запрос на сервер с обновленной информацией записи, тогда он уведомляет об этом родительский компонент, отправив обновленную версию записи с помощью метода handleEditRecord
, этот метод будет получен через @props
, так же, как мы делали это раньше при удалении записей:
# 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 и отправив их дословно на бэкэнд. Обновление состояния для переключения в режим редактирования на успех не является обязательным, но пользователь, безусловно, будет благодарить нас за это.
И последнее, но не в последнюю очередь, нам просто нужно обновить состояние на компоненте Records, чтобы перезаписать прежний record с новой версией потомка record и пусть React выполнять свою магию. Реализация может выглядеть следующим образом:
# 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
изменение нашего состояния может привести к более конкретным методам. Конечным звеном между Records и Record выступает метод @updateRecord
он передается через handleEditRecord
настройки.
Обновите свой браузер в последний раз и попробуйте обновить некоторые существующие записи, обратите внимание, как боксы в верхней части страницы, следят за каждой записью, которую вы измените.
Мы сделали!
Мы только что построили небольшой Rails + React приложение с нуля!
Вы можете увидеть результат кода тут исправить тут
Завершающая мысль: React.js, простота и гибкость
Мы рассмотрели некоторые из функциональных возможностей React и узнали, что он едва вводит новые понятия. Я слышал комментарии, как люди говорят X или Y рамки, JavaScript имеет крутую кривую обучения из-за всех ново-введенных понятий, это не React случай; он реализует основные понятия JavaScript, такие как обработчики событий и привязки, что делает его простым в освоении и познании. Опять же, одна из его сильных сторон это его простота.
Мы также узнали на примере, как интегрировать его в «активную работу и насколько хорошо он играет вместе с CoffeeScript, JQuery, Turbolinks, и остальной частю рельсов» Рельс-оркестр так сказать. Но это не единственный способ достижения желаемых результатов. Например, если вы не используете Turbolinks (а значит, вам не нужно react_ujs) вы можете использовать Rails активы вместо гема react-reils, вы могли бы использовать JBuilder для построения более сложных JSON ответов вместо рендеринга объектов JSON; однако вы все равно сможете получить те же прекрасные результаты.
Автор: markosipenko