WebComponents(Custom Elements / ShadowDOM / ESModules)を使用してExcelライクなライブラリを作ってみた

# はじめに
近年、素のJavaScript(Vanilla JS)の進化は著しく、数々の便利な機能がリリースされています。しかしながら、ネット上の活用事例が未だ充実しておらず大体の人は諦めてフレームワーク(Vue,Angular,React等)を利用してロックインされているのが現状です。

そこで本記事では、実際にWebComponentsを利用して作成をした、Excelライクな表作成ライブラリを用いてWebComponentsの機能を紹介します。(実際に作成したライブラリはGITに公開しています)

WebComponentsのメリットは一言でいうとカプセル化です。WebComponentsの主な機能は以下の通り3つあり（本当は4つですが4つ目は推奨されていないので省略）、WebComponentsの価値はそれらの機能を全て特定のフレームワークに依存せず実現できることにあります。

 - WebComponentsの3機能
   1. CustomElements：タグに紐づいた、そのタグの振る舞いのカプセル化    
   2. ShadowDOM : DOMの隠蔽化やCSSの分離といったカプセル化
   3. ESModules : js Objectのカプセル化

# 実際のソースコードと対応させて見てみよう！
まずは完成した画面です。Excelっぽく複数選択やソート、行追加/削除も可能。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2543425/280927b6-5bdd-ee98-eb69-d580ae3b52d9.png)



それでは、HTMLから見ていきます。非常にシンプルだと感じるはず。
```html:excel.html
<!DOCTYPE html> 
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

<link rel="shortcut icon" href="./images/favicon.ico" type="image/vnd.microsoft.icon">
<link rel="icon" href="./images/favicon.ico" type="image/vnd.microsoft.icon">
<title>excelライクなテーブル</title>
</head>
<body>
  <h3>右クリックで行追加/削除できます</h3>
  <local-sheet></local-sheet>

<script type="module" src="./js/local-sheet.js"></script>

</body>
</html>
```
 - `<local-sheet></local-sheet>`がCustom Elementsと呼ばれている部分です。今まではひたすらdivを使っていましたが、独自のタグ名だけで使いまわせるパーツを作れます。
 - 上記の中身(中身はlocal-sheet.jsに記述)が後述しますがShadowDOMというものになっており、通常のDOMツリーの要素の下に隠れたDOMツリーを取り付けています。ShadowDOMを使用するメリットは、何といっても独立したDOMとして扱えるためcssを該当のツリーにのみ適応する等、他のDOMから影響を受けない独立したツリーとして扱える点です。
 - `<script type="module" src="./js/local-sheet.js"></script>`の部分がESModulesの部分で、local-sheet.jsをモジュールとして呼び出しています。モジュール化のメリットとしてはモジュール化したjsから別のjsを呼び出せるようになる点がありますが、1点注意事項としてローカルではCORS policyに引っかかるようになるため動かなくなります。ブラウザのセキュリティを下げるか、おとなしくサーバ上で動作させてください。

次にHTMLでモジュールとして呼び出したlocal-sheet.jsの中身を見てみます。
```js:local-sheet.js
 //===============================================================
 //  画面表示部分のJS
 //===============================================================
 //===============================================================
 //  ShadowDOM生成
 //===============================================================
class LocalSheet extends HTMLElement {
  constructor() {
    super();
    //HTMLから分離されたノード(ShadowDOM)を作成（親のドキュメントのCSSなどに影響されない）
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.innerHTML = `

<!-- ShadowDOM用css読み込み -->
<link rel="stylesheet" href="./css/local-sheet.css">
<link rel="stylesheet" href="./css/filter.css">

<!-- table構造 -->
<table id="localSheet" style="table-layout:fixed;"">
  <thead id="localSheetThead">
  </thead>
  <tbody id="localSheetTbody">
  </tbody>
</table>

~~~ 中略 ~~~

<div id="tableFilter"></div>
`;
  }
}
 //===============================================================
 //  タグで呼び出せるように設定 + 基幹関数実行
 //===============================================================
(function main() {
  // local-sheetという名前でLocalSheetクラスを登録する
  customElements.define("local-sheet", LocalSheet);

  import ("./local-sheet-function.js")
      .then((obj) => {
          // 各モジュールには、引数objのプロパティからアクセスできる。
          obj.localSheetFunction();
      });
})();
```
 - `customElements.define("local-sheet", LocalSheet);`がCustom Elementsの部分。HTMLで`<local-sheet></local-sheet>`と記載すれば呼び出せるようにしています。
 - `class LocalSheet extends HTMLElement {`がShadowDOMの部分。
 - `import ("./local-sheet-function.js")`がESModulesの部分。jsから別のjsをインポートしています。

最後に、local-sheet.jsからインポートされているlocal-sheet-function.jsの中身です。
```js:local-sheet-function.js（抜粋）
 //===============================================================
 //  テーブル構築をする基幹となる関数
 //===============================================================
export function localSheetFunction() {
  if (dbUseFlg == 1){
    let uri = getUri();
    const encodeUri = encodeURI(uri);
    fetch(encodeUri)
      //KAGRA API実行
      .then(function(res){
        return(res.json());
      })
      .then(function(json){
        let arrayData = JSON.stringify(json["return"]);
        arrayData = JSON.parse(arrayData);
        //th入力
        arrayThData = arrayData[0];
        //thを削除
        let exe = [0];
        for(let i=0; i<exe.length; i++){
          arrayData.splice(exe[i]-i, 1);
        }
        //tdを入力
        arrayTdData = arrayData;
        setTableInfo();
        return(json.setDisable);
      })
      .catch(function(err){
        console.log(err);
      });
  } else{
    setTableInfo();
  }
}
```
 - 見るべき点は`function`先頭についている`export`です。別のjsから呼び出されるためには、`export`の記載が必要です。

# さいごに
いかがだったでしょうか。使い慣れてくると非常に便利で、Vanilla JSのため陳腐化することの無い普遍的な技術なのでWebComponentsの使い手が増えることを祈っています。
また、本記事の説明で使用したソースはGIT上に公開していますので、ご興味があれば以下参照リンクからアクセスしてください。

# 本記事で使用したコード(GitHub)
https://github.com/NakayamaShota/local-sheet

# 参考リンク(WebComponentsのMDNリンク)
[WebComponents](https://developer.mozilla.org/ja/docs/Web/Web_Components)
[CustomElements](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_custom_elements)
[ShadowDOM](https://developer.mozilla.org/ja/docs/Web/Web_Components/Using_shadow_DOM)
[ESModule](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules)