【Laravel Sail × Editor.js】保存したテキストエディタのデータを表示・編集・更新する

現在エンジニアを目指して勉強中です。

ポートフォリオ作品にEditor.jsを導入したので、実装の内容をまとめました。
[導入~出力](https://qiita.com/Oidon/items/4b25c81a9574d8a8ee36) / [データ整形~保存](https://qiita.com/Oidon/items/3d06ef6b8e0dfc420c9a) / 画面表示・再編集 / 画像プラグイン/
の4部作となっています。（現在進行中）

今回は画面表示・再編集までです。

## 環境
・macOS Monterey 12.6
・PHP 8.1.13
・Laravel 9.3.12
・MySQL 8.0.31
・Docker 20.10.2
・Laravel Sail

## おさらいと今回の概要

[前回の記事](https://qiita.com/Oidon/items/3d06ef6b8e0dfc420c9a)では、エディタに入力されたデータを
JSON文字列としてAjaxでDBに保存しました。
```editor.js
$(function() {
  $('.js-save-note').on('click', function() {
    let input1 = $('.js-get-input1').val();
    let input2 = $('.js-get-input2').val();
    let text = '';

    editor.save()
      .then((outputData) => {
          ['time', 'version'].forEach(e => delete outputData[e]);
          text = JSON.stringify(outputData);
          ///////////////////////////////
          // ここからajax通信
          $.ajax({
            method: 'POST',
            url: '/hoge',
          // textはJSON文字列
            data: {data1: input1, data2: input2, text: text} 
          })
          .done(function() {
            window.location = ('/home');
          })
          .fail(function() {
             console.log('失敗');
          });
      })
      .catch((error) => {console.log('Saving failed: ', error)});
  });
});

```
今回は、
①保存したデータを画面に表示
②データの再編集・更新
をやっていきたいと思います。

## 保存したデータの表示
まず、保存されたJSON文字列`text`を確認してみましょう。
```
{"blocks":[
    {"id":"nqS8CCS-k1",
     "type":"header",
     "data":{
        "text":"これはh2タグです",
        "level":2
        }
    },
    {"id":"GeRkgT5Iv_",
     "type":"header",
     "data":{
        "text":"こっちはh3タグです",
        "level":3
        }
    },
    {"id":"rvYJXqlyTb",
     "type":"paragraph",
     "data":{
        "text":"サンプルテキスト<br>サンプル"
        }
    },
    {"id":"qL8EW6-g8r",
     "type":"list",
     "data":{
        "style":"ordered",
        "items":["このリストは","番号順で","並べられています"]
        }
    },
    {"id":"lTQTSXFMNF",
     "type":"list",
     "data":{
        "style":"unordered",
        "items":["一方で","こっちのリストは","ただの点です"]
        }
    }
]}
```
### 連想配列化
PHPの場合、このままのデータでは扱いづらいので、
JSON文字列から連想配列のデータに変換します。
今回使う便利な関数は[`json_decode`](https://www.php.net/manual/ja/function.json-decode.php)です

```app/Http/Controllers/HogeController.php
namespace App\Http\Controllers;

use App\Http\Requests\Hoge\CreateRequest;
use App\Models\Hoge;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class HogeController extends Controller
{
  public function show(id) {
    $hoge = Note::with('user')->find(id);
    //////////////////////
    // $hoge->textの値をjson_decodeで連想配列化して上書き代入
    $hoge->text = json_decode($hoge->text, true);

    // viewファイルと一緒に$hogeをhogeという変数名で渡す
    return view('hoge.article')
      ->with('hoge', $hoge);
}
```

これで、変数`$hoge`ごとデータをviewで扱えるようになりました。
リッチテキストエディタのデータは`$hoge->text`を出力するとこんな感じ

```php
// json_decodeで連想配列化した$hoge->textのデータ

Array
(
    [blocks] => Array
        (
            [0] => Array
                (
                    [id] => nqS8CCS-k1
                    [type] => header
                    [data] => Array
                        (
                            [text] => これはh2タグです
                            [level] => 2
                        )
                )

            [1] => Array
                (
                    [id] => GeRkgT5Iv_
                    [type] => header
                    [data] => Array
                        (
                            [text] => こっちはh3タグです
                            [level] => 3
                        )
                )

            [2] => Array
                (
                    [id] => rvYJXqlyTb
                    [type] => paragraph
                    [data] => Array
                        (
                            [text] => サンプルテキスト<br>サンプル
                        )
                )

            [3] => Array
                (
                    [id] => qL8EW6-g8r
                    [type] => list
                    [data] => Array
                        (
                            [style] => ordered
                            [items] => Array
                                (
                                    [0] => このリストは
                                    [1] => 番号順で
                                    [2] => 並べられています
                                )
                        )
                )

            [4] => Array
                (
                    [id] => lTQTSXFMNF
                    [type] => list
                    [data] => Array
                        (
                            [style] => unordered
                            [items] => Array
                                (
                                    [0] => こっちのリストは
                                    [1] => シンプルなリストです
                                )
                        )
                )
        )
)
```
### `foreach × if`で画面表示

エディタのデータが連想配列形式になりました。
`['blocks']`の中にデータが含まれているので、
あとは、`$hoge->text['blocks']`をforeachで回して
データを取り出していくだけです。

取り出したデータは、
見出しやパラグラフ、リストといった形式ごとに分岐させます。

```article.html
<!-- Laravelプロジェクトなので実際はarticle.blade.phpファイル -->

<div class="hoge_text">
@foreach($hoge->text['blocks'] as $text)
    
    <!-- $text['type']の条件分岐でhtmlタグを変更 -->

    <!-- 見出し -->
    @if($text['type'] === 'header')
    <h{{ $text['data']['level'] }} class="hoge_h{{ $text['data']['level'] }}">{{ $text['data']['text'] }}</h{{ $text['data']['level'] }}>

    <!-- 文章 -->
    @elseif($text['type'] === 'paragraph')
    <p class="hoge_paragraph">{!! $text['data']['text'] !!}</p>
 
    <!-- リスト 形式によってulタグのスタイルを変更 -->
    @elseif($text['type'] === 'list')
        <!-- 数字付きリスト -->
        @if($text['data']['style'] === 'ordered')
        <ul style="list-style: decimal inside;">
        <!-- 黒丸のシンプルなリスト -->
        @elseif($text['data']['style'] === 'unordered')
        <ul style="list-style: disc inside;">
        @endif
            <!-- リストのアイテムをforeachで表示 -->
            @foreach($text['data']['items'] as $item)
            <li>{{ $item }}</li>
            @endforeach
        </ul>

    @endif
@endforeach
</div>
```
これで保存されたデータをhtmlタグとして画面に表示することができます。

## データの再編集

ここに関しては随分ハマったのですが、
__`Editor.js`の初期化時に保存済みのデータを渡す__
のが良いという結論になりました。

[公式](https://editorjs.io/configuration#passing-saved-data)にも以下の記載があります
>Passing saved data
To initialize the Editor with previously saved data, pass it through the data property:

公式の別の箇所の記載で、`render`メソッドで云々という記載もあったんですが、
うまく実装できなかったので、上記の方法でいきます。
（もっと良い方法もありそうなので、わかる方はコメントいただけると幸いです。）

### 実装の前提
今回の自分のケースでは、
初回投稿時の画面と再編集時の画面を使い回します。
そのため、
・`editor.js`
・`editor.blade.php`
それぞれのファイルで初回投稿か再編集かを判断する処理を持たせて分岐させます。
（今回ふれるのはjsファイルのみ）

### jsの実装
#### 1. 分岐処理
jsでは、URLから分岐を判断させます。
```editor.js
$(function(){

    // 正規表現を設定
    const updateRegex = new RegExp('hoge/[1-9]+[0-9]*/edit');
    // 現在のパス取得(参考：https://developer.mozilla.org/ja/docs/Web/API/Location)
    let path = location.pathname;  

    // pathが正規表現に合致する場合は再編集
    if (updateRegex.test(path)) {　
```

#### 2. 保存済みデータの取得
上記if文の続きになります。

`Editor.js`に保存済みのデータを渡して初期化させたい
ということで、前回のデータ保存時に続いて再度`Ajax`の出番です。
```editor.js
// pathが正規表現に合致する場合は再編集
if (updateRegex.test(path)) {　

    // URLの数字部分はidなので、余計な部分を取り除いて取得。
    let id = path.replace('/hoge/', '').replace('/edit', '');

    // DBからテキストデータを取得
    $.ajax({
        headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
        },
        method: 'POST',
        url: path, // 先ほどの変数path
        data: { id: id }, // DB検索時に必要なため、idも持たせる
        dataType: 'json' // 検索結果のデータをJSON形式で取得
    })
    .done(function(savedData){
        let editor = new EditorJS ({
            holderId: 'editor',
            ////////////////////////
            // dataプロパティに保存済みのデータを持たせる。
            data: savedData,
            
            // その他は初回投稿時と一緒
            tools: {}, // 自分が使うプラグインを書いておく
            placeholder: 'プレイスホルダーの設定もできる'
        });
    })
```
これで、保存済みのデータを反映させた状態で
エディターが表示されます。

ちなみに初回投稿を判断する正規表現も用意しました。
以下の処理によって、まっさらなエディターが表示されます。
```editor.js
const createRegex = new RegExp('notes/new');

if (createRegex.test(path)) {
    // 初回投稿
    let editor = new EditorJS ({
        holderId: 'editor',
        data: {},  // dataプロパティには何も入れない。そもそもプロパティ自体書かなくてもよい
        tools: {}, // 自分が使うプラグインを書いておく
        placeholdr:
    });
}
```

### Laravelの実装
`Laravel`の`web.php`にはpost送信のルーティングを設定しておき、
コントローラーは以下のように設定しておきます。
これで、Ajax通信を受けてデータを返してくれます。
```app/Http/Controllers/HogeController.php
// テキストデータ取得
public function getText(Request $request) {
    $text = Hoge::where('id', $request->id)->value('text');
    echo $text;
}
```

### おまけ：保存するデータについて
[Editor.jsのデータ保存方法の投稿](https://qiita.com/Oidon/items/3d06ef6b8e0dfc420c9a#%E4%B8%8D%E8%A6%81%E3%81%AA%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%81%AE%E5%89%8A%E9%99%A4)に記載しましたが、私の実装では
`Editor.js`の内容を保存する際、不要なプロパティを落としてから
DBに保存していました。

```editor.js
// Editor.jsの保存時に生成されるデータ。
// エディタに記載した内容はblocksに格納されているため、timeとversionを落としていた。
{time: 1672829019201, blocks: Array(2), version: '2.26.4'}
```
結論、上記の加工の有無に関わらず、今回の保存済みデータの再表示は可能です。
[公式](https://editorjs.io/configuration#passing-saved-data)は「保存済みデータは保存時と同じオブジェクト形式で渡せ」
と言っているようですが、問題ありませんでした。
>Format of the data object should be the same as returned by Editor saving.

## 更新
データを更新する場合の処理は、基本的には初回保存時と同じです。
通信タイプに`put`を指定して`Ajax`でデータを送信します。
ここではコントローラーだけ載せてあとは割愛します。
```app/Http/Controllers/HogeController.php
public function update(UpdateRequest $request) {
    $hoge = Hoge::find($request->route('id'));
    $hoge->fill($request->all());
    $hoge->save();
    // フラッシュメッセージを追加
    session()->flash('session_success', '更新が完了しました');
    // リダイレクト用にidを返却
    echo $hoge->id;
}
```
__`$request->route('id')`について__
ルーティングで`Route::put('hoge/{hoge_id}' ~~`のようにパラメータをとる場合には
`$request->route('hoge_id')`と呼び出せるらしい。便利。

## おまけ（リファクタしてみた）

長かったですが、ここまで読んでくださりありがとうございました。

自分と同じ初心者の方向けに、
最終的にこんな感じになりました、のコードを貼っておきます。
何かの参考になれば嬉しいです。
```editor.js
import EditorJS from '@editorjs/editorjs';
import Header from '@editorjs/header';
import List from '@editorjs/list';
import Paragraph from '@editorjs/paragraph';

$(function(){

  // 新規登録か編集かを判断する正規表現
  const createRegex = new RegExp('hoge/new');
  const updateRegex = new RegExp('hoge/[1-9]+[0-9]*/edit');
  // 現在のURLを取得
  let path = location.pathname;

  ////////////////////
  // エディターJSを初期化する関数
  function initEditor(data) {
    let editor = new EditorJS ({
      holderId: 'editor',
      data: data,
      tools: {
          header: {
            class: Header,
            config: {
              levels: [2, 3, 4],
              defaultLevel: 2
            }
          },
          list: {
            class: List,
            inlineToolBar: true,
            config: {
              defaultStyle: 'unorderd'
            }
          },
          paragraph: {
            class: Paragraph,
            inlineToolBar: true,
            config: {
              preserveBlank: true
            }
          }
      },
      placeholder: '＋を押してスタート！'
    });

    // Editor.jsが格納された変数editorを関数外でも使えるようにreturn
    return editor;
  }

  ////////////////////
  // AjaxでDB保存する関数
  function saveHoge(editor, method) {
    let input1 = $('.js-get-input1').val();
    let input2 = $('.js-get-input2').val();
    let text = '';
    let redirectURL = '';

    editor.save()
      .then((outputData) => {
          text = JSON.stringify(outputData);

          // Ajax通信
          $.ajax({
            headers: {
              'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            },
            method: method,
            url: path,
            data: {data1: input1, data2: input2, text: text}
          })
          .done(function( redirect ) {
            redirectURL = 'hoge/' + redirect
            window.location = redirectURL;
          })
          .fail(function() {
             console.log('失敗');
          });
      })
      .catch((error) => {
        console.log('Saving failed: ', error);
      });
  }


  ////////////////////
  // ここから処理を開始

  ////////////////////
  // 新規投稿
  if (createRegex.test(path)) {
    // エディターを初期化
    let editor = initEditor({});

    // ノートの新規登録
    $('.js-save-hoge').on('click', function() {
      saveHoge(editor, 'POST');
    });

  ////////////////////
  // 保存済み投稿の表示・更新
  }else if (updateRegex.test(path)) {

    // 保存済み投稿を取得
    let note_id = path.replace('/hoge/', '').replace('/edit', '');
    $.ajax({
      headers: {
        'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
      },
      method: 'POST',
      url: path,
      data: { id: id },
      dataType: 'json'
    })
    .done(function(savedData){
      // 保存したデータを渡してエディタを初期化
      let editor = initEditor(savedData);

      // ノートの更新処理をセット
      $('.js-save-hoge').on('click', function() {
        saveHoge(editor, 'PUT');
      });

    }).fail(function() {
      console.log('失敗');
    });
  }

});
```
自分の場合、editor.jsファイルのなかに
初回登録時の処理と編集・更新時の処理が混在していました。

どちらもエディターの初期化とAjaxによるDBへの保存は共通していたので、
それぞれを関数化して外に出すことで比較的スッキリしました。

今回はここまでです。

## 参考

### 公式

http://takemaru-hirai.github.io/japan-map/

https://www.php.net/manual/ja/function.json-decode.php

### その他

https://web-tsuku.life/multiple-text-replace/

https://qiita.com/ntm718/items/638a8f56d30d047ead0f

https://note.com/catondesk/n/nb5264c0bf39f