みなさんはじめまして。
株式会社BitStarでエンジニアインターン
をしている塩原です。
下準備編がまだの人はこちら
怖くない!ReactとRailsで作るTODOアプリ 下準備編 - Qiita
さて今回でいよいよ実装へ入っていきたいと思います
まずその前にwebpackに関連するファイルの変更をする際にはwebpackのサーバーを立てておかなくてはならないので、ターミナルの別タブで以下のコマンドを走らせておいてください
$ npm run webpack-watch
仕様を分割して以下のステップでやっていきます
- とりあえずリストを表示する
- リストを追加する
- チェックした情報を保持する
- サーバーに送信してデータを保存する
- データベースにあるリストを初期表示する
とりあえず表示
- frontent/index.js
import React, { Component } from 'react'
import { render } from 'react-dom'
class App extends Component {
render() {
const { todoLists } = this.props
return (
<div>
<ul>
{this.listCreator(todoLists)}
</ul>
</div>
)
}
listCreator(todoLists) {
let lists = []
todoLists.forEach((todo, i, todoLists) => {
let idName = `todo_input_${i}`
lists.push(
<li key={i}>
<input key={i} type='checkbox' value={todo['title']} id={idName} />
<label key={i + 1} htmlFor={idName}>
{todo['title']}
</label>
</li>
)
})
return lists
}
}
render(
<App todoLists={[{title: "テスト"}, {title: "テスト2"}]} />,
document.getElementById('container'))
render(Reactコーポネント, 表示場所)
という風にかくとhtml側の<div id="container"></div>
の中にReactコンポーネントで作成した仮想DOMを生成することが可能になります
todoLists={[{title: "テスト"}, {title: "テスト2"}]}
という部分でpropsを渡すことでclass Appの中でthis.props.todoLists
という風に利用することができます
- app/view/todo_lists/index.html.erb
<h1>TODOアプリケーション</h1>
<div id="container"></div>
<%= javascript_include_tag 'webpack/react_components' %>
リストを追加する
さて次はリストを追加する機能を実装して行きましょう
- frontend/index.js
完成するコードはこんな感じ
import React, { Component } from 'react'
import { render } from 'react-dom'
class App extends Component {
constructor(props) {
super(props);
this.state = { todoLists: props.todoLists, value: '' }
}
render() {
return (
<div>
<ul>
{this.listCreator(this.state.todoLists)}
</ul>
<div>
<h3>TODOリストに追加する</h3>
<form onSubmit={(e) => this.handleSubmit(e)}>
<input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
<input type="submit" value="追加" />
</form>
</div>
</div>
)
}
listCreator(todoLists) {
let lists = []
todoLists.forEach((todo, i, todoLists) => {
let idName = `todo_input_${i}`
lists.push(
<li key={i}>
<input key={i} type='checkbox' value={todo['title']} id={idName} />
<label key={i + 1} htmlFor={idName}>
{todo['title']}
</label>
</li>
)
})
return lists
}
handleSubmit(e) {
e.preventDefault()
let title = this.state.value
let todoLists = [
...this.state.todoLists,
{
title: title
}
]
this.setState({ todoLists: todoLists, value: '' })
}
handleChange(event) {
this.setState({value: event.target.value});
}
}
render(
<App todoLists={[{title: "テスト"}, {title: "テスト2"}]} />,
document.getElementById('container'))
こんな風に追加ができるようになる
この部分で外部から渡されたpropsをstateに変換しています
なぜこんなことをするのかというとReactには以下の概念があります
propsはimmutableであり
stateはmutableであるというような概念があります
以下の記事などで詳しく説明してあります
React における State と Props の違い - Qiita
constructor(props) {
super(props);
this.state = { todoLists: props.todoLists, value: '' }
}
追加機能のjsxの部分でonSubmit
とonChange
が設定されています
<div>
<h3>TODOリストに追加する</h3>
<form onSubmit={(e) => this.handleSubmit(e)}>
<input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
<input type="submit" value="追加" />
</form>
</div>
二つの関数はそれぞれstateを更新するためのものです
// 送信したtodoリストを追加する
handleSubmit(e) {
e.preventDefault()
let title = this.state.value
let todoLists = [
...this.state.todoLists,
{
title: title
}
]
this.setState({ todoLists: todoLists, value: '' })
}
// inputフォームに入力した値を常にstateとして持つ
handleChange(event) {
this.setState({value: event.target.value});
}
チェック情報を保持する
import React, { Component } from 'react'
import { render } from 'react-dom'
class App extends Component {
constructor(props) {
super(props);
this.state = { todoLists: props.todoLists, value: '' }
}
render() {
return (
<div>
<ul>
{this.listCreator(this.state.todoLists)}
</ul>
<div>
<h3>TODOリストに追加する</h3>
<form onSubmit={(e) => this.handleSubmit(e)}>
<input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
<input type="submit" value="追加" />
</form>
</div>
</div>
)
}
listCreator(todoLists) {
let lists = []
todoLists.forEach((todo, i, todoLists) => {
let idName = `todo_input_${i}`
lists.push(
<li key={i}>
<input key={i} type='checkbox' value={todo['title']} id={idName} onClick={(e) => this.handleCheck(e, i)} />
<label key={i + 1} htmlFor={idName}>
{todo['title']}
</label>
</li>
)
})
return lists
}
handleSubmit(e) {
e.preventDefault()
let title = this.state.value
let todoLists = [
...this.state.todoLists,
{
title: title,
checked: false
}
]
this.setState({ todoLists: todoLists, value: '' })
}
handleChange(e) {
this.setState({value: e.target.value});
}
handleCheck(e, index) {
let stateTodoLists = this.state.todoLists
let targetTodo = stateTodoLists[index]
let todoLists = [
...stateTodoLists.slice(0, index),
Object.assign({}, targetTodo, {
checked: !targetTodo.checked
}),
...stateTodoLists.slice(index + 1)
]
this.setState({ todoLists: todoLists })
}
}
render(
<App todoLists={[]} />,
document.getElementById('container'))
先ほどと同じような流れですね
違いとしては追加とは違い、情報の更新のため位置の指定をする必要があります
<input key={i} type='checkbox' value={todo['title']} id={idName} onClick={(e) => this.handleCheck(e, i)} />
handleCheck
にindex情報を渡しています
このindexによってtodListsのどの位置をクリックしたかという情報を引数で渡しているわけです
handleCheck(e, index) {
let stateTodoLists = this.state.todoLists
let targetTodo = stateTodoLists[index]
let todoLists = [
...stateTodoLists.slice(0, index),
Object.assign({}, targetTodo, {
checked: !targetTodo.checked
}),
...stateTodoLists.slice(index + 1)
]
this.setState({ todoLists: todoLists })
}
stateを直接更新することはできないため新しくtodoListsの配列を作ってあげる必要があります
let todoLists = [
...stateTodoLists.slice(0, index),
Object.assign({}, targetTodo, {
checked: !targetTodo.checked
}),
...stateTodoLists.slice(index + 1)
]
Object.assign({}, 更新したいObject, 更新内容)
とすることで更新したいObjectを更新した状態のObjectを返します
let obj_a = {a: 3, b: 4}
let obj_b = Object.assign({}, obj_a, { b: 5})
console.log(obj_b) // {a: 3, b: 5}
サーバーに送信してデータを保存する
さていよいよサーバーに対してデータを送信したいと思います
送信するデータはリストそれぞれのタイトルにcheckしているかどうかの二点を送信します
import React, { Component } from 'react'
import { render } from 'react-dom'
class App extends Component {
constructor(props) {
super(props);
this.state = { todoLists: props.todoLists, value: '' }
}
render() {
return (
<div>
<ul>
{this.listCreator(this.state.todoLists)}
</ul>
<div>
<h3>TODOリストに追加する</h3>
<form onSubmit={(e) => this.handleSubmit(e)}>
<input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
<input type="submit" value="追加" />
</form>
</div>
<div>
<button onClick={(e) => this.ajaxSubmit(e, this.state.todoLists)}>保存</button>
</div>
</div>
)
}
listCreator(todoLists) {
let lists = []
todoLists.forEach((todo, i, todoLists) => {
let idName = `todo_input_${i}`
lists.push(
<li key={i}>
<input key={i} type='checkbox' value={todo['title']} id={idName} onClick={(e) => this.handleCheck(e, i)} />
<label key={i + 1} htmlFor={idName}>
{todo['title']}
</label>
</li>
)
})
return lists
}
handleSubmit(e) {
e.preventDefault()
let title = this.state.value
let todoLists = [
...this.state.todoLists,
{
title: title,
checked: false
}
]
this.setState({ todoLists: todoLists, value: '' })
}
handleChange(e) {
this.setState({value: e.target.value});
}
handleCheck(e, index) {
let stateTodoLists = this.state.todoLists
let targetTodo = stateTodoLists[index]
let todoLists = [
...stateTodoLists.slice(0, index),
Object.assign({}, targetTodo, {
checked: !targetTodo.checked
}),
...stateTodoLists.slice(index + 1)
]
this.setState({ todoLists: todoLists })
}
ajaxSubmit(e, todoLists) {
$.ajax({
url: '/todo_lists',
type: 'post',
contentType: 'application/json',
data: JSON.stringify({
todo_lists: todoLists
})
}).done(json => {
})
}
}
render(
<App todoLists={[]} />,
document.getElementById('container'))
- app/controller/todo_lists_controller.rb
class TodoListsController < ApplicationController
def index
todolists = TodoList.all.to_a.map do |list|
{ id: list.id, title: list.title, checked: list.checked }
end
@state = { todoLists: todolists }
end
def create
params["todo_lists"].each do |param|
title_param = param[:title]
checked_param = param[:checked]
TodoList.create(title: title_param, checked: checked_param)
end
redirect_to root_path
end
end
ではカラムも追加しておきましょう
$ bundle exec rails g migration add_column_todolists
新しくできたmigrationファイルに以下のカラムを追加する
class AddColumnTodolists < ActiveRecord::Migration[5.0]
def change
add_column :todo_lists, :title, :string
add_column :todo_lists, :checked, :boolean, default: :false
end
end
データベースのTodoListを初期表示する
さていよいよ最後です
やることは以下
- データベースのtodoListsのデータを初期表示のときに出す
- データベースに保存済みのデータにはidを持たせる
- config/routes.rb
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
resources :todo_lists, only: [:index, :create] do
collection do
get "fetch"
get "list"
end
end
root to: "todo_lists#index"
end
- app/controllers/todo_lists_controller.rb
class TodoListsController < ApplicationController
def index
todolists = TodoList.all.to_a.map do |list|
{ id: list.id, title: list.title, checked: list.checked }
end
@state = { todoLists: todolists }
end
def create
params["todo_lists"].each do |param|
puts param
id_param = param["id"]
title_param = param[:title]
checked_param = param[:checked]
if id_param.present?
TodoList.find(id_param).update(title: title_param, checked: checked_param)
else
TodoList.create(title: title_param, checked: checked_param)
end
end
redirect_to root_path
end
def list
@todo_lists = TodoList.all
end
def fetch
state = []
TodoList.all.each do |todo|
state << { title: todo.title, checked: todo.checked, id: todo.id }
end
render json: state
end
end
fetchアクションで初期表示に使うtodoリストのjsonを返します
- app/views/todo_lists/list.html.erb
<ul>
<%- @todo_lists.each do |list| %>
<li><%= list.checked ? "済" : "未" %> : <%= list.title %></li>
<%- end %>
</ul>
<%= link_to "ToDoListへ", root_path %>
- frontend/index.js
import React, { Component } from 'react'
import { render } from 'react-dom'
class App extends Component {
constructor() {
super();
this.state = { todoLists: [], value: '' }
this.fetchState()
}
render() {
return (
<div>
<ul>
{this.listCreator(this.state.todoLists)}
</ul>
<div>
<h3>TODOリストに追加する</h3>
<form onSubmit={(e) => this.handleSubmit(e)}>
<input type='text' value={this.state.value} onChange={(e) => this.handleChange(e)} />
<input type="submit" value="追加" />
</form>
</div>
<div>
<button onClick={(e) => this.ajaxSubmit(e, this.state.todoLists)}>保存</button>
</div>
</div>
)
}
listCreator(todoLists) {
let lists = []
todoLists.forEach((todo, i, todoLists) => {
let idName = `todo_input_${i}`
lists.push(
<li key={i}>
<input key={i} type='checkbox' id={idName} onChange={(e) => this.handleCheck(e, i)} checked={todo['checked']} />
<label key={i + 1} htmlFor={idName}>
{todo['title']}
</label>
</li>
)
})
return lists
}
handleSubmit(e) {
e.preventDefault()
let title = this.state.value
let todoLists = [
...this.state.todoLists,
{
title: title,
checked: false
}
]
this.setState({ todoLists: todoLists, value: '' })
}
handleChange(e) {
this.setState({value: e.target.value});
}
handleCheck(e, index) {
let stateTodoLists = this.state.todoLists
let targetTodo = stateTodoLists[index]
let todoLists = [
...stateTodoLists.slice(0, index),
Object.assign({}, targetTodo, {
checked: !targetTodo.checked
}),
...stateTodoLists.slice(index + 1)
]
this.setState({ todoLists: todoLists })
}
ajaxSubmit(e, todoLists) {
$.ajax({
url: '/todo_lists',
type: 'post',
contentType: 'application/json',
data: JSON.stringify({
todo_lists: todoLists
})
}).done(json => {
location.href = "/todo_lists/list"
})
}
fetchState() {
$.ajax({
url: '/todo_lists/fetch',
type: 'get',
contentType: 'application/json'
}).done(json => {
this.setState({ todoLists: json })
})
}
}
render(
<App />,
document.getElementById('container'))
fetchState() {
$.ajax({
url: '/todo_lists/fetch',
type: 'get',
contentType: 'application/json'
}).done(json => {
this.setState({ todoLists: json })
})
}
この関数をconstructorで呼び出し、TodoListの初期jsonを取得し、todoListsステートに入れている
完成!