React, Fluxxor, RailsでTodo list部品

## 概要
- ReactとFlux実装の一つである[Fluxxor](http://fluxxor.com/)でコンポーネントを作成する。
- サーバーサイドはRailsを使用する。
- ビューテンプレート上にてJSONデータを渡してレンダリングする。(HAMLの:coffeeフィルタを利用)
）
- 必要な時にAjaxでデータ交信するシンプルなコンポーネント。
- ある程度形が整ったので今後のためにメモ。

[![Screenshot.png](https://qiita-image-store.s3.amazonaws.com/0/82804/47bc7061-9f65-ab2a-0cef-763fa225e35e.png)](https://github.com/mnishiguchi/TodoListComponent)
----

## 環境
ruby 2.2.1
Rails 4.2.1
react-rails
fluxxor

#### コントローラ（#index）でJSONデータを準備し、ビューテンプレートに渡す目的でインスタンス変数に格納

```rb:todos_controller.rb
class TodosController < ApplicationController
  #...

  # Initializes the todo app with initial JSON data.
  def index
    @todos = current_user.todos.select(:id, :content, :completed, :created_at).to_json
  end

  #...
```

#### React

```coffeescript
R = React.DOM

@Components.TodoApp = React.createClass
  mixins: [ Fluxxor.FluxMixin(React),
            Fluxxor.StoreWatchMixin("TodoStore") ]

  getInitialState: ->
    newTodoText: ""
    filterMode: "ALL"

  getStateFromFlux: ->
    flux = @getFlux()
    flux.store('TodoStore').getState()

  handleChangeAddTodoText: (e) ->
    @setState(newTodoText: e.target.value)

  handleSubmitForm: (e) ->
    e.preventDefault()
    if @state.newTodoText.trim()
      @getFlux().actions.addTodo(@state.newTodoText)
      @setState(newTodoText: "")

  # Clears one completed item that is first found.
  handleClearCompleted: (e) ->
    e.preventDefault()
    todos = @getStateFromFlux().todos
    for todo in todos when todo.completed
      if confirm("Clear #{todo.content}?")
        @getFlux().actions.deleteTodo(todo)
        break

  handleSelectFilter: (e) ->
    selectedFilter = e.target.name
    @setState filterMode: selectedFilter

  todoFilter: (todo) ->
    switch @state.filterMode
      when "ALL" then true
      when "ACTIVE" then not todo.completed
      when "DONE" then todo.completed

  addForm: ->
    R.form
      className: "form-horizontal"
      id:       "add_form"
      onSubmit: @handleSubmitForm
      R.div
        className: "form-group"
        R.div
          className: "input-group"
          R.input
            className:   "form-control"
            type:        "text"
            placeholder: "New Todo"
            ref:         'input'
            value:       @state.newTodoText
            onChange:    @handleChangeAddTodoText
          R.div
            className: "input-group-btn"
            R.button
              className: "btn btn-primary"
              type:      "submit"
              "Add"

  filterButtons: ->
    R.ul
      className: "nav nav-tabs"
      id: "filter_buttons"
      R.li
        className: if @state.filterMode is "ALL" then "active" else ""
        R.a
          onClick: @handleSelectFilter
          name: "ALL"
          "All"
      R.li
        className: if @state.filterMode is "ACTIVE" then "active" else ""
        R.a
          onClick: @handleSelectFilter
          name: "ACTIVE"
          "Active"
      R.li
        className: if @state.filterMode is "DONE" then "active" else ""
        R.a
          onClick: @handleSelectFilter
          name: "DONE"
          "Done"
      R.li
        className: "pull-right"
        R.a
          @clearButton()

  clearButton: ->
    R.button
      onClick:   @handleClearCompleted
      className: "pull-right"
      "Clear completed"

  createTodoItems: ->
    todos = @state.todos
    R.div
      id: "todo_items_wrapper"
      for todo in todos when @todoFilter(todo)
        React.createElement TodoItem,
          key:  todo.id
          todo: todo

  render: ->
    R.div
      id: "todolist_wrapper"
      @addForm()
      @filterButtons()
      @createTodoItems()
```

```coffeescript
R = React.DOM

@TodoItem = React.createClass
  mixins: [Fluxxor.FluxMixin(React)]

  getInitialState: ->
    value:      @props.todo.content
    completed:  @props.todo.completed
    changed:    false
    updated:    false

  handleToggleCompleted: (e) ->
    e.preventDefault()
    @getFlux().actions.toggleTodo(@props.todo, not @state.completed)
    @setState(completed: not @state.completed)

  handleChange: (e) ->
    input = e.target.value
    newState = if input is @props.todo.content
    then { value: input, changed: false, updated: false }
    else { value: input, changed: true, updated: false }
    @setState newState

  handleUpdate: (e) ->
    e.preventDefault()
    input = React.findDOMNode(@refs.input).value
    @getFlux().actions.updateTodo(@props.todo, input)
    @setState(changed: false, updated: true)

  handleCancelChange: (e) ->
    e.preventDefault()
    originalContent = @props.todo.content
    @setState(value: originalContent, changed: false)

  checkBox: ->
    R.div
      className: "input-group-addon"
      R.i
        className: if @state.completed then "fa fa-check-square-o" else "fa fa-square-o"
        onClick: @handleToggleCompleted

  field: ->
    R.input
      className: "form-control"
      type:      "text"
      style:     { fontSize: "1.5em" }
      ref:       'input'
      value:     @state.value
      onChange:  @handleChange

  fieldColor: ->
    if @state.changed
      'has-warning'
    else if @state.updated
      'has-success'

  updateButton: ->
    R.div
      className: "input-group-addon"
      R.div null,
        R.a
          onClick: @handleUpdate
          "Update"
        R.div
          "\u0020|\u0020"
        R.a
          onClick: @handleCancelChange
          "Cancel"

  render: ->
    R.form
      className: "form-horizontal"
      R.div
        className: "form-group #{@fieldColor()}"
        R.div
          className: "input-group"
          @checkBox()
          @field()
          @updateButton() if @state.changed
```

#### Fluxxor

Storeがデータを受け取れるようにinitializeメソッドをセットアップ

```coffeescript
@Components.TodoStore = Fluxxor.createStore

  initialize: (todos=[]) ->
    @todos = todos
  ...
```

```coffeescript
# ==> Constants

TodoConstants =
  ADD_TODO:    'ADD_TODO'
  TOGGLE_TODO: 'TOGGLE_TODO'
  UPDATE_TODO: 'UPDATE_TODO'
  DELETE_TODO: 'DELETE_TODO'
@Components.TodoConstants = TodoConstants


# ==> Store

@Components.TodoStore = Fluxxor.createStore

  initialize: (todos=[]) ->
    @todos = todos

    @bindActions(TodoConstants.ADD_TODO,    @onAddTodo,
                 TodoConstants.TOGGLE_TODO, @onToggleTodo,
                 TodoConstants.UPDATE_TODO, @onUpdateTodo,
                 TodoConstants.DELETE_TODO, @onDeleteTodo )

  getState: ->
    todos: @todos

  onAddTodo: (payload) ->
    # Update UI
    new_todo = payload.new_todo
    @todos.unshift(new_todo)
    @emit('change')

  onToggleTodo: (payload) ->
    # Update UI
    index = @todos.indexOf(payload.todo)
    @todos[index].completed = payload.completed
    @emit('change')

  onUpdateTodo: (payload) ->
    # Update UI
    index = @todos.indexOf(payload.todo)
    @todos[index].content = payload.new_content
    @emit('change')

  onDeleteTodo: (payload) ->
    # Update UI
    index = @todos.indexOf(payload.todo)
    @todos.splice(index, 1)  # Deletes the todo.
    @emit('change')


# ==> Actions

@Components.TodoActions =

  # Creates a new todo to database.
  # Waits for data because we need a new id generated by database.
  # Dispatches ADD_TODO on successful Ajax.
  addTodo:    (content) ->
    return if not isOnline()
    $.ajax
      method: "POST"
      url:    "/todos/"
      data:   todo:
                content: content
    .done (data, textStatus, XHR) =>
      new_todo =
        id:        data.id
        content:   data.content
        completed: data.completed
      @dispatch(TodoConstants.ADD_TODO, new_todo: new_todo)
      $.growl.notice title: "Todo added", message: data.content
    .fail (XHR, textStatus, errorThrown) =>
      if error_messages = JSON.parse(XHR.responseText)
        for k, v of error_messages
          $.growl.error title: "#{ capitalize(k) } #{v}", message: ""
      else
        $.growl.error title: "Error adding todo", message: "#{errorThrown}"
      console.error("#{textStatus}: #{errorThrown}")

  # Saves a new completion status to database.
  toggleTodo: (todo, completed) ->
    return if not isOnline()
    @dispatch(TodoConstants.TOGGLE_TODO, todo: todo, completed: completed)
    $.ajax
      method: "PATCH"
      url:    "/todos/" + todo.id
      data:   todo:
                completed: completed
    .done (data, textStatus, XHR) =>
      title = if data.completed then "Completed" else "Not completed"
      $.growl.notice title: title, message: data.content
    .fail (XHR, textStatus, errorThrown) =>
      if error_messages = JSON.parse(XHR.responseText)
        for k, v of error_messages
          $.growl.error title: "#{ capitalize(k) } #{v}", message: ""
      else
        $.growl.error title: "Error toggling todo", message: "#{errorThrown}"
      console.error("#{textStatus}: #{errorThrown}")

  # Saves a new content to database.
  updateTodo: (todo, new_content) ->
    return if not isOnline()
    @dispatch(TodoConstants.UPDATE_TODO, todo: todo, new_content: new_content)
    $.ajax
      method: "PATCH"
      url:    "/todos/" + todo.id
      data:   todo:
                content: new_content
    .done (data, textStatus, XHR) =>
      $.growl.notice title: "Todo updated", message: ""
    .fail (XHR, textStatus, errorThrown) =>
      if error_messages = JSON.parse(XHR.responseText)
        for k, v of error_messages
          $.growl.error title: "#{ capitalize(k) } #{v}", message: ""
      else
        $.growl.error title: "Error updating todo", message: "#{errorThrown}"
      console.error("#{textStatus}: #{errorThrown}")

  # Deletes a todo to database.
  deleteTodo: (todo) ->
    return if not isOnline()
    @dispatch(TodoConstants.DELETE_TODO, todo: todo)
    $.ajax
      method: "DELETE"
      url:    "/todos/" + todo.id
    .done (data, textStatus, XHR) =>
      $.growl.notice title: "Deleted", message: data.content
    .fail (XHR, textStatus, errorThrown) =>
      if error_messages = JSON.parse(XHR.responseText)
        for k, v of error_messages
          $.growl.error title: "#{ capitalize(k) } #{v}", message: ""
      else
        $.growl.error title: "Error deleting todo", message: "#{errorThrown}"
      console.error("#{textStatus}: #{errorThrown}")


# ==> Utils

isOnline = ->
  return true if navigator.onLine
  $.growl.error(title: "Offline", message: "")
  false

capitalize = (string) ->
  string.charAt(0).toUpperCase() + string.slice(1)
```

#### ReactコンポーネントとFluxを受け取ったデータを用い初期化するメソッドを準備

```coffeescript
class @Components.initTodoApp
  constructor: (mountNode, options={}) ->

    todoData =  if options.hasOwnProperty("todos") then options["todos"] else []

    # Instantiating the stores.
    stores =
      TodoStore: new Components.TodoStore(todoData)

    # Actions
    actions = Components.TodoActions

    # Instantiating the flux with the stores and actions.
    flux = new Fluxxor.Flux(stores, actions)

    # Logging for the "dispatch" event.
    flux.on 'dispatch', (type, payload) ->
      console.log "[Dispatch]", type, payload if console?.log?

    # Rendering the whole component to the mount node.
    app = React.createElement Components.TodoApp, {flux: flux}
    React.render(app, document.getElementById(mountNode))
```

#### HTMLテンプレート上で初期化メソッドにデータを渡し、呼ぶ

```haml:_todo_component.html.haml
%h1 Todo List

/ MountNode
#todo_component

:coffee
  jQuery ->
    new Components.initTodoApp("todo_component", todos: #{ Todo.getInitialData })
```

# 資料

- http://aspiringwebdev.com/react-js-and-rails-real-world-results/
- http://aspiringwebdev.com/react-js-and-flux-in-rails-a-complete-example/
- https://github.com/mnishiguchi/TodoListComponent