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