目次
前回のあらすじ
前回、TODOリストにTODOを追加できるようになりました。
今回は 前回のソースコード
を元に、編集ボタンで登録済みのTODOのタイトルや本文を変更できるようにします。
編集ボタンの挙動を考える
コンポーネントの構造を考えると、TodoItem
コンポーネントの編集ボタンのクリックリスナからTodoEdit
コンポーネントのStateを変更するのはムリでしょう。
Todo
┣ TodoEdit ←ココニ アクセス ハ デキナイ
┗ TodoList
┣ TodoItem ←ココカラ
┣ TodoItem
┣ TodoItem
┣ TodoItem
┗ TodoItem
なので、TodoItem
コンポーネントからTodo
コンポーネントを経由してTodoEdit
コンポーネントにアクセスすることを考えました。
TodoItem
コンポーネントの編集ボタンを押すTodo
コンポーネントのクリックリスナを呼ぶTodoEdit
コンポーネントのStateの値を変更する
が、
1.と2.は問題ないのですが、3.がうまくいきませんでした。
外部からStateを変更できない
Todo
コンポーネントにonClick_Edit()
メソッドを用意して、Propsを通じてTodoItem
コンポーネントから呼ぶ。ここまではOKです。
問題はonClick_Edit()
メソッドからTodoEdit
コンポーネントのStateの値を変更する手段がないことです。
まずそもそも、Stateはコンポーネント内部でのみ使用するものですから、当然外部からの直接操作はできません。それではTodoEdit
コンポーネントのPropsを介してTodo
コンポーネントから値を渡し、TodoEdit
コンポーネント自身にStateを変更させれば、と思ったのですが、これもうまくいきません。
ではどうするのか?
Todo
コンポーネントがtitle
とcontent
を操作できるようにするため、title
とcontent
をTodoEdit
コンポーネントからTodo
コンポーネントに引き上げます。
編集ボタンのクリックリスナを作る
まずTodoItem
コンポーネントのPropsにクリックリスナの定義を追加して、編集ボタンが押されたときにこれを呼ぶようにします。
TODOデータはTodo
コンポーネントが持っているので、引数にIDさえ渡せばどのTODOの編集ボタンが押されたのかはわかります。
TodoItem.tsx
1 2 3 4 5 6 7 8 9 10 |
// Propsインタフェース interface TodoItemPropsInterface { todoData: TodoData; onClick_Edit(id:number):void; } ... <td><button onClick={() => this.props.onClick_Edit(this.props.todoData.id)}>編集</button></td> <td><button>削除</button></td> |
ひとつ上のTodoList
コンポーネントでも同様の対応が必要です。
TodoList.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Propsインタフェース interface TodoListPropsInterface { todoData:TodoData[]; onClick_Edit(id:number):void; } … // 描画 render() { const items = this.props.todoData.map((d) => { return <TodoItem todoData={d} onClick_Edit={(i) => this.props.onClick_Edit(i)} />; }); |
最後にTodo
コンポーネントにonClick_Edit()
メソッドを用意してTodoList
コンポーネントに渡します。これでクリックリスナの用意は完了です。編集ボタンを押すとTODOのタイトルがアラート表示されるようになっているはずです。
Todo.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// クリック:編集ボタン private onClick_Edit(id:number) { const d = this.state.todoData.find((d:TodoData) => { return (d.id === id); }); if (d) { alert(d.title); } else { alert('Error!'); } } // 描画 public render() { return ( <div> <TodoEdit onClick_Submit={(t, c) => this.onClick_Submit(t, c)} /> <TodoList todoData={this.state.todoData} onClick_Edit={(i) => this.onClick_Edit(i)} /> </div> ); } |
TodoEdit
コンポーネントからTodo
コンポーネントへStateを移動する
前述のとおり、TodoEdit
コンポーネントのStateにあるtitle
とcontent
をTodo
コンポーネントのStateに移動します。
Todo.tsx
まず、Todo
コンポーネントのStateにtitle
とcontent
を追加します。
編集中のTODOデータのIDも管理したいので、これも追加します。
追加したメンバ変数はコンストラクタで初期化します。
Todo.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Stateインタフェース interface TodoStateInterface { todoData: TodoData[]; idCount: number; id: number; title: string; content: string; } class Todo extends React.Component<TodoPropsInterface, TodoStateInterface> { public constructor(props: TodoPropsInterface) { super(props); this.state = { todoData: [], idCount: 0, id: -1, title: '', content: '', }; } |
また、フォームが変更されたときに呼ぶメソッドonChange_Edit()
メソッドを用意します。引数としてタイトルと本文を受け取ってsetState()
メソッドでStateに反映するメソッドです。
1 2 3 4 5 6 7 |
// 入力 private onChange_Edit(title:string, content:string) { this.setState({ title: title, content: content, }) } |
title
とcontent
はTodo
コンポーネントで管理するようになるので、onClick_Submit()
メソッドの引数としては不要になりました。onClick_Submit()
メソッドの中身については後述するので、いまは空にしておきます。
1 2 3 |
// 登録 private onClick_Submit() { } |
setState()
メソッドを呼ぶとコンポーネントの再描画が行われるので、最新のtitle
とcontent
をTodoEdit
コンポーネントに反映するためにTodoEdit
コンポーネントのPropsへこれらを渡します。
onChange_Edit()
メソッドもTodoEdit
コンポーネントから呼んでもらわなければならないので、これもPropsへ渡します。
1 2 3 4 5 6 7 8 9 |
// 描画 public render() { return ( <div> <TodoEdit onClick_Submit={() => this.onClick_Submit()} onChange_Edit={(t, c) => this.onChange_Edit(t, c)} title={this.state.title} content={this.state.content}/> <TodoList todoData={this.state.todoData} onClick_Edit={(i) => this.onClick_Edit(i)} /> </div> ); } |
Todo.tsx
への修正は以上です。
TodoEdit.tsx
次に TodoEdit.tsx
を修正します。
まず Todo.tsx
の修正で TodoEdit
コンポーネントのPropsにメソッドonChange_Edit()
と変数title
とcontent
が追加されました。またonClick_Submit()
メソッドの引数がなくなりました。それらをPropsに反映します。
Stateのメンバは不要になったので削除します。
TodoEdit.tsx
1 2 3 4 5 6 7 8 9 10 11 |
// Propsインタフェース interface TodoEditPropsInterface { onClick_Submit():void; onChange_Edit(title:string, content:string):void; title: string; content: string; } // Stateインタフェース interface TodoEditStateInterface { } |
フォームが変更されたときに、Todo
コンポーネントのonChange_Edit()
メソッドを呼ぶように修正します。変更された値はevent
から、そうでない値はPropsから取得します。
1 2 3 4 5 6 7 8 9 |
// フォーム変更:タイトル private onChange_Title(event:any) { this.props.onChange_Edit(event.target.value, this.props.content); } // フォーム変更:本文 private onChange_Content(event:any) { this.props.onChange_Edit(this.props.title, event.target.value); } |
タイトルと本文のvalue
はStateではなくPropsの値を参照します。
メソッドonClick_Submit
の引数がなくなったので、呼出しも修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<div className='todo-edit'> <div className='title'> <div>タイトル</div> <input type='text' value={this.props.title} onChange={(e) => this.onChange_Title(e)} /> </div> <div className='content'> <div>本文</div> <textarea value={this.props.content} onChange={(e) => this.onChange_Content(e)} /> </div> <div> <button>新規</button> <button onClick={() => this.props.onClick_Submit()} >登録</button> </div> </div> |
以上でtitle
とcontent
をTodoEdit
コンポーネントからTodo
コンポーネントへ移動できました。
登録と更新
まだonClick_Submit
メソッドが空っぽなので、TODOデータの登録ができません。TODOデータが登録または更新できるようにします。
ちょっと長ったらしいですが、要はIDが一致するデータが既にあるなら更新、ないなら登録(追加)しているだけです。
Todo.tsx
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 |
// クリック:登録 private onClick_Submit() { if (this.state.title == '' || this.state.content == '') { alert("タイトルと本文を入力してください。"); return; } // 日時文字列作成 let date = new Date(); let dateStr = date.getFullYear() + "/" + (date.getMonth() + 1) + "/" + date.getDate(); let timeStr = date.getHours() + ":" + date.getMinutes() + ":" + date.getUTCSeconds(); let id = this.state.id; let todoData = this.state.todoData.slice(); const d = todoData.find((d:TodoData) => { return (d.id === id); }); let idCount = this.state.idCount; if (d) { d.title = this.state.title; d.content = this.state.content; d.date = dateStr; d.time = timeStr; } else { // TODOデータ追加 id = idCount; todoData.push({ id: id, title: this.state.title, content: this.state.content, date: dateStr, time: timeStr, }); idCount++; } this.setState({ todoData: todoData, idCount: idCount, id: id, }); } |
編集ボタンが押されたときに、Stateの値を更新します。
setState()
メソッドが呼ばれることでTodoEdit
コンポーネントも再描画されるので、編集ボタンを押したTODOデータがフォームに反映されます。
Todo.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// クリック:編集ボタン private onClick_Edit(id:number) { const d = this.state.todoData.find((d:TodoData) => { return (d.id === id); }); if (d) { this.setState({ id: d.id, title: d.title, content: d.content, }); } } |
以上で、編集ボタンが実装できました。
新規ボタン
最初の1件は登録できますが、以降は登録ボタンを押しても最初のTODOデータが更新されるだけになっています。IDがリセットされないためです。
なので、新規ボタンを押すとIDとタイトルと本文がリセットされるようにします。
新規ボタンのクリックリスナonClick_Clear()
を用意して、Stateをリセットする処理を記述します。
onClick_Clear()
メソッドはTodoEdit
コンポーネントから呼ぶため、Propsに加えます。
Todo.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// クリック:新規ボタン private onClick_Clear() { this.setState({ id: -1, title: '', content: '', }) } // 描画 public render() { return ( <div> <TodoEdit onClick_Submit={() => this.onClick_Submit()} onChange_Edit={(t, c) => this.onChange_Edit(t, c)} onClick_Clear={() => this.onClick_Clear()} title={this.state.title} content={this.state.content} /> <TodoList todoData={this.state.todoData} onClick_Edit={(i) => this.onClick_Edit(i)} /> </div> ); } |
TodoEdit
コンポーネントにも必要な変更を加えます。
PropsにonClick_Clear()
メソッドを加え、新規ボタンクリックで呼び出します。
TodoEdit.tsx
1 2 3 4 5 6 7 8 |
// Propsインタフェース interface TodoEditPropsInterface { ... onClick_Clear():void; ... } ... <button onClick={() => this.props.onClick_Clear()}>新規</button> |
新規ボタンの実装は以上です。
削除ボタンは?
削除ボタンの実装がまだ残っています。
説明がめんどうくさいここまでの知識で削除ボタンも実装できるので、試してみましょう。
おわりに
以上でReact+Typescriptの一連の記事を終わります。
ここまでの ソースコード です。
このままではリロードするとTODOデータは消えてしまいますが、APIでデータを操作できるサーバを用意して、要処でAPIを呼ぶようにすればよいので、難しいことではないでしょう。
Reactがよくわからないまま思いつきで書いてみたのでPropsやStateやメソッドの場所や内容がコロコロと変わってしまってみっともない限りでしたが、Reactの特性を理解して設計できるようになればもっとスマートに書けるようになるような気がします。