Web Componentsを使いやすくする kit.js

## はじめに

JavaScriptにはWeb Componentsという機能が存在します。
標準化されたコンポーネントは、個人的に欲しかったのでよく使ってますが、
機能が不足していたり、冗長な書き回しになったり、少し不便です。

そこでWeb Componentsを使いやすくするためのプログラムを作ったので、その紹介です。


## kit.jsの使い方

1. ``kit.js``をモジュールとして読み込む
2. クラスのコンストラクタ等で``kit(this)``を実行する

```html
<script type="module">
import kit from 'https://cdn.jsdelivr.net/gh/worldace/kit/kit.js'

class SampleElement extends HTMLElement{

    constructor(){
        super()
        kit(this) // これだけ
    }

    html(){
        return `<h1>タイトル</h1><p>本文</p>`
    }
}
customElements.define('sample-element', SampleElement)
</script>

<sample-element></sample-element>
```

基本的な使い方はこれだけです。
次からは``kit.js``の機能について説明してきます。

## HTMLの自動登録

- クラスの``html()``が返すテンプレートが、自動的にShadowDOMに登録されます
- テンプレートは**HTML文字列**・**DOM**・**仮想DOM**の3種類に対応


```js:テンプレートがHTML文字列の場合
class SampleElement extends HTMLElement{

    html(){
        return `
          <h1>タイトル</h1>
          <p>本文</p>
        `
    }

    constructor(){
        super()
        kit(this)
    }
}
```

```js:テンプレートをtemplateタグに書く場合
class SampleElement extends HTMLElement{

    html(){
        return document.querySelector('template') // JavaScript内にHTMLを書かずに済む
    }

    constructor(){
        super()
        kit(this)
    }
}
```

※仮想DOMについては後述

## CSSの自動登録

- クラスの``css()``が返す文字列が、CSSとしてShadowDOMに登録されます
- CSSファイルに書くのと同じ要領で書けます

```js
class SampleElement extends HTMLElement{

    html(){
        return `<h1>タイトル</h1><p>本文</p>`
    }

    css(){
        return `
            h1{color:blue;}
            p{color:red;}
        `
    }

    constructor(){
        super()
        kit(this)
    }
}
```

- ``css()``の結果は再利用され、adoptedStyleSheetsに登録されるので効率的です。(Safari除く)
- ``html()``のテンプレート内に&lt;style&gt;タグを書くことも可能ですが、生成毎に処理が走るので非効率。


## イベントの自動登録

クラスに``$ID名_イベント名``という名前のメソッドがあると、イベントとして登録されます

```js
class SampleElement extends HTMLElement{

    constructor(){
        super()
        kit(this)
    }

    $title_click(event){ // IDが title のタグに click イベントを登録
        this.$.result.textContent = 'タイトルがクリックされました'

        // イベント解除例
        // this.$.title.removeEventListener('click', this.$title_click)
    }

    html(){
        return `<h1 id="title">タイトル</h1> <p id="result"></p>`
    }
}
```
* イベント内の``this``は固定の安心設計。(発生源はevent.targetで取得)
* 特別なIDが登録先として使えます。例えば"$_click"なら``shadowRoot``に登録されます

| 特別なID | 登録先DOM |
|:-----------|:------|
| $       | this.shadowRoot|
| $Host   | this  |
| $Window  | window    |
| $Document | document  |
| $Body   | document.body   |

## HTML選択の短縮化


HTMLを選択するには``querySelector()``を使いますが、冗長なのでjQuery風の短縮構文があります。

```js
//単数選択
this.$('セレクタ') // this.shadowRoot.querySelector('セレクタ') の短縮形

//複数選択はセレクタの頭に*を付ける。戻り値は配列
this.$('*セレクタ')
```

IDが付いてるタグには、さらに短くアクセスできます。

```js
this.$.ID名 // this.shadowRoot.querySelector('#ID名') の短縮形
```

:::note
LightDOMでは、IDが付いてるタグには``window.ID名``でアクセス可能です。
既存プロパティと重複しないように、ID名を **$** から始めるのがベストプラクティス。
:::


## 仮想DOMの使用

テンプレートには仮想DOMを使うこともできます。使い方は
* ``html()``は``kit``タグ付きテンプレートか配列を返す
* 描画したい時に``kit(this)``を実行する 

文法はReact風で、直感的に書けるように<a href="https://github.com/developit/htm" target="_blank">htm</a>と<a href="https://github.com/preactjs/preact" target="_blank">preact</a>を採用。
注意点としてタグは必ず閉じてください。**単独タグでも閉じて**ください。``<br />``

### 仮想DOMのサンプル

```js:カウンタ
class SampleElement extends HTMLElement{
    constructor(){
        super()
        this.count = 0
        kit(this) // 初回は全描画される
    }

    click(event){
        this.count++
        kit(this) // 2回目からは差分描画
    }

    html(){
        return kit`<button onclick=${this.click}>＋</button> <p>${this.count}</p>`
    }
}
```


```js:フォームに入力されたものを表示する
class SampleElement extends HTMLElement{
    constructor(){
        super()
        this.result = '未入力'
        kit(this) // 全描画
    }

    input(event){
        this.result = event.target.value
        kit(this) // 差分描画
    }

    html(){
        return kit`<input oninput=${this.input} /> <p class=abc>${this.result}</p>`
    }
}
```

```js:配列を使い、リストを動的に追加する
class SampleElement extends HTMLElement{
    constructor(){
        super()
        this.list = [Date.now()] // 現在の時間
        kit(this)
    }

    click(event){
        this.list.push(Date.now())
        kit(this)
    }

    html(){
        return kit`
            <button onclick=${this.click}>追加</button>
            <ol>
              ${this.list.map(v => kit`<li>${v}</li>`)}
            </ol>
     `}
}
```


```js:二重ループ
class SampleElement extends HTMLElement{
    constructor(){
        super()
        this.table = [
            ['バナナ', '200円'],
            ['リンゴ', '100円'],
        ]
        kit(this)
    }

    html(){
        const tr = []
        for(const row of this.table){
            tr.push(kit`<tr>${row.map(v => kit`<td>${v}</td>`)}</tr>`)
        }
        // one-liner
        // this.table.map(row => kit`<tr>${row.map(v => kit`<td>${v}</td>`)}</tr>`)

        return kit`<table>${tr}</table>`
    }

}
```
## 作成事例


![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70356/2384e1c7-4968-3e9c-0153-bb2bf1391aad.png)

-------

![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70356/e38d5eea-5efb-41fd-dfd2-1c53f8b2a2b6.png)

-------

![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/70356/74891995-3bd9-4536-4296-51fbe352e939.png)