Rails6+Reactで付箋アプリっぽいページを作ってみた。5(react-contenteditable導入編)

# 記事について
[前回](https://qiita.com/hanaitama-kaz/items/e82a9ec9538914861746)まででUIをそれっぽくしてみましたが、そろそろ付箋の中身を書き換えたくなってきました。
ということで、やってみます。

# 関連する記事
今までの記事です。
[その１(環境構築〜モデル作成編)](https://qiita.com/hanaitama-kaz/items/5b68b9c665f75020bcd4)
[その２(API作成編)](https://qiita.com/hanaitama-kaz/items/b461ff8564a5368523fc)
[その３(UI作成編1)](https://qiita.com/hanaitama-kaz/items/9b680c50566161046690)
[その４(UI作成編2)](https://qiita.com/hanaitama-kaz/items/e82a9ec9538914861746)
[その５(react-contenteditable導入編)](https://qiita.com/hanaitama-kaz/items/17832369f279ba0e331a)
[おまけ(モデルのテスト編)](https://qiita.com/hanaitama-kaz/items/d3b9e54913139aecf530)

# divのまま編集してみたい。
またしても手段を目的として面倒臭いことになるパターンです。
これまで、タスクのタイトルや説明などをdivタグで作ってきてしまったので、そのまま編集可能にできたら修正が少なくなって楽かもしれない？

divタグを編集可能にするには```contentEditable=true```という属性を使います。
[参考(MDN Web Docs - contentEditable)](https://developer.mozilla.org/ja/docs/Web/HTML/Global_attributes/contenteditable)

なんだ、簡単そうだ。
早速、[以前作った](https://qiita.com/hanaitama-kaz/items/9b680c50566161046690)Sticky.jsのタイトル表示部分で試してみます。

```javascript:app/javascript/components/Sticky.js(変更箇所のみ)
// contentEditable="true"を追加
<div className="TaskTitle" contentEditable="true">{this.props.task.title}</div>
```

実際にブラウザで表示するとどうなるか！？

おやおや？？？
<img width="438" alt="スクリーンショット 2020-05-23 20.26.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244595/9d7817f9-a93c-d4b9-e357-c0af028c56c9.png">

Reactから「contentEditableはあなたの責任で使いなさいよ」という警告メッセージが出ていました。

はーい、がんばりまーす。
って言うほど頑張れないので、メッセージ出さない方法ないかなぁ？
と調べた結果、[react-contenteditable](https://github.com/lovasoa/react-contenteditable)というものを見つけたので、使ってみたいと思います。

# react-contenteditableの追加

以下のとおり、yarnで追加できました。

```bash:shell
bundle exec yarn add react-contenteditable
```

# react-contenteditableを使ってみる。
インストールができたので、ソースコードに追加してみます。
対象は、先ほどのSticky.jsです。

## ソースコードの変更

```jsx:app/javascript/components/Sticky.js
import React from "react"
import PropTypes from "prop-types"

// ContentEditableコンポーネントをimportします。
import ContentEditable from "react-contenteditable"

class Sticky extends React.Component {
  // (中略)
  // レンダラです。
  render () {
    return (
          { /* 先ほどのdivをContentEditableに変更します。 */ }
          { /* 表示内容はhtml=で指定します。 */ }
          <ContentEditable className="TaskTitle" html={this.props.task.title} />
          { /* 以後、色々省略 */ }

    );
  }
}

export default Sticky
```
コメントしてあるとおり、最初のimport文でContentEditableをインポートし、render()内で編集可能にしたい要素をContentEditableに置き換えます。

なお、propsとして指定できるのは、以下のもののようです。

| お名前 | ご説明 | 型 |
|:------|:------|:---|
|innerRef|refに指定する値(あとで値の取得等に使うためのものです。)|Object または Function|
|html|要素の値です。名前の通りhtmlが利用できます。必須要素です。(null値はエラーになる。。)|String|
|disabled|trueにすると編集不可にできます。|Boolean|
|onChange|innerHTMLの内容が変更されたときに呼び出されるハンドラを指定します。 | Function |
|onBlur|フォーカスが離れた時のハンドラです。| Function |
|onKeyUp|キーを離した際のハンドラ| Function |
|onKeyDown|キーを押した際のハンドラ| Function |
|className|そのまんまクラス名です。| String |
|style|スタイルも指定できるんですね。| Object |

## 動作確認
ソースコードを直したら、早速反映されたか確認してみます。
<img width="253" alt="スクリーンショット 2020-05-24 10.33.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244595/727e33c5-89c7-251b-befa-199072caa1f8.png">

おぉ！編集できる！！
ならば、この値を取得して保存する流れを作ってみます。

## CotentEditableで作成した要素から値を取得する。
reactで編集可能な要素の値を取得するには、onChangeを使ってstateに値を保存するなどの方法が考えられますが、ここではrefを使った方法を使ってみます。
ContentEditableコンポーネントでは、innerRefというプロパティを使うことで実現します。

具体的には以下のようなソースコードとなります。(変更箇所に1〜5の番号をふってあります。)

```jsx:app/javascript/components/Sticky.js
import React from "react"
import PropTypes from "prop-types"

// ContentEditableコンポーネントをimportします。
import ContentEditable from "react-contenteditable"

class Sticky extends React.Component {
  // コンストラクタです。
  constructor(props) {
    // おまじないです。
    super(props);

    /*
    ((中略))
    */

    // createRefで要素参照用のインスタンス変数を作ります。 -- 1
    this.taskTitle = React.createRef();

    // 保存ボタンクリック時のハンドラをバインド -- 2
    this.onSaveButtonClick = this.onSaveButtonClick.bind(this);

  }

  /*
   ((さらに中略))
  */

  // 保存ボタンクリックイベントハンドラ -- 3
  onSaveButtonClick(event){
    // 以下のように"current.textContent"で要素の値が取得できます。
    // ここでは、とりあえず取得した値をconsole.logに吐いてみます。
    console.log(this.taskTitle.current.textContent);
  }

  // レンダラです。
  render () {
    return (
          { /* innerRef={this.taskTitle}という記述を追加しました。 -- 4 */ }
          <ContentEditable className="TaskTitle" html={this.props.task.title} innerRef={this.taskTitle} />
          { /* 保存ボタンとして使う要素を追加します。 -- 5 */ }
          <div className="TaskFooter">
            <div className="TaskUpdateButton" onClick={this.onUpdateButtonClick} >save</div>
          </div>
          { /* 以後、色々省略 */ }

    );
  }
}
```

表示する要素が増えてしまったので、ついでに、スタイルシートも追加しておきます。

```scss:app/assets/stylesheets/white_board.scss
// 付箋のフッター
div.TaskFooter {
  grid-row: 8;
  grid-column: 1 / 3;
  display: flex;
  justify-content: space-between;
}

// 保存ボタン
div.TaskUpdateButton {
  color: #0000FF;
  font-weigt: bold;
  font-size: 10px;
  text-align: right;
}

```

試してみます。
<img width="130" alt="スクリーンショット 2020-05-24 15.13.44.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244595/5ef88031-d372-915c-4b3d-55d16f3beacd.png">
タイトルを編集(hogegeってしたり、updatedってしたり)して、"save"を押してみると。
おぉ、ログが出た。
<img width="452" alt="スクリーンショット 2020-05-24 15.04.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244595/474393ee-7951-92bb-33b4-220252e8efe5.png">

# 実際にDBに保存させてみる。
編集した値が取得できることが分かったので、実際にDBに反映する流れを作ってみます。

## シーケンス図で考える。
Railsで作ってるAPIにて

```ruby:APIの処理イメージ
@task = Task.find(params[:task][:id]);
@task.update(params[:task]);
```
みたいな感じになれば良いわけですが。
誰がこのAPIを呼ぶ？
みたいなことを考えねばなりません。

で、以前作っている[WhiteBoard.js](https://qiita.com/hanaitama-kaz/items/9b680c50566161046690)ですでにAPI呼び出しを行っているので、
こいつにまとめてしまおう。
ちょっと複雑になるので、シーケンス図を起こしてみました。
<img width="713" alt="スクリーンショット 2020-05-24 16.28.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/244595/ed14de76-f51c-bdf1-3e0c-ba7653613b1d.png">

図にするとわかりやすいですね。
最終的にWhiteBoardのonTaskSave()が呼ばれるようにpropsを引き継いでいけば実装できそうです。

## APIを用意してみる。
### コントローラの準備
まずは、コントローラの準備をします。

```ruby:app/controllers/api/tasks_controller.rb
  # タスク情報更新処理
  def update
    # エラーメッセージリストを初期化
    @errmsgs = [];

    # 無害化したパラメータを取得
    updparam = update_params();

    begin
      # タスク情報を取得
      @task = Task.find(updparam[:id]);

      # アップデート実行
      if ! @task.update(updparam) then
        # エラー時はエラーメッセージリストにエラーメッセージを追加しておいて
        @task.errors.each do | key |
          @task.errors[key].each do | message |
            @errmsgs.push(key.to_s + ":" + message);
          end
        end
        # エラー表示用レンダラーを指定します。
        render :show_error
      end

    rescue => ex
      # こちらも、例外情報をメッセージリストに追加して
      @errmsgs.push(ex.to_s);
      # エラー表示用レンダラーを指定します。
      render :show_error
    end

    # 更新がうまくいったら、json形式で更新結果をお知らせするので、
    # app/views/api/tasks/update.json.jbuilderを用意しておきます。
    # Railsのデフォルト動作では、コントローラと同じ名前のviewを表示しようとするので、"render :update"などと書く必要はありません。
    # 必要はありませんが、知らないと分からないですよね。。

  end

  private
    # update時パラメータの取得
    def update_params()
      return params.require(:task).permit(:id, :title, :description, :due_date, :user_id);
    end
```
### Viewを作る
次にviewを用意します。

```ruby:app/views/api/tasks/update.json.jbuilder
# 更新後のデータを返せば良いかなぁ。と。
json.task do
  json.id(@task.id);
  json.title(@task.title);
  json.description(@task.description);
  json.due_date(@task.due_date.strftime("%Y-%m-%d"));
  json.user_id(@task.user.id);
end
```

エラー時はこんな感じでいいかしらね。

```ruby:app/views/api/tasks/show_error.json.jbuilder
# エラーメッセージをJSON形式で返します。
json.errors @errmsgs do | msg |
  json.message(msg);
end
```

### ルーティングを追加する。
コントローラにアクションを追加したので、ルーティングを追加します。
updateだから、putで。

```ruby:config/route.rb
namespace :api do
  put 'tasks/update'
end
```

### とりあえずテスト
ブラウザでいちいち動作確認するのめんどいので、テストコードを追加しときます。
エラー系は、とりあえず後で。。

```ruby:test/controllers/api/tasks_controller_test.rb
  test "should success to update" do
    task2 = tasks(:task2);
    due_date = Date.new(1894,2,11);

    put(api_tasks_update_url(:json), params: { task: { id: task2.id, title: "title-updated", description: "description-updated", due_date: due_date }});
 
    assert_response :success

    json_data = ActiveSupport::JSON.decode(@response.body);
    assert_equal(task2.id, json_data['task']['id']);
    assert_equal("title-updated", json_data['task']['title']);
    assert_equal("description-updated", json_data['task']['description']);
    assert_equal(due_date, Date.parse(json_data['task']['due_date']));
  end

```

## フロントエンド側に処理を追加していく
### Rails側の修正
Reactで作ったコンポーネントから、追加したアクション(今回はtasks/update)を呼び出してもらうため、react_compnent()呼び出し時の引数を修正します。

```erb:app/views/white_board/main.html.erb
<%
# update_task_url: api_tasks_update_url(:json)
# を追加しました。
%>
<%= react_component('WhiteBoard', { 
  title: 'You can let others do your task', 
  user_tasks_url: api_users_user_task_list_url(:json), 
  switch_user_url: api_tasks_switch_user_url(:json),
  update_task_url: api_tasks_update_url(:json),
  secure_token: form_authenticity_token
}) %>

```

### WhiteBoard.jsの修正
ここには、updateを呼び出すための処理の追加と、自身の"onTaskSave()"をコールバックしてもらうための処理を追加します。

```jsx:app/javascript/components/WhiteBoard.js 
import React from "react"
import PropTypes from "prop-types"

// 自作コンポーネントはこのように呼び出せます。
import UserBox from "./UserBox"

// WhiteBoardコンポーネントの定義
class WhiteBoard extends React.Component {
  // コンストラクタ
  constructor(props) {
    // おまじない
    super(props);

    // いくつか省略

    // イベントハンドラのバインド
    this.onTaskSave = this.onTaskSave.bind(this); // タスク更新時の処理
  }

  // またまた省略

  // タスク更新イベント処理
  onTaskSave(task) {
    // タスク更新処理(tasks/update)を呼び出します。
    fetch(this.props.update_task_url, {
      method: "PUT",
      headers: {
        "Content-Type": "application/json; charset=utf-8",
        "X-CSRF-Token": this.props.secure_token
      },
      body: JSON.stringify(task)
    })
    .then(response => response.json())
    .then(json => {
      /* 実は返されたデータの使い道を見失った。。 */
      console.log(JSON.stringify(json));
    })
    .catch(error_response => console.log(error_response));
  }

  // レンダリング
  // UserBoxのpropsにonTaskSaveを追加しました。
  render () {
    return (
      <React.Fragment>
        <div id="WhiteBoardFlame">
          <div id="WhiteBoardTitle">{this.props.title}</div>
          <div id="AddUserButton" onClick={this.onAddUserClick} >+Add User</div>
          { this.state.show_add_user && <UserForm onAddButtonClick={this.ExecuteAddUser} onCancelButtonClick={this.CancelAddUser} /> }
          <div id="WhiteBoard">
            { ! this.state.loading && this.state.users.map((user) => <UserBox user={user} key={user.id} dropHandlerRegister={this.dropHandlerRegister} onTaskDrop={this.onTaskDrop} onTaskSave={this.onTaskSave} /> )}
          </div>
        </div>
      </React.Fragment>
    );
  }
}

```

### UserBox.jsの修正
ここにも、onTaskSaveというメソッドを追加してあげます。

```jsx:app/javascript/components/UserBox.js
// ユーザ毎の箱を表示します。
class UserBox extends React.Component {

  // コンストラクタです。
  constructor(props) {
    // おまじない
    super(props);

    // 色々省略
    // イベントハンドラのバインド
    this.onTaskSave = this.onTaskSave.bind(this); // <-- onTaskSaveメソッドのバインドを追加します。

  }

  // タスク更新処理
  onTaskSave(task) {
    // stateに保持しているタスクデータを更新
    var tasks = this.state.tasks;
    tasks[task.id] = task;

    // さらにコールバック(この中でDB反映が行われます。)
    this.props.onTaskSave(task);

    // stateを更新
    this.setState({tasks: tasks});

  }

  // レンダリング
  render () {
    return (
      <React.Fragment>
        <div id={"user-" + this.props.user.id} className="UserBox" onDrop={this.onDrop} onDragOver={this.preventDefault} >
          <div className="UserName">{this.props.user.name}</div>
          <div className="TaskArea">
            {
              Object.keys(this.state.tasks).map(
               (key) => <Sticky 
                          user_id={this.props.user.id} 
                          task={ this.state.tasks[key] } 
                          key={ key }
                          onTaskSave={this.onTaskSave} /> 
              ) 
            }
          </div>
        </div>
      </React.Fragment>
    );
  }
```

### Sticky.jsの修正
やっとここまできましたー。
最初に書いたonSaveButtonClickの中身をちゃんと書いてみます。

```jsx:app/javascript/components/Sticky.js 
  // 保存ボタンクリックイベントハンドラ
  onSaveButtonClick(event){
    // 以下のように"current.textContent"で取得した値を使って、
    // UserBoxのonTaskSave()を呼び出して（コールバックして）あげます。
    var task = this.props.task;
    task.title = this.taskTitle.current.textContent;
    this.props.onTaskSave(task);
    
    // title以外の値も同じように更新できます。

  }
```

### テストじゃぁ！！
ここでも、しつこくテストを追加していきます。
UI側は、systemテストでやるのが楽です。

```ruby:test/system/whiteboards_test.rb
  test "sticky is able to update" do
    # fixutreで登録したデータを取得しておきます。
    task1 = tasks(:task1);

    # divのidを設定します。
    task1_id = "task-" + task1.id.to_s;

    # white_board/mainを開く。
    visit white_board_main_url;

    # task1の要素を取得します。
    div_task1 = find(id: task1_id);

    # title要素を取得します。
    div_title1 = div_task1.find("div", class: "TaskTitle");

    # title要素の中身を書き換えます。(text=でできるかと思ったら、setでした。)
    div_title1.set("task1_updated!!");

    # saveボタンを押しちゃいます。
    div_task1.find("div", class: "TaskUpdateButton").click();

    # 表示されている値が更新されていますように。
    assert_equal("task1_updated!!", div_title1.text);

    # DBに反映されていますように！！
    task1_updated = Task.find(task1.id);
    assert_equal("task1_updated!!", task1_updated.title); 

  end
```

# まとめ
1. Reactでレンダリングした要素でcontentEditable=trueを使いたい場合は、react-contenteditableを使うと良い。
1. しかし、今回の場合はinputタグでスタイル指定してそれっぽく見せたほうが良かったのではないかと。
1. ContentEditableで描画した部分も普通にCapybaraでテストできる。