Litで作ったWebコンポーネントをSpring Web MVCのWebアプリに組み込んだ話

[Lit](https://lit.dev/)はWebコンポーネントを開発するのに使用できるモダンなフロントエンドフレームワークです。

Webコンポーネント([Web Components | MDN](https://developer.mozilla.org/en-US/docs/Web/Web_Components))は、簡単に言えば以下のような感じで使用できる再利用可能なHTMLタグという感じです。

```html
<html>

  <script type="module" src="sample-web-components.js"></script>

  <sample-web-components param="なんらかのパラメータ"></sample-web-components>

</html>
```

私は普段フロントエンド開発でAngularやVue等のフレームワークを使っているので、それらに対応したオープンソースの部品を見つけてきたり、自分で実装したりすれば事足りていました。そのため、特にWebコンポーネントを使う理由はあまり無いと思っていました。

しかし、ある案件でWebコンポーネントが良い感じにハマる構成があったので、本記事で紹介したいと思います。

## アプリの概要

このアプリはいわゆる典型的なSpring Web MVCのマルチページアプリケーションで、JavaのロジックやボイラープレートコードとHTMLテンプレートで構成されていました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/dabcac12-cccd-5f2b-0fab-99fa83f64faf.png)


このアプリの機能改修である画面を追加することになり、画面の要件が「サーバーから取得したデータをリッチなUIで色々と編集し、保存ボタンでサーバーに送信して保存する」という物でした。

「リッチなUI」にはCanvasでいい感じにデータを表示したり、Canvas上のマウス操作でデータを編集するといった機能が含まれていたため、JavaScriptのロジックが必要でした。  
しかし、Spring自体はフロントエンド側の実装とは関係が無いため、JavaScriptを自分で書く必要があります。  
昔はjQuery等のライブラリで頑張って実装したものですが、最近はVSCode/TypeScript/ESLintといった開発環境に慣れてしまったためもはや昔のような生JavaScriptを書きたくありませんでした。

そこで考えたのが、以下のような方式です。
* 編集対象のデータはJSONで表現する。
* 画面の表示時は、DBから取り出したデータをJSON化してViewに埋め込む。
  * `<script>`タグ内にJavaScriptオブジェクトとしてグローバル定数にする。
* リッチなUI部分はWebコンポーネントで実装する。
  * 実装したWebコンポーネントはHTMLテンプレート内で読み込んで使用する。
* フロントエンドからサーバーへの送信は通常のフォーム送信(formタグ)で行う。
  * Webコンポーネントでは編集後のデータをJSON化→文字列化して`<input type="hidden">`要素のvalueに設定する。
  * その状態でformをsubmitすることで、編集後のデータを文字列としてサーバーに送信する。
* サーバーでは受信したJSONを適宜DBに格納する形に変換して保存する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/f1b2d6d5-a5f2-00c7-a845-c2e0793d3c2c.png)

この方式には以下の利点があると思っています。
* リッチなUI部分をLitを使って実装できる(TypeScriptでコーディングできる！)
* バックエンド(Java)側はシンプルなデータ形式の変換ロジックのみで実装できる。
  * 特殊な処理は必要なく、HTML内でWebコンポーネントのタグを書くだけで使用できる

## Litコード

公式ドキュメント[lit.dev](https://lit.dev/docs/tools/starter-kits/)には[Lit TypeScript starter project](https://github.com/lit/lit-element-starter-ts)が掲載されていますが、たまたま調べていた[Vite](https://vitejs.dev/)のテンプレートにLitの物があったため、こちらを使用します。

```shell-session
$ npm create vite@latest my-lit-component -- --template lit-ts
```

以下のようなディレクトリ構成が出力されます。
```
my-lit-component/
  public/
    vite.svg       生成されたサンプルページ内に表示される画像アセット(後で消す)
  src/
    assets/
      lit.svg      生成されたサンプルページ内に表示される画像アセットその2(後で消す)
    index.css      index.htmlから読み込む用のCSS
    my-element.ts  Webコンポーネントのコード本体
    vite-env.d.ts
  .gitignore
  index.html       テスト用のメインHTML
  package.json
  tsconfig.json
```

以下のように実行することで、テスト用のメインHTML内にWebコンポーネントが埋め込まれた状態で起動します。
```shell-session
$ npm install
$ npm run dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/58569919-c4da-3187-ad59-1396ba2de26d.png)  
↑ボタンをクリックすると数字が増えるよくあるサンプル

### サーバーからの値を表示

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/c3d2d9b1-0bef-4587-1530-70858d88590a.png)

サーバーからの値をLitコンポーネント側から参照する部分の概略です。  
まず、テスト用のメインHTMLをSpring側のテンプレートHTMLを想定した物に書き換えます。

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite + Lit + TS</title>
    <link rel="stylesheet" href="./src/index.css" />

    <!-- 追加: サーバーでデータを埋め込む想定のscriptタグ -->
    <script type="module">
      window.myAppData = {
        "data": [
          {"name": "John", "age": 20},
          {"name": "Charlie", "age": 35}
        ]
      };
    </script>

    <script type="module" src="/src/my-element.ts"></script>
  </head>
  <body>
    <my-element></my-element>
  </body>
</html>
```

`追加:` とある部分が追加した箇所です。

コンポーネント側(my-element.ts)からは以下のように参照できます。
```ts
  data: any = (window as any).myAppData; // TODO 型定義

  render() {
    const dataElements = this.data.data.map(
      (item: any) => html`<div>name = ${item.name}, age = ${item.age}</div>`
    );
    return html` <div>${dataElements}</div> `;
  }
```
(型定義の部分は適宜d.tsで定義することが可能ですが今回は省略しています)

### サーバーに値を送信

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2238582/87ce7d74-6668-cdb6-deed-6b98ebf4f79b.png)

サーバーに値を送信する箇所は以下のような感じになります。  
HTML側はformタグと、デバッグ用にformで送信されるはずの値を表示できるようにします。

```html
  ...
  <body>

    <!-- 追加: ユーザー操作で送信するフォーム -->
    <section class="debug-form">
      <form id="debugForm" method="post">
        <input id="dataOutput" name="data" type="hidden">
        <input type="submit" value="送信">
      </form>
    </section>

    <!-- 追加: デバッグ用に送信されたフォームのJSON値を表示する処理 -->
    <div id="debugOutput" class="debug-output"></div>
    <script type="module">
      function debugSubmit(e) {
        e.preventDefault();
        // <input id="dataOutput">から値を取得して<div id="debugOutput">内に表示する
        const jsonValue = document.getElementById("dataOutput").value
        document.getElementById("debugOutput").textContent = jsonValue;
      }
      document.getElementById("debugForm").addEventListener("submit", debugSubmit);
    </script>

    <my-element></my-element>

  </body>
```

リッチなUI部分は省きますが、コンポーネント側(my-element.ts)からformに値をセットする部分は以下のようなシンプルなコードになります。
```ts
  private _setOutputValue() {
    const jsonString = JSON.stringify(this.data); // this.dataはリッチなUIにより書き換え済み
    const inputElement = document.getElementById("dataOutput") as HTMLInputElement;
    inputElement.value = jsonString;
  }
```

あとは、Litのコンポーネントで好きなだけリッチなUIのロジックを作るだけです。

## まとめ

本記事では、LitコンポーネントをSpring Web MVCのマルチページアプリケーションに組み込んだらいい感じにできた話を紹介しました。  
今後も、同様の構成ではLitコンポーネントの使用を検討したいと思います。