React + Typescript その4

前回のあらすじ

前回、TODOリストにTODOを追加できるようになりました。

今回は 前回のソースコード
を元に、編集ボタンで登録済みのTODOのタイトルや本文を変更できるようにします。

編集ボタンの挙動を考える

コンポーネントの構造を考えると、TodoItemコンポーネントの編集ボタンのクリックリスナからTodoEditコンポーネントのStateを変更するのはムリでしょう。

Todo
 ┣ TodoEdit ←ココニ アクセス ハ デキナイ
 ┗ TodoList
   ┣ TodoItem ←ココカラ
   ┣ TodoItem
   ┣ TodoItem
   ┣ TodoItem
   ┗ TodoItem

なので、TodoItemコンポーネントからTodoコンポーネントを経由してTodoEditコンポーネントにアクセスすることを考えました。

  1. TodoItemコンポーネントの編集ボタンを押す
  2. Todoコンポーネントのクリックリスナを呼ぶ
  3. TodoEditコンポーネントのStateの値を変更する

が、
1.と2.は問題ないのですが、3.がうまくいきませんでした。

外部からStateを変更できない

TodoコンポーネントにonClick_Edit()メソッドを用意して、Propsを通じてTodoItemコンポーネントから呼ぶ。ここまではOKです。

問題はonClick_Edit()メソッドからTodoEditコンポーネントのStateの値を変更する手段がないことです。

まずそもそも、Stateはコンポーネント内部でのみ使用するものですから、当然外部からの直接操作はできません。それではTodoEditコンポーネントのPropsを介してTodoコンポーネントから値を渡し、TodoEditコンポーネント自身にStateを変更させれば、と思ったのですが、これもうまくいきません。

ではどうするのか?

Todoコンポーネントがtitlecontentを操作できるようにするため、titlecontentTodoEditコンポーネントからTodoコンポーネントに引き上げます。

編集ボタンのクリックリスナを作る

まずTodoItemコンポーネントのPropsにクリックリスナの定義を追加して、編集ボタンが押されたときにこれを呼ぶようにします。
TODOデータはTodoコンポーネントが持っているので、引数にIDさえ渡せばどのTODOの編集ボタンが押されたのかはわかります。

TodoItem.tsx

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

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

  // クリック:編集ボタン
  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にあるtitlecontentTodoコンポーネントのStateに移動します。

Todo.tsx

まず、TodoコンポーネントのStateにtitlecontentを追加します。
編集中のTODOデータのIDも管理したいので、これも追加します。
追加したメンバ変数はコンストラクタで初期化します。

Todo.tsx

// 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に反映するメソッドです。

  // 入力
  private onChange_Edit(title:string, content:string) {
    this.setState({
      title: title,
      content: content,
    })
  }

titlecontentTodoコンポーネントで管理するようになるので、onClick_Submit()メソッドの引数としては不要になりました。onClick_Submit()メソッドの中身については後述するので、いまは空にしておきます。

  // 登録
  private onClick_Submit() {
  }

setState()メソッドを呼ぶとコンポーネントの再描画が行われるので、最新のtitlecontentTodoEditコンポーネントに反映するためにTodoEditコンポーネントのPropsへこれらを渡します。
onChange_Edit()メソッドもTodoEditコンポーネントから呼んでもらわなければならないので、これもPropsへ渡します。

  // 描画
  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()と変数titlecontentが追加されました。またonClick_Submit()メソッドの引数がなくなりました。それらをPropsに反映します。

Stateのメンバは不要になったので削除します。

TodoEdit.tsx

// 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から取得します。

  // フォーム変更:タイトル
  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の引数がなくなったので、呼出しも修正します。

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

以上でtitlecontentTodoEditコンポーネントからTodoコンポーネントへ移動できました。

登録と更新

まだonClick_Submitメソッドが空っぽなので、TODOデータの登録ができません。TODOデータが登録または更新できるようにします。

ちょっと長ったらしいですが、要はIDが一致するデータが既にあるなら更新、ないなら登録(追加)しているだけです。

Todo.tsx

  // クリック:登録
  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

  // クリック:編集ボタン
  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

  // クリック:新規ボタン
  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

// Propsインタフェース
interface TodoEditPropsInterface {
  ...
  onClick_Clear():void;
  ...
}
...
<button onClick={() => this.props.onClick_Clear()}>新規</button>

新規ボタンの実装は以上です。

削除ボタンは?

削除ボタンの実装がまだ残っています。
説明がめんどうくさいここまでの知識で削除ボタンも実装できるので、試してみましょう。

おわりに

以上でReact+Typescriptの一連の記事を終わります。
ここまでの ソースコード です。

このままではリロードするとTODOデータは消えてしまいますが、APIでデータを操作できるサーバを用意して、要処でAPIを呼ぶようにすればよいので、難しいことではないでしょう。

Reactがよくわからないまま思いつきで書いてみたのでPropsやStateやメソッドの場所や内容がコロコロと変わってしまってみっともない限りでしたが、Reactの特性を理解して設計できるようになればもっとスマートに書けるようになるような気がします。