Статьи

Как создать приложение React, работающее с Rails 5.1 API

Реагировать + Ruby on Rails = ?

Реакция взяла мир развития фронтенда штурмом. Это отличная библиотека JavaScript для создания пользовательских интерфейсов. И это замечательно в сочетании с Ruby on Rails. Вы можете использовать Rails на заднем конце с React на переднем конце различными способами .

В этом практическом руководстве мы собираемся создать приложение React, которое работает с Rails 5.1 API.

Читать Rails: от новичка до ниндзя
Руководство для начинающих по Rails!

Вы можете посмотреть видео версию этого урока здесь .

Посмотрите видео версию этого урока

Чтобы следовать этому уроку, вы должны быть знакомы с Rails и знать основы React.

Если вы не используете Rails, вы также можете создать API на выбранном вами языке или платформе и просто использовать это руководство для части React.

Учебное пособие охватывает функциональные компоненты без сохранения состояния, компоненты на основе классов, использование приложения Create React, использование axios для выполнения вызовов API, помощник по постоянству и многое другое.

Что мы собираемся построить

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

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

Демо приложения Idea Board

В конце этого урока у нас будет функциональное приложение CRUD, в которое мы можем добавить некоторые улучшения, такие как анимация, сортировка и поиск, в будущем уроке.

Вы можете увидеть полный код приложения на GitHub:

Ideaboard Rails API

Интерфейс Ideaboard React

Настройка Rails API

Давайте начнем с создания Rails API. Мы будем использовать встроенную функцию Rails для создания приложений только для API.

Убедитесь, что у вас установлена ​​версия 5.1 или более поздняя версия Rails.

gem install rails -v 5.1.3 

На момент написания этого руководства 5.1.3 был последним стабильным выпуском, поэтому мы будем его использовать.

Затем сгенерируйте новое приложение Rails API с флагом --api .

 rails new --api ideaboard-api cd ideaboard-api 

Далее давайте создадим модель данных. Нам нужна только одна модель данных для идей с двумя полями — заголовок и тело, оба типа string .

Давайте сгенерируем и запустим миграцию:

 rails generate model Idea title:string body:string rails db:migrate 

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

В файле db/seeds.rb добавьте следующий код:

 ideas = Idea.create( [ { title: "A new cake recipe", body: "Made of chocolate" }, { title: "A twitter client idea", body: "Only for replying to mentions and DMs" }, { title: "A novel set in Italy", body: "A mafia crime drama starring Berlusconi" }, { title: "Card game design", body: "Like Uno but involves drinking" } ]) 

Не стесняйтесь добавлять свои собственные идеи.

Затем запустите:

 rails db:seed 

Далее, давайте создадим IdeasController с действием index в app/controllers/api/v1/ideas_controller.rb :

 module Api::V1 class IdeasController < ApplicationController def index @ideas = Idea.all render json: @ideas end end end 

Обратите внимание, что контроллер находится в app/controllers/api/v1 потому что мы версии нашего API. Это хорошая практика, чтобы избежать внесения изменений и обеспечить некоторую обратную совместимость с нашим API.

Затем добавьте идеи в качестве ресурса в config/routes.rb :

 Rails.application.routes.draw do namespace :api do namespace :v1 do resources :ideas end end end 

Хорошо, теперь давайте проверим нашу первую конечную точку API!

Сначала давайте запустим сервер Rails API на порту 3001:

 rails s -p 3001 

Затем давайте проверим нашу конечную точку для получения всех идей с помощью curl:

 curl -G http://localhost:3001/api/v1/ideas 

И это печатает все наши идеи в формате JSON:

 [{"id":18,"title":"Card game design","body":"Like Uno but involves drinking","created_at":"2017-09-05T15:42:36.217Z","updated_at":"2017-09-05T15:42:36.217Z"},{"id":17,"title":"A novel set in Italy","body":"A mafia crime drama starring Berlusconi","created_at":"2017-09-05T15:42:36.213Z","updated_at":"2017-09-05T15:42:36.213Z"},{"id":16,"title":"A twitter client idea","body":"Only for replying to mentions and DMs","created_at":"2017-09-05T15:42:36.209Z","updated_at":"2017-09-05T15:42:36.209Z"},{"id":15,"title":"A new cake recipe","body":"Made of chocolate","created_at":"2017-09-05T15:42:36.205Z","updated_at":"2017-09-05T15:42:36.205Z"}] 

Мы также можем проверить конечную точку в браузере, перейдя по адресу http: // localhost: 3001 / api / v1 / ideas .

Тестирование нашей конечной точки API в браузере

Настройка нашего интерфейсного приложения с помощью приложения Create React

Теперь, когда у нас есть базовый API, давайте настроим наше внешнее приложение React с помощью приложения Create React . Create React App — это проект Facebook, который поможет вам быстро приступить к работе с приложением React без какой-либо настройки.

Во-первых, убедитесь, что у вас установлены Node.js и npm. Вы можете скачать установщик с веб-сайта Node.js. Затем установите приложение Create React, запустив:

 npm install -g create-react-app 

Затем убедитесь, что вы находитесь за пределами каталога Rails, и выполните следующую команду:

 create-react-app ideaboard 

Это создаст приложение React под названием ideaboard, которое мы теперь будем использовать для общения с нашим Rails API.

Давайте запустим приложение React:

 cd ideaboard npm start 

Это откроет его на http: // localhost: 3000 .

Домашняя страница нового приложения, созданного приложением Create React

Приложение имеет страницу по умолчанию с компонентом React под названием App, на котором отображается логотип React и приветственное сообщение.

Содержимое страницы отображается через компонент React в файле src/App.js :

 import React, { Component } from 'react' import logo from './logo.svg' import './App.css' class App extends Component { render() { return ( <div className="App"> <div className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <h2>Welcome to React</h2> </div> <p className="App-intro"> To get started, edit <code>src/App.js</code> and save to reload. </p> </div> ); } } export default App 

Наш первый компонент React

Наш следующий шаг — отредактировать этот файл, чтобы использовать только что созданный API, и перечислить все идеи на странице.

Начнем с того, что заменим приветственное сообщение тегом h1 заголовком нашего приложения «Доска идей».

Давайте также добавим новый компонент под названием IdeasContainer . Нам нужно импортировать его и добавить в функцию рендеринга:

 import React, { Component } from 'react' import './App.css' import IdeasContainer from './components/IdeasContainer' class App extends Component { render() { return ( <div className="App"> <div className="App-header"> <h1>Idea Board</h1> </div> <IdeasContainer /> </div> ); } } export default App 

Давайте создадим этот компонент IdeasContainer в новом файле в src/IdeasContainer.js src/components .

 import React, { Component } from 'react' class IdeasContainer extends Component { render() { return ( <div> Ideas </div> ) } } export default IdeasContainer 

Давайте также изменим стили в App.css чтобы они имели белый заголовок и черный текст, а также удалим стили, которые нам не нужны:

 .App-header { text-align: center; height: 150px; padding: 20px; } .App-intro { font-size: large; } 

Скелет Идея доска объявлений

Этот компонент должен общаться с нашей конечной точкой Rails API для получения всех идей и их отображения.

Извлечение данных API с помощью axios

Мы сделаем Ajax-вызов API в методе жизненного цикла componentDidMount() компонента IdeasContainer и сохраним идеи в состоянии компонента.

Давайте начнем с инициализации состояния в конструкторе с идеями в виде пустого массива:

 constructor(props) { super(props) this.state = { ideas: [] } } 

А затем мы обновим состояние в componentDidMount() .

Давайте использовать библиотеку axios для выполнения вызовов API. Вы также можете использовать fetch или jQuery, если хотите.

Установите axios с npm:

 npm install axios --save 

Затем импортируйте его в IdeasContainer :

 import axios from 'axios' 

И используйте его в componentDidMount() :

 componentDidMount() { axios.get('http://localhost:3001/api/v1/ideas.json') .then(response => { console.log(response) this.setState({ideas: response.data}) }) .catch(error => console.log(error)) } 

Теперь, если мы обновим страницу … она не будет работать!

Нет заголовка Access-Control-Allow-Origin

Мы получим ошибку «Нет заголовка Access-Control-Allow-Origin», потому что наш API находится на другом порту, и мы не включили Cross Source Resource Sharing (CORS).

Включение перекрестного общего доступа к ресурсам (CORS)

Итак, давайте сначала включим CORS, используя гем rack-cors в нашем приложении Rails.

Добавьте драгоценный камень в Gemfile:

 gem 'rack-cors', :require => 'rack/cors' 

Установите это:

 bundle install 

Затем добавьте конфигурацию промежуточного программного обеспечения в файл config/application.rb :

 config.middleware.insert_before 0, Rack::Cors do allow do origins 'http://localhost:3000' resource '*', :headers => :any, :methods => [:get, :post, :put, :delete, :options] end end 

Мы ограничиваем источники нашего внешнего приложения по адресу http://localhost:3000 и разрешаем доступ к стандартным методам конечной точки API REST для всех ресурсов.

Теперь нам нужно перезапустить сервер Rails, и если мы обновим браузер, мы больше не получим ошибку CORS.

Страница загрузится нормально, и мы увидим данные ответов, записанные в консоли.

Идеи JSON-ответа от API, записанного на консоль

Теперь, когда мы знаем, что можем извлекать идеи из нашего API, давайте использовать их в нашем компоненте React.

Мы можем изменить функцию рендеринга, чтобы перебирать идеи списка из состояния и отображать каждое из них:

 render() { return ( <div> {this.state.ideas.map((idea) => { return( <div className="tile" key={idea.id} > <h4>{idea.title}</h4> <p>{idea.body}</p> </div> ) })} </div> ); } 

Это покажет все идеи на странице сейчас.

Список идей, отображаемых компонентом

Обратите внимание на key атрибут на плитке div.

Мы должны включить его при создании списков элементов. Клавиши помогают React определить, какие элементы были изменены, добавлены или удалены.

Теперь давайте добавим немного стилей в App.css чтобы каждая идея выглядела как плитка:

 .tile { height: 150px; width: 150px; margin: 10px; background: lightyellow; float: left; font-size: 11px; text-align: left; } 

Мы устанавливаем высоту, ширину, цвет фона и заставляем плитки плавать влево.

Стилизованная идея плитки

Функциональные компоненты без сохранения состояния

Прежде чем мы продолжим, давайте проведем рефакторинг нашего кода и переместим JSX для плиток идей в отдельный компонент под названием Idea .

 import React from 'react' const Idea = ({idea}) => <div className="tile" key={idea.id}> <h4>{idea.title}</h4> <p>{idea.body}</p> </div> export default Idea 

Это функциональный компонент без сохранения состояния (или, как его называют некоторые, «тупой» компонент), что означает, что он не обрабатывает никакое состояние. Это чистая функция, которая принимает некоторые данные и возвращает JSX.

Затем внутри функции карты в IdeasContainer мы можем вернуть новый компонент Idea:

 {this.state.ideas.map((idea) => { return (<Idea idea={idea} key={idea.id} />) })} 

Не забудьте также импортировать Idea :

 import Idea from './Idea' 

Отлично, так что первая часть нашего приложения завершена. У нас есть API с конечной точкой для получения идей и приложение React для отображения их в виде плиток на доске!

Добавление новой записи

Далее мы добавим способ создания новых идей.

Давайте начнем с добавления кнопки, чтобы добавить новую идею.

Внутри функции рендеринга в IdeasContainer добавьте:

 <button className="newIdeaButton"> New Idea </button> 

И давайте добавим немного стилей для этого в App.css :

 .newIdeaButton { background: darkblue; color: white; border: none; font-size: 18px; cursor: pointer; margin-right: 10px; margin-left: 10px; padding:10px; } 

Кнопка Новая идея

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

Как только мы отредактируем форму, мы хотим отправить ее в наш API, чтобы создать новую идею.

Конечная точка API для создания новой идеи

Итак, начнем с создания конечной точки API для создания новых идей в IdeasController :

 def create @idea = Idea.create(idea_params) render json: @idea end private def idea_params params.require(:idea).permit(:title, :body) end 

Поскольку в Rails используются idea_params параметры , мы определяем закрытый метод idea_params для idea_params в белый список idea_params нам параметров — title и body .

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

Вернувшись в наше приложение React, теперь давайте добавим обработчик addNewIdea к кнопке новой идеи:

 <button className="newIdeaButton" onClick={this.addNewIdea} > New Idea </button> 

Давайте определим addNewIdea как функцию, которая использует axios для вызова POST нашей новой конечной точки идеи с пустой идеей. Давайте пока просто запишем ответ на консоль:

 addNewIdea = () => { axios.post( 'http://localhost:3001/api/v1/ideas', { idea: { title: '', body: '' } } ) .then(response => { console.log(response) }) .catch(error => console.log(error)) } 

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

Пустые данные идеи записываются в консоль

Когда мы обновляем страницу, мы видим пустую плитку, представляющую нашу новую идею.

Пустая идея плитки

Что мы действительно хотим, чтобы это произошло, так это то, что, когда мы нажимаем кнопку новой идеи, идея создается немедленно, и на странице появляется форма для редактирования этой идеи.

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

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

Итак, давайте изменим определение @ideas в IdeasController чтобы упорядочить идеи в порядке убывания их времени created_at :

 module Api::V1 class IdeasController < ApplicationController def index @ideas = Idea.order("created_at DESC") render json: @ideas end end end 

Хорошо, теперь последние идеи отображаются в первую очередь.

Сначала показывайте новые идеи

Теперь давайте продолжим с определением addNewIdea .

Сначала давайте воспользуемся ответом на наш вызов POST чтобы обновить массив идей в состоянии, чтобы при добавлении новой идеи она сразу появлялась на странице.

Мы могли бы просто push новую идею в массив, поскольку это только пример приложения, но рекомендуется использовать неизменяемые данные.

Итак, давайте используем immutability-helper , который является хорошим пакетом для обновления данных без непосредственного их изменения.

Установите его с помощью npm:

 npm install immutability-helper --save 

Затем импортируйте функцию update в IdeasContainer :

 import update from 'immutability-helper' 

Теперь давайте использовать его внутри addNewIdea чтобы вставить нашу новую идею в начало массива идей:

 addNewIdea = () => { axios.post( 'http://localhost:3001/api/v1/ideas', { idea: { title: '', body: '' } } ) .then(response => { console.log(response) const ideas = update(this.state.ideas, { $splice: [[0, 0, response.data]] }) this.setState({ideas: ideas}) }) .catch(error => console.log(error)) } 

Мы создаем новую копию this.state.ideas и используем команду $splice чтобы вставить новую идею (в response.data ) в 0-й индекс этого массива.

Затем мы используем этот новый массив идей для обновления состояния с помощью setState .

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

Добавить новую идею

Теперь мы можем приступить к редактированию этой идеи.

Во-первых, нам нужно новое свойство editingIdeaId , которое отслеживает, какая идея редактируется в данный момент.

По умолчанию мы не редактируем ни одной идеи, поэтому давайте инициализируем editingIdeaId в состоянии с нулевым значением:

 this.state = { ideas: [], editingIdeaId: null } 

Теперь, когда мы добавляем новую идею, в дополнение к добавлению ее в state.ideas , мы также хотим установить ее id в качестве значения state.editingIdeaId . Итак, давайте setState вызов addNewIdea в addNewIdea чтобы включить также set editingIdeaId :

 this.setState({ ideas: ideas, editingIdeaId: response.data.id }) 

Таким образом, это означает, что мы только что добавили новую идею и хотим немедленно ее отредактировать.

Полная функция addNewIdea теперь выглядит так:

 addNewIdea = () => { axios.post( 'http://localhost:3001/api/v1/ideas', { idea: { title: '', body: '' } } ) .then(response => { const ideas = update(this.state.ideas, { $splice: [[0, 0, response.data]] }) this.setState({ ideas: ideas, editingIdeaId: response.data.id }) }) .catch(error => console.log(error)) } 

Компонент формы

Теперь мы можем использовать state.editingIdeaId в функции рендеринга, чтобы вместо отображения обычной плитки идей мы могли отображать форму.

Внутри функции map давайте изменим возвращаемое значение на условный оператор, который отображает компонент IdeaForm если идентификатор идеи совпадает с state.editingIdeaId , в противном случае отображается компонент Idea :

 {this.state.ideas.map((idea) => { if(this.state.editingIdeaId === idea.id) { return(<IdeaForm idea={idea} key={idea.id} />) } else { return (<Idea idea={idea} key={idea.id} />) } })} 

Давайте импортируем компонент IdeasContainer в IdeasContainer :

 import IdeaForm from './IdeaForm' 

И давайте определим это в IdeaForm.js . Начнем с простого компонента класса, который отображает форму с двумя полями ввода для заголовка и тела идеи:

 import React, { Component } from 'react' import axios from 'axios' class IdeaForm extends Component { constructor(props) { super(props) this.state = { } } render() { return ( <div className="tile"> <form> <input className='input' type="text" name="title" placeholder='Enter a Title' /> <textarea className='input' name="body" placeholder='Describe your idea'></textarea> </form> </div> ); } } export default IdeaForm 

Давайте добавим немного CSS в App.css для App.css формы:

 .input { border: 0; background: none; outline: none; margin-top:10px; width: 140px; font-size: 11px; } .input:focus { border: solid 1px lightgrey; } textarea { resize: none; height: 90px; font-size: 11px; } 

Теперь, когда мы нажимаем на кнопку новой идеи, появляется новая плитка с формой:

Стилизованная форма для редактирования новой идеи

Теперь давайте сделаем эту форму функциональной!

Нам нужно подключить поля ввода формы к состоянию.

Во-первых, давайте инициализируем IdeaForm состояния компонента IdeaForm из idea которую он получает от IdeasContainer :

 class IdeaForm extends Component { constructor(props) { super(props) this.state = { title: this.props.idea.title, body: this.props.idea.body } } 

Затем установите значения полей формы в соответствующие значения состояния и установите обработчик onChange :

 <form> <input className='input' type="text" name="title" placeholder='Enter a Title' value={this.state.title} onChange={this.handleInput} /> <textarea className='input' name="body" placeholder='Describe your idea' value={this.state.body} onChange={this.handleInput}> </textarea> </form> 

Мы определим handleInput таким образом, чтобы при вводе в любом из полей ввода соответствующее значение состояния, а затем значение поля обновлялось:

 handleInput = (e) => { this.setState({[e.target.name]: e.target.value}) } 

Отслеживание изменений состояния в React Developer Tools

Давайте посмотрим на эти изменения состояния в действии с расширением браузера React Developer Tools. Вы можете получить его для Chrome здесь и для Firefox здесь .

После установки обновите страницу приложения и откройте консоль разработчика. Вы должны увидеть новую вкладку React.

Когда вы щелкнете по нему, вы увидите дерево компонентов нашего приложения слева и все реквизиты и состояние, связанные с каждым компонентом справа.

Реагируйте на инструменты разработчика, показывающие обновления состояния

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

Конечная точка API для обновления идей

Во-первых, нам нужно определить конечную точку API для обновления идей. Итак, давайте добавим действие update в IdeasController :

 def update @idea = Idea.find(params[:id]) @idea.update_attributes(idea_params) render json: @idea end 

Вернувшись в IdeaForm.js , мы установим обработчик handleBlur с именем handleBlur в форму:

 <form onBlur={this.handleBlur} > 

Мы определим handleBlur чтобы сделать PUT вызов нашей конечной точки API для обновления идей данными идеи из состояния. А пока давайте просто запишем ответ на консоль и посмотрим, работает ли наш вызов:

 handleBlur = () => { const idea = { title: this.state.title, body: this.state.body } axios.put( `http://localhost:3001/api/v1/ideas/${this.props.idea.id}`, { idea: idea }) .then(response => { console.log(response) }) .catch(error => console.log(error)) } 

Нам также нужно импортировать axios в этот файл, чтобы иметь возможность использовать его:

 import axios from 'axios' 

Теперь, если мы нажмем кнопку новой идеи, отредактируем ее заголовок и вычеркнем из этого поля, мы увидим наш ответ API, зарегистрированный в консоли, с новыми отредактированными данными идеи.

То же самое происходит, если мы редактируем тело и размываем это поле.

Проверка отредактированных данных идеи в консоли

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

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

Мы будем использовать метод с именем updateIdea , который мы передадим в качестве поддержки от IdeasContainer к IdeaForm . Мы будем вызывать updateIdea с данными ответа от нашего вызова API:

 handleBlur = () => { const idea = { title: this.state.title, body: this.state.body } axios.put( `http://localhost:3001/api/v1/ideas/${this.props.idea.id}`, { idea: idea }) .then(response => { console.log(response) this.props.updateIdea(response.data) }) .catch(error => console.log(error)) } 

Теперь в IdeasContainer давайте отправим функцию updateIdea в качестве опоры для IdeaForm:

 <IdeaForm idea={idea} key={idea.id} updateIdea={this.updateIdea} /> 

Давайте определим функцию для выполнения неизменного обновления идеи в state.ideas :

 updateIdea = (idea) => { const ideaIndex = this.state.ideas.findIndex(x => x.id === idea.id) const ideas = update(this.state.ideas, { [ideaIndex]: { $set: idea } }) this.setState({ideas: ideas}) } 

Сначала мы находим индекс отредактированной идеи в массиве, а затем используем команду $set чтобы заменить старое значение новым. Наконец, мы вызываем setState для обновления state.ideas .

Мы можем увидеть это в действии в браузере с открытой вкладкой React Developer Tools.

Обновления состояния IdeasContainer

Отображение уведомления об успехе

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

Давайте добавим диапазон рядом с кнопкой новой идеи, чтобы отобразить уведомление от значения в состоянии:

 <span className="notification"> {this.state.notification} </span> 

Давайте инициализируем state.notification как пустую строку:

 constructor(props) { super(props) this.state = { ideas: [], editingIdeaId: null, notification: '' } } 

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

Поэтому в вызове setState в updateIdea , помимо обновления ideas , давайте также обновим notification :

 this.setState({ ideas: ideas, notification: 'All changes saved' }) 

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

Уведомление об успешном обновлении идеи

Мы также хотим сбросить уведомление, как только пользователь внесет изменение, которое еще не было сохранено.

Итак, в функции IdeaForm компонента IdeaForm , давайте вызовем функцию resetNotification для сброса уведомляющего сообщения:

 handleInput = (e) => { this.props.resetNotification() this.setState({[e.target.name]: e.target.value}) } 

Теперь, внутри функции render IdeasContainer , давайте также передадим resetNotification в качестве опоры для IdeaForm :

 <IdeaForm idea={idea} key={idea.id} updateIdea={this.updateIdea} resetNotification={this.resetNotification} /> 

Давайте определим resetNotification как:

 resetNotification = () => { this.setState({notification: ''}) } 

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

Сбросить уведомление о несохраненных изменениях

Редактирование существующей идеи

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

Затем мы можем отредактировать идею, и она будет сохранена при размытии.

Чтобы добавить эту функцию, нам нужно добавить обработчик кликов на плитки идей.

Итак, сначала нам нужно преобразовать наш компонент Idea из функционального компонента в компонент класса, а затем мы можем задать функцию-обработчик handleClick для заголовка и тела.

 import React, { Component } from 'react' class Idea extends Component { handleClick = () => { this.props.onClick(this.props.idea.id) } render () { return( <div className="tile"> <h4 onClick={this.handleClick}> {this.props.idea.title} </h4> <p onClick={this.handleClick}> {this.props.idea.body} </p> </div> ) } } export default Idea 

Обратите внимание, что мы должны добавить this.props. использовать значение props, потому что, в отличие от функционального компонента, мы больше не разрушаем объект props.

handleClick вызывает this.props.onClick с идентификатором идеи.

Теперь, внутри функции render IdeasContainer , давайте также передадим onClick в качестве опоры для Idea :

 return (<Idea idea={idea} key={idea.id} onClick={this.enableEditing} />) 

Мы определим enableEditing чтобы установить значение state.editingIdeaId для state.editingIdeaId идеи:

 enableEditing = (id) => { this.setState({editingIdeaId: id}) } 

Теперь, когда мы нажимаем на плитку, она мгновенно становится редактируемой!

Нажмите на плитку идеи, чтобы редактировать ее

Когда мы нажимаем на плитку, когда форма появляется, давайте также установим фокус курсора на поле ввода заголовка.

Мы можем сделать это, добавив IdeaForm поле ввода заголовка в IdeaForm :

 <input className='input' type="text" name="title" placeholder='Enter a Title' value={this.state.title} onChange={this.handleInput} ref={this.props.titleRef} /> 

Нам нужно передать ref как опору, потому что мы хотим использовать его в родительском компоненте IdeasContainer , где мы можем определить ref как функцию обратного вызова:

 <IdeaForm idea={idea} key={idea.id} updateIdea={this.updateIdea} titleRef= {input => this.title = input} resetNotification={this.resetNotification} /> 

Теперь мы можем использовать эту enableEditing в enableEditing чтобы установить фокус в поле ввода заголовка:

 enableEditing = (id) => { this.setState({editingIdeaId: id}, () => { this.title.focus() }) } 

Обратите внимание, что мы не вызывали this.title.focus() как отдельную функцию после вызова setState . Вместо этого мы передали его setState внутри обратного вызова в качестве второго аргумента.

Мы сделали это, потому что setState не всегда сразу обновляет компонент. Передавая фокус-вызов в обратном вызове, мы гарантируем, что он будет вызван только после обновления компонента.

Теперь, если мы попробуем приложение в браузере, когда мы нажимаем на плитку идеи, оно становится редактируемым с помощью формы, и курсор фокусируется внутри поля ввода заголовка.

Нажмите, чтобы отредактировать идею и установить фокус на поле ввода

Так что теперь мы можем добавлять и редактировать идеи.

Удаление идеи

Наконец, мы хотим иметь возможность удалять идеи.

Когда мы наводим курсор на плитку идеи, мы хотим, чтобы кнопка удаления (в форме красного креста) появилась в верхнем правом углу. Нажатие на этот крестик должно удалить идею и удалить плитку с доски.

Итак, давайте начнем с добавления некоторой разметки и CSS для отображения кнопки удаления при наведении курсора.

В компоненте Idea добавьте span с классом deleteButton и текстом «x»:

 <div className="tile"> <span className="deleteButton"> x </span> 

Затем давайте добавим CSS в App.css чтобы скрыть этот диапазон по умолчанию и сделать его видимым при наведении курсора на плитку:

 .deleteButton { visibility: hidden; float: right; margin: 5px; font-size: 14px; cursor: pointer; color: red; } .tile:hover .deleteButton { visibility: visible; } 

Кнопка удаления появляется при наведении на плитку

Далее, давайте добавим обработчик handleDelete к этой кнопке удаления, которая затем удалит идею:

 <span className="deleteButton" onClick={this.handleDelete}> x </span> 

Подобно handleClick , мы определим handleDelete как функцию стрелки, которая вызывает другую функцию this.props.onDelete с идентификатором идеи плитки:

 handleDelete = () => { this.props.onDelete(this.props.idea.id) } 

Давайте перейдем к onDelete в качестве опоры от IdeasContainer :

 <Idea idea={idea} key={idea.id} onClick={this.enableEditing} onDelete={this.deleteIdea} /> 

Мы определим deleteIdea через мгновение, но сначала давайте добавим конечную точку API для удаления идей в IdeasController :

 def destroy @idea = Idea.find(params[:id]) if @idea.destroy head :no_content, status: :ok else render json: @idea.errors, status: :unprocessable_entity end end 

Теперь давайте определим deleteIdea в IdeasContainer как функцию, которая выполняет вызов DELETE для нашего API с идентификатором идеи и, в случае успеха, обновляет state.ideas :

 deleteIdea = (id) => { axios.delete(`http://localhost:3001/api/v1/ideas/${id}`) .then(response => { const ideaIndex = this.state.ideas.findIndex(x => x.id === id) const ideas = update(this.state.ideas, { $splice: [[ideaIndex, 1]]}) this.setState({ideas: ideas}) }) .catch(error => console.log(error)) } 

Еще раз, мы ищем индекс удаленной идеи, используем update с командой $splice чтобы создать новый массив идей, а затем обновляем state.ideas с этим.

Теперь мы можем попробовать это в браузере. Когда мы наводим курсор на плитку идеи, появляется красная кнопка удаления. Нажатие на нее удаляет идею и удаляет плитку с доски.

Нажмите кнопку удаления, чтобы удалить идеи

Ура, теперь у нас есть функциональное приложение со всеми основными функциями CRUD!

Заворачивать

В этом руководстве мы создали полное приложение CRUD с использованием Rails 5.1 API и интерфейсного приложения React.

Наш API имеет три конечных точки, по одной для создания, обновления и удаления идей.

Мы использовали Create React App, чтобы сделать наше приложение React. Это сделало настройку совершенно безболезненной и легкой. Мы могли бы погрузиться прямо в создание нашего приложения, а не настраивать что-либо

Мы использовали axios для выполнения вызовов Ajax к API и immutability-helper для обновления данных.

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

Вы можете посмотреть видео версию этого урока здесь .

Вы можете увидеть полный код приложения на GitHub:

Ideaboard Rails API

Интерфейс Ideaboard React