Stencilを使ってWebComponentを作ってみる

**身内の勉強会用の資料です**
**ソースは[github](https://github.com/kuwabataK/Stencil-Calc)においてあります**

---

## WebComponentとは？

---

説明するよりもデモを見てもらったほうが早いと思うので、code penに簡単な電卓コンポーネント(ks-calc)を作りました。
ソースは[github](https://github.com/kuwabataK/Stencil-Calc)においてあります

---

<p class="codepen" data-height="265" data-theme-id="0" data-default-tab="html,result" data-user="kuwabatak" data-slug-hash="YMxZwN" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;" data-pen-title="YMxZwN">
  <span>See the Pen <a href="https://codepen.io/kuwabatak/pen/YMxZwN/">
  YMxZwN</a> by KuwabataK (<a href="https://codepen.io/kuwabatak">@kuwabatak</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

---

### 

コードは以下のような感じ

``` sample.html
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <!-- WebComponentを読み込み -->
  <script src="https://kuwabatak.github.io/Stencil-Calc/mycomponent.js"></script>
</head>
<body>
  
  <!-- Custom Elementを書く -->
  <ks-calc input="33-4"></ks-calc>

</body>
</html>

```

---

WebComponetのソースを`<script>`タグで読み込んで、使いたい場所にCustom Element(今回は`ks-calc`)を書くだけで、色んな人が作ったWebComponetを自分のサイト上で動かすことができます。

とても簡単

しかもフレームワーク非依存(VueでもReactでも、Vanila JS上でも動く)

---

WebComponet.orgとか見ると、色んな人が作ったWebComponentを探すことができます

https://www.webcomponents.org/

---

# Stencilとは？

ionic-teamが作っているWebComponentを作るためのフレームワーク(ではないらしい)。

ionic v4に組み込まれているのコンポーネントたちはこれで作られているらしい。
(なので実はionic v4のコンポーネントをVueやReact上で動かすこともできる）

[公式サイト](https://stenciljs.com/)

---

## StencilでWebComponentを作ってみる！！

注: npm v6以上が必要です

1 - プロジェクトの作成

```
$ npm init stencil 
```

2 - componentを選んでEnter

```
? Pick a starter › - Use arrow-keys. Return to submit.

   ionic-pwa     Everything you need to build fast, production ready PWAs
   app           Minimal starter for building a Stencil app or website
❯  component     Collection of web components that can be used anywhere
```

ちなみに今回のプロジェクトで使ったStencilのバージョンは`0.16.2`です

---

### プロジェクトの構成

---

```
.
├── LICENSE
├── package-lock.json
├── package.json
├── readme.md
├── src
│   ├── components
│   │   └── my-component
│   │       ├── my-component.css    // cssファイル
│   │       ├── my-component.e2e.ts // テストコード
│   │       ├── my-component.tsx    // コンポーネントファイル
│   │       └── readme.md
│   ├── components.d.ts  // コンポーネントの型定義ファイル。自動生成されるのでいじる必要なし
│   ├── index.html
│   └── utils
│       ├── utils.spec.ts
│       └── utils.ts
├── stencil.config.ts
└── tsconfig.json

```

---

### とりあえずビルドしてみる

---

```
$ npm start
```

なんか文字が出る

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/37a8cb75-f53d-75b2-6b7e-784d20651ff4.png)

私をFrameWorkと呼ばないで！！　とのこと
じゃあなんて呼べばいいんです？Stencilさん

---

### ソースを見てみる

---

``` index.html
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <title>Stencil Component Starter</title>
  <script src="/build/mycomponent.js"></script>

</head>
<body>

  <my-component first="Stencil" last="'Don't call me a framework' JS"></my-component>

</body>
</html>

```
---

index.htmlでは`my-component`というコンポーネントを呼び出しているようだ

---

``` my-component.tsx
import { Component, Prop } from '@stencil/core';
import { format } from '../../utils/utils';

@Component({
  tag: 'my-component',
  styleUrl: 'my-component.css',
  shadow: true
})
export class MyComponent {
  /**
   * The first name
   */
  @Prop() first: string;

  /**
   * The middle name
   */
  @Prop() middle: string;

  /**
   * The last name
   */
  @Prop() last: string;

  private getText(): string {
    return format(this.first, this.middle, this.last);
  }

  render() {
    return <div>Hello, World! I'm {this.getText()}</div>;
  }
}

```

---

* `@Props` で引数を受け取り、`render()`でDOMを返すみたい
* すごくReactっぽい。TSX使ってるし・・・(ionic-teamが作ったのにAngularじゃないんだ)
* でもデータバインディング周りは`@Event()`(Angularで言うところの`@Output`)とか用意されていて、Angularっぽくもある
* TypeScriptベースなのはすごく良さそう

---

### 簡単な電卓コンポーネントを作ってみる

実際に電卓コンポーネントを作っていく。とりあえず、内部の計算ロジックの方から書いていくことに。
`util.ts`がすでにあったので、ここに計算ロジックを書きます(引数チェックとか雑なのは許してください・・・)

---

``` util.ts

...追記

/**
 * 文字列をパースして計算します
 * 
 * @param input 例: '12+4'
 * @return output 例 16
 */
export function calc(input: string): number {

  const inputNums = input.split(/\+|\-|\*|\//).map(s => parseInt(s))
  const inputOpe = input.split(/\d+/).filter( s => s !== "")

  if (inputNums == null || inputOpe == null){
    return 0
  }
  if(inputNums.length <=1 || inputOpe.length <= 0 ){
    return 0
  }
  if(inputNums.length - inputOpe.length !== 1 ){
    return 0
  }

  return inputOpe.reduce((acc, cur, index) => {
    return _calc(acc, inputNums[index + 1], cur)
  }, inputNums[0])
}

function _calc(num1: number, num2: number, ope: string): number {
  switch (ope) {
    case '+':
      return num1 + num2
    case '-':
      return num1 - num2
    case '*':
      return num1 * num2
    case '/':
      return num1 / num2
  }
}

```

--- 

#### コンポーネントを作っていく

ざっと調べた感じ、ionic cliみたいなコマンド一発でcomponentを作ってくれるツールは公式には存在しないようなので、`my-component`のファイルをコピペしてもらってくることにする

---

```
./src/
├── components
│   ├── ks-calc
│   │   ├── ks-calc.css        // 追加
│   │   ├── ks-calc.tsx        // 追加
│   │   └── readme.md          // ビルドすると自動生成された
│   └── my-component
│       ├── my-component.css
│       ├── my-component.e2e.ts
│       ├── my-component.tsx
│       └── readme.md
├── components.d.ts
├── index.html
└── utils
    ├── utils.spec.ts
    └── utils.ts
```

---

ks-calcの中身を以下のように書き換え

``` ks-calc.tsx
import { Component, Prop, State, Event, EventEmitter } from '@stencil/core';
import { calc } from '../../utils/utils';

@Component({
  tag: 'ks-calc',
  styleUrl: 'ks-calc.css',
  shadow: true
})
export class KsCalc {
  /**
   * 外からの入力
   */
  @Prop() input: string = "12+4"

  /**
   * 内部で持っている入力の状態
   */
  @State() _input: string = "12+4"

  /**
   * 計算結果が変わったときに結果を返すEvent
   * Angularで言うところの '@Output'
   * 
   * @return {number} 計算結果 
   */
  @Event() changeResult: EventEmitter

  /**
   * コンポーネントロード時に、Prop()で渡された値をStateに設定
   */
  componentWillLoad() {
    return this._input = this.input
  }

  /**
   * 入力された文字列から計算結果を返す
   */
  private calcText() {
    return calc(this._input);
  }

  /**
   * inputの中身が変わったときに結果を再計算し、
   * changeResultイベントを発火して呼び出し元に伝える
   * 
   * @param event  
   */
  handleInputChange(event: Event) {
    this._input = event.target["value"]
    this.changeResult.emit(this.calcText())
  }

  render() {
    return <div>
      <input value={this._input} onChange={(event) => this.handleInputChange(event)} />
      <div>{this.calcText()}</div>
    </div>
  }
}
```

---

とりあえずこれでできたはずなので、index.htmlを以下のように書き換える

``` index.html
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <title>Stencil Component Starter</title>
  <script src="/build/mycomponent.js"></script>
</head>
<body>
  <!-- コンポーネントを呼び出し -->
  <ks-calc input="33-4"></ks-calc>
</body>
</html>

```

---

動かしてみる

```
$ npm start
```

うごいたー

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/15a07c5c-694d-fb95-0d87-34564599fbec.png)

---

### ビルドしてGithubPageでコンポーネントとして公開してみる

npmとかで公開もできますが、今回はテストなのでGithubPageにホストして公開することにします

---

作ったWebComponentをビルドする

```
$ npm run-script build
```

`dist`ディレクトリにビルド後のコンポーネントが吐き出される

---

Githubにリポジトリを作り、`docs`ディレクトリを作成。
`docs`ディレクトリの中に`dist`ディレクトリの中身を突っ込む

GitHubの`Setting`を開き、GitHubPageでdocsディレクトリを公開するように設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/c4f79deb-a1a6-7296-d513-9f93d1872356.png)

---

### 公開したWebComponentが使えるかどうか試してみる

---

以下のようなhtmlファイルを適当に作成してブラウザで開く

``` sample.html
<!DOCTYPE html>
<html dir="ltr" lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <title>Stencil Component Starter</title>
  <!-- 公開先のmycomponent.jsを指定 -->
  <script src="https://kuwabatak.github.io/Stencil-Calc/mycomponent.js"></script>
</head>
<body>
  <!-- 以下が今回作ったCustomElement -->
  <ks-calc input="33-4"></ks-calc>

</body>
</html>
```

---

先ほど見せたようにCode Penでも読み込んで使えるよ！

---

### おまけ

---

WebComponent内で発火して`@Event`経由で伝搬したイベントはDOMの標準APIで拾うことができるよ！

``` sample.html
<!DOCTYPE html>
<html dir="ltr" lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0">
  <title>Stencil Component Starter</title>
  <script src="/build/mycomponent.js"></script>
</head>

<body>
  <!-- コンポーネントを呼び出し -->
  <ks-calc input="33-4"></ks-calc>
  <!-- コンポーネントで発火したイベントを受け取るためのコード -->
  <script>
    window.addEventListener("load", (evt) => {
      const components = document.querySelectorAll("ks-calc");
      for (let comp of components) {

        // ks-calcコンポーネントで発火されたchangeResultイベントを受け取る事もできる
        comp.addEventListener("changeResult", (evt) => {
          console.log("計算結果は" + evt.detail);
        });
      }
    });
  </script>
</body>

</html>
```

---

## まとめ

---

* 思った以上に簡単にWebComponent作れる
* フレームワーク非依存なので、これでコンポーネント作れば再利用性高くなりそう
* ぱっと見Reactと思いきや、データバインディング周りがAngularな感じで面白い

みんなもこれでクソコンポーネント作ろうぜ！

---
## おわり