React関連の使い方を復習する/(1) create-react-app
React関連の使い方を復習する/(2) prop
前回、reactのprops(+react-bootstrap)を使ってカンバンの画面構成を作成しました。
今回は、stateとイベントハンドラを使って画面を少し動的にしてみましょう。
この記事で触れること
- react-bootstrap
- react(state、イベントハンドラ)
タスクの完了状態を管理して画面に反映する
せっかくTasksコンポーネントの1タスクごとにチェックボックスを置いたので、on/offの切り替えをできるようにしたいですよね。ついでに、チェック済みになったタスクは取り消し線を、全てのタスクがチェック済みになったCardコンポーネントの名前も取り消し線をつけるとわかりやすいですね。
これを実現するために、全てpropsで管理していたデータの一部をstateで管理するように修正していきます。
Tasksコンポーネント
前回作ったTasksコンポーネントは、親のCardコンポーネントから渡されたpropsをそのまま描画に使っていましたが、このままでは画面の変更に対応することができません。
propsは、親コンポーネントから渡される、いわゆる「初期情報」で、stateは自コンポーネントを管理する「状態」です。stateが更新されると、自動的にReactコンポーネントのrender関数が呼び出され、画面が再描画されます。
今回は、 Tasksコンポーネントのインスタンスが作られるときにpropsで渡されたタスク情報を、stateで管理するように変更してみます。
合わせて、チェックボックスをクリックしたときにイベントをハンドリングして、チェックボックスの状態を変更するように処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import React from 'react'; import { Checkbox } from 'react-bootstrap'; export default class Tasks extends React.Component { constructor(props) { super(props); // stateの初期値を設定 // createClassの場合はgetInitialState this.state = { tasks: props.tasks }; } // orderByの昇順にソート compare(a, b) { return a.orderBy > b.orderBy ? 1 : -1; } // チェックボックスの状態を反転する toggleTask(toggleTask) { let tasks = this.state.tasks.map(task => { if (task.id === toggleTask.id) { task.isDone = !task.isDone; } return task; }); this.setState({ tasks }); this.props.completeCheck(tasks); } render() { const tasks = this.state.tasks.sort(this.compare).map(task => ( <Checkbox key={task.id} onClick={this.toggleTask.bind(this, task)} checked={task.isDone} readOnly> { task.isDone ? <del>{task.label}</del> : task.label} </Checkbox> )); return ( <div className="tasks"> {tasks} </div> ); } } |
L8-L15、初期stateを設定する場合はコンストラクタを利用します。es6のclass構文を使わない場合のsetInitialState関数ですね。propsで渡されたtasksをまるっとstateで管理します。合わせて、L38、描画するtasksをpropsからstateに変更していますね。
L39、チェックボックスをクリックしたときのイベントをonClickでハンドリングしています。toggleTask関数を呼び出すわけですが、何もしないと呼び出し先の関数でthisを使うことができないため、bind関数でthisを紐づけます。
ハンドリングするイベントの種類は、ReactのSyntheticEventの項を参照。
L40、isDoneの状態に応じて取り消し線を付与するように修正しています。<del />で取り消し線を付与できるんですね、知らなかった。
L23-L34、toggleTask関数では、クリックされたタスクのisDoneを反転してstateを更新しています。
さらに、親コンポーネントのcompleteCheckを呼び出していますが、これはこの後Cardコンポーネントで定義する関数です。
ピュアなReactを使用している場合は、コンポーネント間の関数呼び出しや状態の受け渡しなど、1つずつ渡していく必要があります。これくらいの単純なものであれば良いですが、コンポーネント間の連携が多くなってくるとなかなか見通しが悪くなりそうに思いますね。
あと、ついでにcomare関数をif-elseから三項演算子に変更してスッキリさせています。
Cardコンポーネント
Tasksコンポーネントだけだと芸がないので、子コンポーネントが全てチェックされたらカードも取り消し線を付与するようにCardコンポーネントを改造します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
import React from 'react'; import { Panel, Accordion } from 'react-bootstrap'; import Tasks from './Tasks'; export default class Card extends React.Component { constructor(props) { super(props); this.state = { // tasksの各要素のisDoneが1つでもtrueであればtrue isDone: props.tasks.map(task => task.isDone).every(item => item) } } completeCheck(tasks) { this.setState({ // tasksの各要素のisDoneが1つでもtrueであればtrue isDone: tasks.map(task => task.isDone).every(item => item) }) } // カードが所属しているレーンに応じて色を付与 cardColor() { let color; switch (this.props.laneId) { case 1: color = 'info'; break; case 2: color = 'warning'; break; case 3: color = 'success'; break; default: color = '' } return color; } render() { return ( <Accordion> <Panel bsStyle={this.cardColor()} header={ // 全てのタスクが完了していれば、カードも取り消し線を引く this.state.isDone ? <del>{this.props.label}</del> : <span>{this.props.label}</span> }> {<strong>{this.props.description}</strong>} <Tasks cardId={this.props.id} tasks={this.props.tasks} completeCheck={this.completeCheck.bind(this)}/> </Panel> </Accordion> ); } } |
L10-L16、コンストラクタで初期stateを設定するのは先ほどと同じです。
カードがもつタスクが全てチェックされているかどうかを確認してstateを設定しています。
L18-L23、先ほどのTasksコンポーネントから呼び出される関数です。コンストラクタと同様、全てのタスクがチェック済みかを確認してstateを更新しています。
L49-L54、Tasksコンポーネントと同様、Cardコンポーネントのstateによって取り消し線を付与するように修正しています。
画面が味気ないので、Laneのステータスに応じて色を付与するようにBootstrapのbsStyleを設定しています。デフォルトのものをそのまま使用しているので、warningとかsuccessとかですけど今回は本質ではないので気にしないことにします。また、カードが増えてくると縦長になってくるので、BootstrapのAccordionコンポーネントを使って開閉できるように修正しています。
Lane
Laneコンポーネントは大きな修正はないですが、CardコンポーネントでLaneのステータスに応じて色を付与するためにlaneIdをpropsに追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
import React from 'react'; import { Col } from 'react-bootstrap'; import Card from './Card'; export default class Lane extends React.Component { // orderByの昇順にソート compare(a, b) { return a.orderBy > b.orderBy ? 1 : -1; } render() { const cards = this.props.cards.sort(this.compare).map(card => { return ( <Card key={card.id} id={card.id} laneId={this.props.id} label={card.label} description={card.description} tasks={card.tasks} orderBy={card.orderBy} /> ); }); return ( <Col className={"lane"} md={4} xs={4}> <h2>{this.props.label}</h2> {cards} </Col> ); } } |
画面の確認
今回修正した画面を確認してみます。
少しだけ画面に動きが出てきましたね。
現時点でのソースコードは、Githubに置いておきます。
次回は何をしましょうかね、props-typeとかですかね。
reduxにいく前にもう少しReactの機能を使いたい。ちょっと考えます。