2020/10/04
書き換え可能なテキストをReactJSで実装する

みなさんは下のように編集できるテキストをみたことはないでしょうか? 普段は通常のテキストなのですが、クリックすることで編集できるようになり、編集を終えれば今度は編集した後のテキストが表示されます。

このテキストはクリックすることで編集できます

よくブログプラットフォームなど会員登録のあるウェブサイトでこれまでに登録していたものを再度編集するようなインターフェイスとして利用されています。 この記事では、ReactJSを使ってこういった編集可能なテキスト表示を実装する方法を解説していきます。

編集できるテキストを作る

まずは簡単に表示から編集にクリックで切り替え、他の場所をクリックされた時に編集を終えて表示に戻す部分を実装します。

この機能に必要な引数として元々の表示する文字列を受け取ります。

編集中か表示中かを表現するisEditingを状態としてもち、編集されればその内容を都度、内部状態に保存していきます。

  • 編集用のHTML要素が持っているautoFocustrueであれば、要素が表示される際に自動的にこの要素を選択(フォーカス)します。そのため、編集に切り替えた際に改めてこの要素をクリックすることなく編集をすることができます。
  • 表示する要素でthis.state.value != "" ? this.state.value : "クリックしてテキストを編集を開始"となっているのは、空文字列が指定されている時にクリックすることができずに編集できなくなることを防ぐためです。
デモ
まずクリックによって編集できるようになりました。
import { RewritableText as RewritableText1 } from "react-notebook/dist/rewritable-text/tutorial/01-switch-edit-view"

<RewritableText1
    defaultValue="まずクリックによって編集できるようになりました。"
/>

編集内容を取り出す

今のままでは編集できるテキストがそこにあるだけですが、実際には編集後のテキストを利用して何かしら別の仕事をさせる必要があります。 例えば、登録しているメールアドレスを再度編集してもらうインターフェイスで利用する場合、通常は編集できることはもちろん、編集したあとでデータベースに送信しなければ意味がありません。また、ReactJSの枠組みで使っていると編集したあとのテキストで他のコンポーネントの状態を変更することも多いと思います。

コールバックを実行する段階として、内容が変更された時のonChangeと編集が終了した時のonFinalizedの2つを設定します。

そこでこうした仕事をさせるためのコールバック関数を与えられるように実装します。

受け取ったコールバック関数を適切に設定すると以下のような実装になります。

デモ
デベロッパーツールで編集過程を、編集が完了した段階でアラームで見られます
import { RewritableText as RewritableText2 } from "react-notebook/dist/rewritable-text/tutorial/02-side-effect"

<RewritableText2
    defaultValue="デベロッパーツールで編集過程を、編集が完了した段階でアラームで見られます"
    onChange={(value) => console.log(value)}
    onFinalized={(value) => alert(value)}
/>

使いやすさの改善

これで最低限用が足りる機能はありますが、実際の利用では様々な追加機能がなければ使いづらいかと思います。例えば以下のような問題・改善点が挙げられます。

  1. 見ただけでは編集可能なテキストかどうかわかりにくい
  2. 空文字列の時に表示される内容を引数として渡したい
  3. 編集中にEnterキーで確定したい
  4. 入力値にバリデーションをかけたい

見ただけでは編集可能なテキストかどうかわかりにくい

これはデザインの問題であり、唯一の決定的な方法があるわけではありませんが、有効な解決策として「クリックできることを示す」ことと「編集できることを示す」ことの二つを実践していきます。

そのためマウスカーソルをcursor: "pointer" で指定しクリックできる雰囲気を出した上で、編集できる感を出すために鉛筆マークをテキストの右横に配置してみました。

デモ
マウスの形がポインタになりました
import { RewritableText as RewritableText3 } from "react-notebook/dist/rewritable-text/tutorial/03-icon"

<RewritableText3
    defaultValue="マウスの形がポインタになりました"
    onChange={(value) => console.log(value)}
    onFinalized={(value) => alert(value)}
/>

どうでしょうか。少しはマシになりましたか? 他にも下線の装飾を入れたり色を変えたりなどここでは工夫しているサイトもあります。

空文字列の時に表示される内容を引数として渡したい

現時点での実装では内部状態が空文字列になってしまった時にクリックしてテキストを編集を開始という文言を表示していますが、これはサイトを国際化するなどの状況でコンポーネントの再利用する上で大きな障害になります。

内部が空文字列になった際に好きな文字列やReactコンポーネントを渡して表示できるようにしましょう。

デモ
文字列をなくすと怒られます
import { RewritableText as RewritableText4 } from "react-notebook/dist/rewritable-text/tutorial/03-icon"

<RewritableText4
    defaultValue="文字列をなくすと怒られます"
    onChange={(value) => console.log(value)}
    onFinalized={(value) => alert(value)}
    displayWhenEmpty={<span style={{color: "red"}}>何か文字列を入力してください!</span>}
/>

編集中にEnterキーで確定したい

キーボードイベントから入力された内容を確認してそれがEnterキーであれば編集と表示を切り替えるtoggleEditingを呼び出すようにします。

Enterキーを検出する方法としてkeyboardEvent.keyCode13かどうか確認するというのが長く使われてきましたが、こちらは現在標準から廃止(deprecated)されました。

Reactの公式ドキュメントでは対応するReact.KeyboardEventでは使えるプロパティとしてはっきりとkeyCodeが書いてありますが、ここではMDNのドキュメントにもReactのドキュメントにもあるkeyの内容がEnterかどうか確認します。

(これはこれでハードコードされている感があって怖い気もしますが....一応公式ドキュメントではW3Cでのイベントの仕様で決まっている範囲のどの値でも取りうるというように言っていますが、それならそれでそのように型をつけてくれたらと思います。)

デモ
Enterキーで編集を終了できます
import { RewritableText as RewritableText5 } from "react-notebook/dist/rewritable-text/tutorial/03-icon"

<RewritableText5
    defaultValue="Enterキーで編集を終了できます"
    onChange={(value) => console.log(value)}
    onFinalized={(value) => alert(value)}
    displayWhenEmpty={<span style={{color: "red"}}>何か文字列を入力してください!</span>}
/>

入力値にバリデーションをかけたい

入力された内容を他の場所で利用する上での制約だったり、嘘の入力や誤った入力を避けるためにフォームを送信する場合にバリデーションをかけることがよくあります。 例えばメールアドレスの入力欄であれば含まれない文字列などルールが決まっています。

利用シーンや入力内容に応じて様々なパターンがありますが、ここではそれらを一般化したバリエーション用の関数valiation: (value: string) => booleanを引数として受け取り、falseになった場合は下にvalidationErrMsgで生成する警告文を赤字で表示するという形で利用者にバリデーション違反を通知します。 ここは関数ではなく正規表現オブジェクトを渡すようにしてもいいかもしれません。

実際にどのようにバリデーションをかけるかというのはいろいろあり、例えば本来ならそもそもバリデーションでこけた場合はonFinalizedコールバックを呼び出すべきでないとか、そもそも編集を完了できないようにするべきなど、このコンポーネントの利用ケースに応じて改造してください。

onFinalizedを呼び出したくない場合は、input内のonBlurイベントでprops.validationを呼び出して結果がtrueになっていればprops.onFinalizedを呼ぶようにします。 さらに編集を完了したくない場合はonBlur中でtoggleEditingを呼び出さないようにします。Enterキーでfinalizeを呼ばないようにしても良さそうです。

デモ
sample@sample.com
import { RewritableText as RewritableText5 } from "react-notebook/dist/rewritable-text/tutorial/03-icon"

<RewritableText6
    onChange={(value) => console.log(value)}
    onFinalized={(value) => alert(`入力が完了しました:${value}`)}
    defaultValue="sample@sample.com"
    displayWhenEmpty="メールアドレスを入力してください"
    validation={(value) => {
        let reg = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
        let result = reg.exec(value)
        if (result != null) {
            return result[0] === value
        } else {
            return false
        }
    }}
    validationErrMsg={(value) => `正しいメールアドレスの形式ではありません: ${value}`}
/>

終わりに

あとは若干のスタイルの調整を施せば以下のようになります。

sample@sample.com

単純なコンポーネントに見えて型付けや使いやすさのことに気を配ると案外に気を配ることが多くてReactJSのいい練習になったのではないでしょうか? ぜひこのコンポーネントと紹介したテクニックをあなたのソフトウェアに組み込んでみてください。