React.js — Руководство для Rails разработчиков

в 20:16, , рубрики: React, react.js, ReactJS, ruby, ruby on rails

На начальном уровне такие переводы — мой вклад в развитие rails сообщества.
Дальше в тексте все что выделено курсивом мои замечания (таких будет не много:) )

image

Введение в 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 дебет. Вот макет проекта:

image

Суммарно приложение будет вести себя так:

  1. Когда пользователь создает новую запись через горизонтальную форму, она будет вставлена в таблицу записей
  2. Пользователь может редактировать любую существующую запись
  3. Кликнув на кнопку Delete он удалит ассоциацию из таблицы
  4. Добавление, редактирование или удаление существующей записи будет обновлять количество боксов в верху страницы

Инициализация 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.

Обновите браузер

image

Отлично. Мы отрендерили наш первый 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 консоли браузера (и вероятно, в ближайшем будущем, иногда, получать головную боль).

image

Вы можете посмотреть код этой секции тут или вы можете посмотреть измения секции.

Связь между родительскими и дочерними элементами: создание 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 действия, это:

  1. Обновлять компоненты состояния
  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 кнопка вкл/выкл зависит от валидации данных.

image

Сейчас наш контроллер и форма на месте. Пришло время что бы отправить нашу новую запись на сервер. Нам нужно обработать формы представления событий. Для выполнения задачи нам нужно добавить на 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
      ...

Просмотрим новый метод построчно:

  1. Предотвратить форму отправки
  2. POST новой record информации текущего URL
  3. Успешный коллбек

Успешный коллбек — это ключ этого процесса, после успешного создания новой записи кто то должен сообщить об этом действии, и состояние обновляется до нового значения. Вы помните когда я упомянул что этот компонент взаимодействует с другими компонентами через настройки (или @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 не удивительно, на этот раз запись была добавлена почти незамедлительно и форма стала пустой после нажатия. Просто выполнилось обновление, конечно бекэнд заполнился новыми данными
image

Если вы используете другой 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 отображаемых бокса, суммы мы вычислили раньше. Но подождите! Есть еще! Создадим новую запись и увидим магию в работе.

image

Вы можете увидеть результат кода
исправления тут

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'

Сохраните ваш файл, обновите браузер и Мы имеем нерабочую кнопку без каких либо событий прикрепленных к ней.

image

Давайте добавить некоторую функциональность. Как мы узнали из нашего RecordForm компонента, используя список:

  1. Удалить событие внути потомка Record компонента (onClick)
  2. Выполнять экшн (отправить DELETE запрос к серверу в этом случае)
  3. Уведомить Records родительских компонентов об этом действии (отправка / прием метод обработчика через настройки)
  4. Обновить состояние 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 это то что первый будет только обновлять один ключ состояния объекта, а второй будет полностью переопределять текущее состояние компонента с любым новым объектом который мы посылаем.

После обновления последнего бита кода обновите окно браузера и пробуйте удалить запись, должно произойти две вещи:

  1. Запись должна пропасть из таблицы
  2. Индикатор должен мгновенно обновить кол-во (для этого ненужен никакой другой код).

image

Мы почти закончили но перед установкой последней фичи мы можем применить маленький рефакторинг в то же время, ввести новую функцию 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()

Вы можете обновить свой браузер, чтобы поиграть с новым поведением переключения, но оно не представляет никаких изменений, поскольку мы не реализовали реальную возможность обновления.

image

Для обработки обновлений записей, нам нужно добавить метод обновления к нашему контроллеру 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 настройки.

Обновите свой браузер в последний раз и попробуйте обновить некоторые существующие записи, обратите внимание, как боксы в верхней части страницы, следят за каждой записью, которую вы измените.
image

Мы сделали!
Мы только что построили небольшой Rails + React приложение с нуля!

Вы можете увидеть результат кода тут исправить тут

Завершающая мысль: React.js, простота и гибкость

Мы рассмотрели некоторые из функциональных возможностей React и узнали, что он едва вводит новые понятия. Я слышал комментарии, как люди говорят X или Y рамки, JavaScript имеет крутую кривую обучения из-за всех ново-введенных понятий, это не React случай; он реализует основные понятия JavaScript, такие как обработчики событий и привязки, что делает его простым в освоении и познании. Опять же, одна из его сильных сторон это его простота.

Мы также узнали на примере, как интегрировать его в «активную работу и насколько хорошо он играет вместе с CoffeeScript, JQuery, Turbolinks, и остальной частю рельсов» Рельс-оркестр так сказать. Но это не единственный способ достижения желаемых результатов. Например, если вы не используете Turbolinks (а значит, вам не нужно react_ujs) вы можете использовать Rails активы вместо гема react-reils, вы могли бы использовать JBuilder для построения более сложных JSON ответов вместо рендеринга объектов JSON; однако вы все равно сможете получить те же прекрасные результаты.

Автор: markosipenko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js