How to build Rails5 API + Redux ToDo Application その2

前回はRails5でToDOリストのREST APIを実装しました。

kogoto.hatenablog.com

今回はクライアント側の実装をしてみます。

イメージ

f:id:ktdk:20160126104936g:plain

言語・ツール

  • ES6 (ES2015)
  • React
  • gulp
  • webpack
.
├── components
│   ├── todo-box.js
│   ├── todo-form.js
│   ├── todo-list.js
│   └── todo.js
└── main.js

コンポーネントの構成

f:id:ktdk:20160126094819p:plain

各パーツをコンポーネント化していきます。

実装してみる

まずTodoから。StateとしてCompleted(完了/未完了)状態を持っています。
完了時にバックカラーが変更され削除時には列ごと消えます。

// components/todo.js

import React, { Component } from 'react'

export default class Todo extends Component {
  constructor(props) {
    super(props)
    this.state = { completed: props.completed }
  }

  rawMarkup() {
    var rawMarkup = marked(this.props.children.toString(), { sanitize : true })
    return { __html: rawMarkup }
  }

  render() {
    return (
      <tr className={this.state.completed ? "success": ""}>
        <td>
          <input
            type="checkbox"
            onChange={(e) => this.props.onTodoCompleted(this, e.target.checked)}
            checked={this.state.completed ? "checked": ""}
          />
        </td>
        <td>{this.props.order}</td>
        <td><span dangerouslySetInnerHTML={this.rawMarkup()} /></td>
        <td>
          <input
            className="btn btn-danger"
            type="button"
            onClick={() => this.props.onTodoDelete(this.props.id)}
            value="削除"
          />
        </td>
      </tr>
    )
  }
}

Todoを管理するTodoListを作成します。

// components/todo-list.js

import React, { Component } from 'react'
import Todo from './todo'

export default class TodoList extends Component {
  constructor(props) {
    super(props)
  }

  render() {
    let todoNodes = this.props.data.map(todo => {
      return (
        <Todo
          url={this.props.url}
          completed={todo.completed}
          order={todo.order}
          id={todo.id}
          key={todo.id}
          onTodoDelete={this.props.onTodoDelete}
          onTodoCompleted={this.props.onTodoCompleted}
        >
          {todo.title}
        </Todo>
      )
    })

    return (
      <div className="row">
        <table className="todoList table table-striped">
          <thead>
            <tr>
              <th width="10%">完了</th>
              <th width="20%">優先順位</th>
              <th>やること</th>
              <th width="20%"></th>
            </tr>
          </thead>
          <tbody>
            {todoNodes}
          </tbody>
        </table>
      </div>
    )
  }
}

次にTodoFormです。優先順位とTodoを入力して登録する普通のフォームです。

// components/todo-form.js

import React, { Component } from 'react'

export default class TodoForm extends Component {
  constructor(props) {
    super(props)
    this.state = { order: 1, title: '' }
  }

  handleOrderChange(e) {
    this.setState({ order: e.target.value })
  }

  handleTitleChange(e) {
    this.setState({ title: e.target.value })
  }

  handleSubmit(e) {
    e.preventDefault()
    let order = this.state.order
    let title = this.state.title.trim()

    if (!order || !title) {
      return
    }

    this.props.onTodoSubmit({ order: order, title: title, completed: false })
    this.setState({ order: 1, title: '' })
  }

  render() {
    return (
      <div className="row">
        <form className="todoForm form-horizontal" onSubmit={this.handleSubmit.bind(this)}>
          <div className="form-group col-md-2">
            <input
              id="todo-order"
              className="form-control"
              type="number"
              value={this.state.order}
              onChange={this.handleOrderChange.bind(this)}
            />
          </div>
          <div className="form-group col-md-8">
            <input
              id="todo-title"
              className="form-control"
              type="text"
              value={this.state.title}
              onChange={this.handleTitleChange.bind(this)}
            />
          </div>
          <div className="col-md-2">
            <input className="btn btn-primary" type="submit" value="登録" />
          </div>
        </form>
      </div>
    )
  }
}

んでこれらを管理するTodoBoxを作成します。
GET, POST, PATCH, DELETE処理はすべてここに実装しています。
これらを下位のコンポーネントに引き渡して状態を更新していく流れです。

import React, { Component } from 'react'
import TodoList from './todo-list'
import TodoForm from  './todo-form'

export default class TodoBox extends Component {
  constructor(props) {
    super(props)
    this.state = { data: [] }
  }

  loadTodosFromServer() {
    $.ajax({
      url: this.props.url,
      dataType: 'json',
      cache: false,
      success: (data) => {
        this.setState({ data: data })
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  handleTodoSubmit(todo) {
    todo.id = Date.now()

    let todos = this.state.data
    let newTodos = todos.concat([todo])
    this.setState({ data: newTodos })

    $.ajax({
      url: this.props.url,
      dataType: 'json',
      type: 'POST',
      data: { todo: todo },
      success: (data) => {
        this.loadTodosFromServer()
      },
      error: (xhr, status, err) => {
        this.setState({ data: todos })
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  handleTodoCompleted(todo, completed) {
    $.ajax({
      url: this.props.url + '/' + todo.props.id,
      dataType: 'json',
      method: 'PATCH',
      data: { todo: { completed: completed } },
      success: (data) => {
        todo.setState({ completed: data.completed })
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  handleTodoDelete(id) {
    $.ajax({
      url: this.props.url + '/' + id,
      dataType: 'json',
      method: 'DELETE',
      success: (data) => {
        this.loadTodosFromServer()
      },
      error: (xhr, status, err) => {
        console.error(this.props.url, status, err.toString())
      }
    })
  }

  componentDidMount() {
    this.loadTodosFromServer()
    // setInterval(this.loadTodosFromServer.bind(this), this.props.pollInterval)
  }

  render() {
    return (
      <div className="todoBox">
        <h1>Todoリスト</h1>
        <TodoForm onTodoSubmit={this.handleTodoSubmit.bind(this)} />
        <TodoList
          data={this.state.data}
          url="/todos"
          onTodoCompleted={this.handleTodoCompleted.bind(this)}
          onTodoDelete={this.handleTodoDelete.bind(this)}
        />
      </div>
    )
  }
}

エントリポイントでこれらのコンポーネントを読み込みます。

import React      from 'react'
import { render } from 'react-dom'
import TodoBox    from './components/todo-box'

let content = document.getElementById('todos')

render(
  <TodoBox url="/todos" pollInterval={2000} />,
  content
)

webpackしたものをHTMLに組み込んで完了です。

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Rail5 + React Todo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
  </head>
  <body>
    <div id="todos" class="container"></div>
    <script type="text/javascript" src="dist/bundle.js"></script>
  </body>
</html>

ReactはあくまでUIに特化したライブラリなので、正直Ajax周りの処理をどこに書いてどうしたらいいかってのが全然わかりませんでした。

これをRedux化するとどうなるんでしょうかね。続きは次回。

入門 React ―コンポーネントベースのWebフロントエンド開発

入門 React ―コンポーネントベースのWebフロントエンド開発