StencilJSで作成したComponentをPublishして他から使ってみる

## 概要

汎用的に使えるComponent作りたい...作りたくない？ってことでStendilJSでCustom Componentを作って他の環境（CDNやReact等）から使ってみようと思います。

この記事を書いている時点で使用している@stencil/coreのバージョンは1.8.4になります。

## StencilJS is 何？

[公式サイト](https://stenciljs.com/)に以下の記載があります。

> Stencil is a toolchain for building reusable, scalable Design Systems. Generate small, blazing fast, and 100% standards based Web Components that run in every browser.

Web Componentを作るビルドシステムって感じですかね。実際にはStencilJS自体が持っているルーティングやIonicのルーティングを使ってアプリも作れるっぽいです。今回はComponentを作ることにフォーカスして進めていこうと思います。

### Polymer3とかじゃダメなの？

ダメじゃないです。Polymerも使えた方が良いとは思いますが、個人的に以前ちょっと触ったことがあるので今回はStencilJSでやっていきます。

## 実装

### 作るもの

まず何を作るかを決めておきましょう。お試しレベルなので簡単なものが良いですね...入力と出力があるやつ...という事で「テキストボックスに文字を入力したら入力した文字がReverseされて表示されるコンポーネント」を作っていこうと思います。使うシーンが無い気がしますが、良いんです、お試しだから。

### セットアップ

```
~$ npx init stencil

? 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
```

starterはcomponentを選択します。project nameはstencil-sampleとでもしておきましょう。confirmでyをタイプするとセットアップを開始してくれます。セットアップが完了したら表示通り以下を実行しましょう。

```
We suggest that you begin by typing:

 $ cd stencil-sample
 $ npm start
```

Port3333で「Hello, World! I'm Stencil 'Don't call me a framework' JS」と表示されたらとりあえずセットアップは完了です。  
npm testを実行するとjestやpuppeteerといったテストツールが追加でインストールされます（インストール直後の流れで走る一発目のテストはコケるっぽい？）。

### 最低限の大枠を作る

/src/componentsに作成Component用のディレクトリを追加します。今回は「reverse-text」というディレクトリを追加します。作成したディレクトリ内に「reverse-text.tsx」と「reverse-text.css」を追加します。  
追加したらreverse.tsxに以下のコードを記述します。

```ts
import { Component, h } from "@stencil/core";

@Component({
  tag: "reverse-text",
  styleUrl: "reverse-text.css",
  shadow: true
})
export class ReverseText {
  render() {
    return <div>reverse text</div>;
  }
}
```

reverse-text.cssには以下を追記します。

```css
div {
  background-color: #f5f5f5;
}
```

記述して保存したら/src/index.htmlのmy-componentタグの下にreverse-textタグを追加してnpm startをしてみましょう。画面にreverse textと追加で表示されていれば最低限の大枠は完成です。  
cssは対象コンポーネントにのみ反映されていることがわかりますね。

### HTML部分を整理する

テキストだけではなくちゃんと仕様を満たすHTMLにしていきます。

```ts
export class ReverseText {
  render() {
    return (
      <div class="wrapper">
        <input type="text" placeholder="ここに文字を入力する" />
        <label>ここにreverseされた文字列が表示される</label>
      </div>
    );
  }
}
```

とりあえずこんな感じですね。見た目の方はお好きに装飾してみてください。

### 仕様通り作っていく

今回作るものであれば入力文字列をstateとして持たせておいてreverseする処理をしてlabelに表示させたいですね。ということで[Internal state - Stencil](https://stenciljs.com/docs/state)を参考にしつつ実装していきます。  
ざっくりこんな感じでしょうか。

```ts
import { Component, State, h } from "@stencil/core";

@Component({
  tag: "reverse-text",
  styleUrl: "reverse-text.css",
  shadow: true
})
export class ReverseText {
  @State() message: string;
  /**
   * lifecycle events
   */
  componentWillLoad() {
    console.log("componentWillLoad");
  }
  componentDidLoad() {
    console.log("componentDidLoad");
  }
  componentWillUpdate() {
    console.log("componentWillUpdate");
  }
  componentDidUpdate() {
    console.log("componentDidUpdate");
  }
  componentDidUnload() {
    console.log("componentDidUnload");
  }

  constructor() {
    this.message = "";
  }

  handle(el: any): void {
    this.message = el.value;
  }

  strReverse(): string {
    return this.message.length > 0
      ? this.message
          .split("")
          .reverse()
          .join("")
      : this.message;
  }

  render() {
    return (
      <div class="wrapper">
        <input
          type="text"
          placeholder="ここに文字を入力する"
          value={this.message}
          onInput={event => this.handle(event.target)}
        />
        <label>{this.strReverse()}</label>
      </div>
    );
  }
}
```

うん、実用性は無いですね。実用性の高いComponentはPropを多く持ってそうな印象がありますが、Propの扱い方はセットアップ時に初期で用意されているmy-componentを参照して頂けたらと思います。  
おまけ程度にライフサイクルイベントメソッドを用意しておきました。適当に遊んでもらえたらと思います。

## 単体テスト

この程度であればテストコード不要だと思いますが、一応流れをなぞる意味でテストコードも書いていこうと思います。Jestをある程度触れる方はスキップしてもらって大丈夫です。  
ある程度込み入ったComponentはe2eの前にspec.tsで単体も書くのですが、今回は単体で何テストするの？って感じなので割愛します。単体テストコードのサンプルは/src/utils/utils.spec.tsを参照ください。  
もし時間があればstrReverseメソッドをutilsに移してutils.spec.tsにテストコードを追加してみるのも良いかも知れませんね。

## E2Eテスト

reverse-text.e2e.tsを追加してそれっぽく書いていきます。まずはレンダリングのテストを書いていきましょう。

```ts
import { newE2EPage } from "@stencil/core/testing";

describe("reverse-text", () => {
  it("renders", async () => {
    const page = await newE2EPage();

    await page.setContent("<reverse-text />");
    const element = await page.find("reverse-text");
    expect(element).toHaveClass("hydrated");
  });
});
```

コードを書いたらnpm testで動かしてみましょう。既存のテストコードと合わせて以下のような結果になればOKです！

```
PASS  src/utils/utils.spec.ts
PASS  src/components/reverse-text/reverse-text.e2e.ts
PASS  src/components/my-component/my-component.e2e.ts

Test Suites: 3 passed, 3 total
Tests:       7 passed, 7 total
Snapshots:   0 total
Time:        4.647s
Ran all test suites.
```

E2Eの詳細は[End-to-end Testing - Stencil](https://stenciljs.com/docs/end-to-end-testing)を参照ください。@stencil/core/testingはPuppeteerをラッピングして色々とAPIを提供してくれてるっぽいです。

## Publish

Publishの前にGitHubにPushしてnpmでサインアップしておきましょう。サインアップが完了したらローカルの作業ディレクトリでnpm adduserを実行してサインインしておきます。後はnpm publishをすれば公開完了です。CDNなら以下のようなURLで作ったComponentが利用可能です。

> https://unpkg.com/[project-name]@latest/dist/[project-name].js

簡単ですね！

## 終わり

1つ1つ細かく説明できる程スキルや知見があるわけではないので如何ともし難いところですが、こしょーもないPrivate Packageを量産して知見を貯めていこうかなと思います。ComponentはStencilJSで作ってプロジェクト自体は可能な限りクリーンに保つとか結構良いプラクティスになるんじゃないかなぁって。