9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

BitStarAdvent Calendar 2017

Day 9

怖くない!React&Railsで作るTODOアプリ 実装編

Last updated at Posted at 2017-12-08

みなさんはじめまして。
株式会社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' %>

スクリーンショット 2017-12-02 12.41.06.png

リストを追加する

さて次はリストを追加する機能を実装して行きましょう

  • 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'))

react機能GIF.gif

こんな風に追加ができるようになる

この部分で外部から渡されたpropsをstateに変換しています
なぜこんなことをするのかというとReactには以下の概念があります
propsはimmutableであり
stateはmutableであるというような概念があります
以下の記事などで詳しく説明してあります
React における State と Props の違い - Qiita

constructor(props) {
  super(props);
  this.state = { todoLists: props.todoLists, value: '' }
}

追加機能のjsxの部分でonSubmitonChangeが設定されています

<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ステートに入れている

todoアプリ機能紹介.gif

完成!

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?