LoginSignup
5
5

React, Fluxxor, RailsでTodo list部品

Last updated at Posted at 2015-06-25

概要

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

Screenshot.png

環境

ruby 2.2.1
Rails 4.2.1
react-rails
fluxxor

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

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

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()
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メソッドをセットアップ

@Components.TodoStore = Fluxxor.createStore

  initialize: (todos=[]) ->
    @todos = todos
  ...
# ==> 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を受け取ったデータを用い初期化するメソッドを準備

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テンプレート上で初期化メソッドにデータを渡し、呼ぶ

_todo_component.html.haml
%h1 Todo List

/ MountNode
#todo_component

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

資料

5
5
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
5
5