#前提
http://todomvc.com/examples/react/#/
が作りたい。react.jsのキャッチアップ用エントリー。
実装の流れがメインでreact.jsの解説はほとんどないので、必要に応じて本家のドキュメント参照して下さい。
ソースコード
開始時点のもの
git clone
、npm install
して開始します
完了(リファクタ後)
リファクタ、データをlocalstrageへ保存、速度改善など(まだあげてない)
##準備
準備編。も参照のこと。主にgulp,package.jsのお話。
前回のエントリから続いてやる場合は、以下の対応を行ってください。
ベースとなるhtml,sassを作成
まずはお手本を参考に、静的なhtmlを返す実装と、sassファイルを作成する。これをベースにコンポーネント化をしていく。(お手本とは少しdom構成等異なります)
長いので、以下参照
src/js/app.js
こんな感じになるはず。
実装
stateをつかってレンダリングを行う。
Reactでは、状態(state)の変更を検知して画面の描画を行う。状態(データ)を定義し、そのデータからDOMツリーを作成するように変更する。
まずはリスト部分からスタート。
- getInitialStateの実装
- renderでstateを利用してliタグの配列を生成して利用
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -2,7 +2,33 @@
var React = require('react/addons');
var TodoApp = React.createClass({
+ getInitialState:function(){
+ return{
+ todos:[
+ {id:'_1',status:0,label:"call to mom."},
+ {id:'_2',status:1,label:"walk the dog"},
+ {id:'_3',status:0,label:"buy groceries for dinner"}
+ ]
+ }
+ },
render: function(){
+
+ var todoArray = this.state.todos.map(function(todo){
+ var completed = todo.status === 1;
+ return (
+ <li className={completed ? 'completed todo-list-item' : "todo-list-item" }>
+ <div className="todo-list-item-view-box">
+ <input className="todo-list-item-check" type="checkbox" checked={completed}></input>
+ <span className="todo-list-item-label">{todo.label}</span>
+ <button className="todo-list-item-remove" type="button"></button>
+ </div>
+ <div className="todo-list-item-edit-box">
+ <input type="text"></input>
+ </div>
+ </li>
+ );
+ });
+
return (
<div className="container">
<header>
@@ -14,36 +40,7 @@
<input className="todo-input" type="text" placeholder="What needs to be done?"></input>
</div>
<ul className="todo-list">
- <li className="todo-list-item">
- <div className="todo-list-item-view-box">
- <input className="todo-list-item-check" type="checkbox"></input>
- <span className="todo-list-item-label">aiueo</span>
- <button className="todo-list-item-remove" type="button"></button>
- </div>
- <div className="todo-list-item-edit-box">
- <input type="text"></input>
- </div>
- </li>
- <li className="todo-list-item completed">
- <div className="todo-list-item-view-box">
- <input className="todo-list-item-check" type="checkbox"></input>
- <span className="todo-list-item-label">kakiku</span>
- <button className="todo-list-item-remove" type="button"></button>
- </div>
- <div className="todo-list-item-edit-box">
- <input type="text"></input>
- </div>
- </li>
- <li className="todo-list-item editing">
- <div className="todo-list-item-view-box">
- <input className="todo-list-item-check" type="checkbox"></input>
- <span className="todo-list-item-label">kakiku</span>
- <button className="todo-list-item-remove" type="button"></button>
- </div>
- <div className="todo-list-item-edit-box">
- <input type="text"></input>
- </div>
- </li>
+ {todoArray}
</ul>
<footer>
<span>1 item left</span>
コンポーネントの作成
次に、リストを構成する項目を別コンポーネント(TodoItem)に切り出す。
TodoItemは、現時点ではrenderメソッドのみ。
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -1,32 +1,39 @@
(function(){
var React = require('react/addons');
+ var TodoItem = React.createClass({
+ render: function(){
+ var todo = this.props.todo;
+ var completed = todo.status === 1;
+ return (
+ <li className={completed ? 'completed todo-list-item' : "todo-list-item" }>
+ <div className="todo-list-item-view-box">
+ <input className="todo-list-item-check" type="checkbox" checked={completed}></input>
+ <span className="todo-list-item-label">{todo.label}</span>
+ <button className="todo-list-item-remove" type="button"></button>
+ </div>
+ <div className="todo-list-item-edit-box">
+ <input type="text"></input>
+ </div>
+ </li>
+ );
+ }
+ });
+
var TodoApp = React.createClass({
getInitialState:function(){
return{
todos:[
{id:'_1',status:0, label:"call to mom."},
{id:'_2',status:1, label:"walk the dog"},
{id:'_3',status:0, label:"buy groceries for dinner"}
]
}
},
render: function(){
var todoArray = this.state.todos.map(function(todo){
- var completed = todo.status === 1;
- return (
- <li className={completed ? 'completed todo-list-item' : "todo-list-item" }>
- <div className="todo-list-item-view-box">
- <input className="todo-list-item-check" type="checkbox" checked={completed}></input>
- <span className="todo-list-item-label">{todo.label}</span>
- <button className="todo-list-item-remove" type="button"></button>
- </div>
- <div className="todo-list-item-edit-box">
- <input type="text"></input>
- </div>
- </li>
- );
+ return <TodoItem key={todo.id} todo={todo}></TodoItem>
});
return (
イベント処理の実装
次にチェックボックスにチェックを入れた際の挙動を実装する。
チェックボックスにチェックを入れた時の挙動は、
- 対象todoのstatusを変更する。
- todoの完了がわかるように、打ち消し線を引く
ポイントは、状態(state)は親コンポーネントが持つが、イベントは子のコンポーネントから発火するため、発火したイベントを親に伝える必要がある事。
また、通常javascriptで実装する、イベントを取得してビューを書き換えるという考え方ではなく、
状態に即したビューの描画の定義(renderメソッド)を行い、イベント発火時に状態を上書きする。この二つが分離している事に気をつける。
すでにrenderメソッドは状態(state)に応じたビューを描画するようになっているので、stateを上書きするイベントの実装を行う
- 親コンポーネントに状態(state)を変更するメソッドを追加し、子コンポーネントのプロパティとして渡す
- onChangeイベントで、プロパティとして渡された関数を呼び、状態を変更する
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -2,13 +2,16 @@
var React = require('react/addons');
var TodoItem = React.createClass({
+ handleChange:function(e){
+ this.props.completeItem(this.props.todo.id, e.target.checked);
+ },
render: function(){
var todo = this.props.todo;
var completed = todo.status === 1;
return (
<li className={completed ? 'completed todo-list-item' : "todo-list-item" }>
<div className="todo-list-item-view-box">
- <input className="todo-list-item-check" type="checkbox" checked={completed}></input>
+ <input className="todo-list-item-check" type="checkbox" checked={completed} onChange={this.handleChange}></input>
<span className="todo-list-item-label">{todo.label}</span>
<button className="todo-list-item-remove" type="button"></button>
</div>
@@ -30,11 +34,22 @@
]
}
},
+ completeItem:function(id,completed){
+ var newTodos = this.state.todos.map(function(todo, index){
+ if(todo.id === id){
+ return React.addons.update(todo,{status: {$set : (completed ? 1 : 0) }});
+ }else{
+ return todo;
+ }
+ });
+ this.setState({todos:newTodos});
+ },
render: function(){
var todoArray = this.state.todos.map(function(todo){
- return <TodoItem key={todo.id} todo={todo}></TodoItem>
- });
+ return <TodoItem key={todo.id} todo={todo} completeItem={this.completeItem}></TodoItem>
+ }.bind(this));
+
return (
<div className="container">
新たなtodoの追加を行う
次に、新しいtodoが追加された場合の挙動を実装する。
Reactでは、仮想DOMと実際のDOMにずれがないように実装する必要がある。
しかし、新規todoの入力欄に文字を入力した場合、今の実装では仮想DOMと実際のDOMがずれてしまっている(inputのvalueが異なる。)
そのため、ここで行う実装としては、
- 入力を行った際の仮想DOMと実際のDOMがずれないよう、入力値をstateとして持ち、そのstateを利用するようrenderメソッドを修正する
- inputのonChangeイベントで、入力値のstateを変更する。
- inputのonKeyPressイベントで、enter押下時に、stateに新しいtodoを追加する。
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -31,7 +31,8 @@
{id:'_1',status:0, label:"call to mom."},
{id:'_2',status:1, label:"walk the dog"},
{id:'_3',status:0, label:"buy groceries for dinner"}
- ]
+ ],
+ newTodoLabel : ''
}
},
completeItem:function(id,completed){
@@ -44,6 +45,29 @@
});
this.setState({todos:newTodos});
},
+ generateId : function(){
+ var num = 4;
+ return function(){
+ return '_' + num++;
+ }
+ }(),
+ handleNewTodoKeyPress: function(e){
+ if(e.charCode == 13){
+ var newTodoLabel = this.state.newTodoLabel.trim();
+ if(newTodoLabel.length > 0){
+ var newTodo = {id : this.generateId(),status:0,label:newTodoLabel};
+ this.setState(
+ {
+ todos:React.addons.update(this.state.todos,{$push:[newTodo]}),
+ newTodoLabel: ''
+ }
+ );
+ }
+ }
+ },
+ handleNewTodoChange: function(e){
+ this.setState({newTodoLabel:e.target.value});
+ },
render: function(){
var todoArray = this.state.todos.map(function(todo){
@@ -59,7 +83,7 @@
<section className="main-area">
<div className="todo-input-area">
<input className="check-all-todos" type="checkbox" ></input>
- <input className="todo-input" type="text" placeholder="What needs to be done?"></input>
+ <input className="todo-input" value={this.state.newTodoLabel} type="text" placeholder="What needs to be done?" onChange={this.handleNewTodoChange} onKeyPress={this.handleNewTodoKeyPress}></input>
</div>
<ul className="todo-list">
{todoArray}
全チェックの実装
同様に、このチェックボックスの仮想DOMにも問題があるため、修正する。
- チェックの有無をstateで保持し、それを利用するようrenderを修正
- チェックボックスのonChangeイベントで、チェックの有無と、各todoのstatusを変更を行う
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -32,7 +32,8 @@
{id:'_2',status:1, label:"walk the dog"},
{id:'_3',status:0, label:"buy groceries for dinner"}
],
- newTodoLabel : ''
+ newTodoLabel : '',
+ allChecked : false
}
},
completeItem:function(id,completed){
@@ -68,6 +69,18 @@
handleNewTodoChange: function(e){
this.setState({newTodoLabel:e.target.value});
},
+ handleAllCheckChange : function(e){
+ this.setState(
+ {
+ todos:
+ this.state.todos.map(function(todo){
+ return React.addons.update(todo,{status:{$set: (e.target.checked ? 1 : 0)}});
+ }),
+ allChecked: e.target.checked
+ }
+
+ );
+ },
render: function(){
var todoArray = this.state.todos.map(function(todo){
@@ -82,7 +95,7 @@
</header>
<section className="main-area">
<div className="todo-input-area">
- <input className="check-all-todos" type="checkbox" ></input>
+ <input className="check-all-todos" type="checkbox" checked={this.state.allChecked} onChange={this.handleAllCheckChange}></input>
<input className="todo-input" value={this.state.newTodoLabel} type="text" placeholder="What needs to be done?" onChange={this.handleNewTodoChange} onKeyPress={this.handleNewTodoKeyPress}></input>
</div>
<ul className="todo-list">
残todo数表示の実装
stateの状態にあわせて件数が表示されるよう修正。
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -86,7 +86,9 @@
var todoArray = this.state.todos.map(function(todo){
return <TodoItem key={todo.id} todo={todo} completeItem={this.completeItem}></TodoItem>
}.bind(this));
-
+ var activeTodoCount = this.state.todos.filter(function(todo){
+ return todo.status === 0;
+ }).length;
return (
<div className="container">
@@ -102,7 +104,7 @@
{todoArray}
</ul>
<footer>
- <span>1 item left</span>
+ <span>{activeTodoCount} items left</span>
<ul className="filter-list">
<li className="filter-list-item">
<a href="#all" className="selected">All</a>
削除ボタンを実装
削除ボタンのonClickイベントから、親コンポーネントに処理を移譲して、該当のtodoを除いた一覧をstateにセットする。
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -5,6 +5,9 @@
handleChange:function(e){
this.props.completeItem(this.props.todo.id, e.target.checked);
},
+ handleRemoveClick:function(e){
+ this.props.removeItem(this.props.todo.id);
+ },
render: function(){
var todo = this.props.todo;
var completed = todo.status === 1;
@@ -13,7 +16,7 @@
<div className="todo-list-item-view-box">
<input className="todo-list-item-check" type="checkbox" checked={completed} onChange={this.handleChange}></input>
<span className="todo-list-item-label">{todo.label}</span>
- <button className="todo-list-item-remove" type="button"></button>
+ <button className="todo-list-item-remove" type="button" onClick={this.handleRemoveClick}></button>
</div>
<div className="todo-list-item-edit-box">
<input type="text"></input>
@@ -81,10 +84,17 @@
);
},
+ removeItem:function(id){
+ this.setState({
+ todos: this.state.todos.filter(function(todo){
+ return todo.id !== id;
+ })
+ });
+ },
render: function(){
var todoArray = this.state.todos.map(function(todo){
- return <TodoItem key={todo.id} todo={todo} completeItem={this.completeItem}></TodoItem>
+ return <TodoItem key={todo.id} todo={todo} completeItem={this.completeItem} removeItem={this.removeItem}></TodoItem>
}.bind(this));
var activeTodoCount = this.state.todos.filter(function(todo){
return todo.status === 0;
完了todo の削除を実装
ボタンのonClickイベントから、完了済みを除いたtodoの一覧をセットする
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -91,6 +91,13 @@
})
});
},
+ handleClearCompletedClick:function(e){
+ this.setState({
+ todos: this.state.todos.filter(function(todo){
+ return todo.status === 0;
+ })
+ });
+ },
render: function(){
var todoArray = this.state.todos.map(function(todo){
@@ -126,7 +133,7 @@
<a href="#completed" >Completed</a>
</li>
</ul>
- <button type="button" className="clear-completed">clear completed</button>
+ <button type="button" className="clear-completed" onClick={this.handleClearCompletedClick}>clear completed</button>
</footer>
</section>
</div>
編集の実装
- 編集中を表す状態を追加し、その状態に応じて表示が変わるように修正
- 編集中文字列を状態に追加し、編集用inputタグに表示されるように修正
- onDoubleClickイベントにて、編集中のstateを変更
- onChangeイベントにて、編集中文字列のstateを変更
- onKeyDownイベントにて、enterキー押下で文字列と編集中のstateを変更(文字列が空の場合は削除)
- onBlurイベントも同様(複数のケースで呼ばれるので注意)
- onKeyPressイベントにて、escキー押下で編集中のstateを変更
- ダブルクリックされるとfocusをあてる
まずはstateとrenderの変更
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -2,6 +2,11 @@
var React = require('react/addons');
var TodoItem = React.createClass({
+ getInitialState:function(){
+ return {
+ editingText: ''
+ }
+ },
handleChange:function(e){
this.props.completeItem(this.props.todo.id, e.target.checked);
},
@@ -12,14 +17,17 @@
var todo = this.props.todo;
var completed = todo.status === 1;
return (
- <li className={completed ? 'completed todo-list-item' : "todo-list-item" }>
+ <li className={todo.editing? "editing todo-list-item": completed ? 'completed todo-list-item' : "todo-list-item" }>
<div className="todo-list-item-view-box">
<input className="todo-list-item-check" type="checkbox" checked={completed} onChange={this.handleChange}></input>
<span className="todo-list-item-label">{todo.label}</span>
<button className="todo-list-item-remove" type="button" onClick={this.handleRemoveClick}></button>
</div>
<div className="todo-list-item-edit-box">
- <input type="text"></input>
+ <input
+ type="text"
+ value={this.state.editingText}
+ ></input>
</div>
</li>
);
@@ -31,9 +39,9 @@
getInitialState:function(){
return{
todos:[
- {id:'_1',status:0, label:"call to mom."},
- {id:'_2',status:1, label:"walk the dog"},
- {id:'_3',status:0, label:"buy groceries for dinner"}
+ {id:'_1',status:0, label:"call to mom.", editing:false},
+ {id:'_2',status:1, label:"walk the dog", editing:false},
+ {id:'_3',status:0, label:"buy groceries for dinner", editing:false}
],
newTodoLabel : '',
allChecked : false
状態を変更する各イベントの実装
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -13,12 +13,43 @@
handleRemoveClick:function(e){
this.props.removeItem(this.props.todo.id);
},
+ handleDoubleClick:function(e){
+ this.setState({
+ editingText : this.props.todo.label
+ })
+ this.props.startEditItem(this.props.todo.id);
+ },
+ handleChangeEdit:function(e){
+ this.setState({
+ editingText:e.target.value
+ })
+ },
+ handleKeyDownEdit:function(e){
+ if(e.which === 13){
+ this.completeEditItem();
+ }else if(e.which === 27){
+ this.props.cancelEditItem(this.props.todo.id);
+ }
+ },
+ handleBlurEdit:function(e){
+ if(this.props.todo.editing){
+ this.completeEditItem();
+ }
+ },
+ completeEditItem:function(){
+ var val = this.state.editingText.trim();
+ if(val){
+ this.props.completeEditItem(this.props.todo.id, val);
+ }else{
+ this.props.removeItem(this.props.todo.id);
+ }
+ },
render: function(){
var todo = this.props.todo;
var completed = todo.status === 1;
return (
<li className={todo.editing? "editing todo-list-item": completed ? 'completed todo-list-item' : "todo-list-item" }>
- <div className="todo-list-item-view-box">
+ <div className="todo-list-item-view-box" onDoubleClick={this.handleDoubleClick}>
<input className="todo-list-item-check" type="checkbox" checked={completed} onChange={this.handleChange}></input>
<span className="todo-list-item-label">{todo.label}</span>
<button className="todo-list-item-remove" type="button" onClick={this.handleRemoveClick}></button>
@@ -27,6 +58,9 @@
<input
type="text"
value={this.state.editingText}
+ onChange={this.handleChangeEdit}
+ onKeyDown={this.handleKeyDownEdit}
+ onBlur={this.handleBlurEdit}
></input>
</div>
</li>
@@ -106,10 +140,52 @@
})
});
},
+ startEditItem:function(id){
+ this.setState({
+ todos: this.state.todos.map(function(todo){
+ return React.addons.update(todo, {editing:{$set:todo.id === id}});
+ })
+ });
+ },
+ completeEditItem:function(id,newValue){
+ if(id){
+ this.setState({
+ todos: this.state.todos.map(function(todo){
+ if(id === todo.id && todo.editing){
+ return React.addons.update(todo, {editing:{$set:false},label:{$set:newValue}});
+ }else{
+ return todo;
+ }
+ })
+ });
+ }
+ },
+ cancelEditItem:function(id){
+ this.setState({
+ todos: this.state.todos.map(function(todo){
+ if(todo.id === id){
+ return React.addons.update(todo, {editing:{$set:false}});
+ }else{
+ return todo;
+ }
+ })
+ });
+ },
render: function(){
var todoArray = this.state.todos.map(function(todo){
- return <TodoItem key={todo.id} todo={todo} completeItem={this.completeItem} removeItem={this.removeItem}></TodoItem>
+ return (
+ <TodoItem
+ key={todo.id}
+ todo={todo}
+ completeItem={this.completeItem}
+ removeItem={this.removeItem}
+ startEditItem={this.startEditItem}
+ completeEditItem={this.completeEditItem}
+ cancelEditItem={this.cancelEditItem}
+ >
+ </TodoItem>
+ );
}.bind(this));
var activeTodoCount = this.state.todos.filter(function(todo){
return todo.status === 0;
最後に、ダブルクリク時にフォーカスを当てる処理
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -44,6 +44,11 @@
this.props.removeItem(this.props.todo.id);
}
},
+ componentDidUpdate:function(prevProps){
+ if(!prevProps.todo.editng && this.props.todo.editing){
+ React.findDOMNode(this.refs.editItem).focus();
+ }
+ },
render: function(){
var todo = this.props.todo;
var completed = todo.status === 1;
@@ -56,6 +61,7 @@
</div>
<div className="todo-list-item-edit-box">
<input
+ ref="editItem"
type="text"
value={this.state.editingText}
onChange={this.handleChangeEdit}
todoのフィルタリング
未完了、完了、全てでフィルタリングする。onClickイベントで実装することもできるが、それぞれ固有のurlを持たせるため、aタグのリンクにする。
directorを利用してルーティングを行う。
npm install director --save
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -1,5 +1,6 @@
(function(){
var React = require('react/addons');
+ var Router = require('director').Router;
var TodoItem = React.createClass({
getInitialState:function(){
@@ -84,9 +85,18 @@
{id:'_3',status:0, label:"buy groceries for dinner", editing:false}
],
newTodoLabel : '',
- allChecked : false
+ allChecked : false,
+ filter:'all'
}
},
+ componentDidMount:function(){
+ Router({
+ '/':this.setState.bind(this,{filter:'all'}),
+ '/active':this.setState.bind(this,({filter:'active'})),
+ '/completed': this.setState.bind(this,({filter:'completed'}))
+ }).init('/');
+
+ },
completeItem:function(id,completed){
var newTodos = this.state.todos.map(function(todo, index){
if(todo.id === id){
@@ -179,7 +189,20 @@
},
render: function(){
- var todoArray = this.state.todos.map(function(todo){
+ var todoArray = this.state.todos.filter(function(todo){
+
+ switch (this.state.filter){
+ case 'all':
+ return true;
+ case 'active':
+ return todo.status === 0;
+ case 'completed':
+ return todo.status === 1;
+ default:
+ return true;
+ }
+ }.bind(this))
+ .map(function(todo){
return (
<TodoItem
key={todo.id}
@@ -214,13 +237,13 @@
<span>{activeTodoCount} items left</span>
<ul className="filter-list">
<li className="filter-list-item">
- <a href="#all" className="selected">All</a>
+ <a href="#/" className={this.state.filter === 'all' && 'selected'}>All</a>
</li>
<li className="filter-list-item">
- <a href="#active" >Active</a>
+ <a href="#/active" className={this.state.filter === 'active' && 'selected'}>Active</a>
</li>
<li className="filter-list-item">
- <a href="#completed" >Completed</a>
+ <a href="#/completed" className={this.state.filter === 'completed' && 'selected'}>Completed</a>
</li>
</ul>
<button type="button" className="clear-completed" onClick={this.handleClearCompletedClick}>clear completed</button>
#参考
react.addons.update
https://facebook.github.io/react/docs/update.html