WebComponents だけでアプリの骨格を作ってみる

# はじめに

WebComponents、何がいいかというとブラウザでネイティブに対応してるとこですね。

そろそろ本家、3rdパーティーともライブラリが充実してきて、Vue とか React とかのフレームワークを使わずに WebComponents だけでいけそうな感じになってきたので、とりあえず下のようなアプリの骨格を作って検証してみます。

![画面収録-2019-10-28-13.05.57.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73493/d89c56e4-44db-251a-ae03-1a3119249cd8.gif)


オフィシャルの [material.io](https://material.io) の [GitHub](https://github.com/material-components) にはメジャーな

* material-components-android
* material-components-ios
* material-components-web
* material-components-flutter

の他に

* [material-components-web-components ](https://github.com/material-components/material-components-web-components)

ってのがあります（名前が非常にまぎらわしい＞＜）。GUI コンポーネントとして今回これを使わせてもらいます。

また router として、

* [VAADIN ROUTER A CLIENT-SIDE ROUTER FOR WEB COMPONENTS](https://vaadin.com/router)

を使わせてもらいます。

早速作ってみましょう。


# 準備

### node, npm をダウンロードします

https://nodejs.org
この記事は node v12.4.0, npm 6.12.0 で検証しています

### ディレクトリを掘って cd して node のプロジェクトを作成します。

```terminal
$ mkdir wc
$ cd wc
$ yarn init
```

### es-dev-server をインストールして動かしておきます。

```terminal
$ npm install -g es-dev-server
$ es-dev-server --node-resolve --watch
```

8000 番のポートが使われてなければ
`es-dev-server started on http://localhost:8000`
と表示されます。ブラウザでここにアクセスしてください。

--node-resolve オプションが何故必要かについて知りたい場合は以下を参照してください。

https://github.com/material-components/material-components-web-components/blob/master/README.md

# 作成

### 使うコンポーネントを登録

```terminal
$ yarn add @material/mwc-drawer
$ yarn add @material/mwc-icon-button
$ yarn add @material/mwc-top-app-bar
$ yarn add @vaadin/router
```

### メイン画面に表示する WebComponent を作成

``` Main.js
class
Main extends HTMLElement {
	constructor() {
		super();
		this.attachShadow( { mode: 'open' } ).innerHTML = '<div>MAIN</div>'
	}
}
customElements.define( 'jp-main', Main )
```

``` About.js
class
About extends HTMLElement {
	constructor() {
		super();
		this.attachShadow( { mode: 'open' } ).innerHTML = '<div>ABOUT</div>'
	}
}
customElements.define( 'jp-about', About )
```

### まとめあげ

``` index.html
<!doctype html>
<html lang=zxx>
	<title>drawer demo</title>

	<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500" rel="stylesheet">
	<link href="https://fonts.googleapis.com/css?family=Material+Icons&display=block" rel="stylesheet">

<body>
	<script type="module">
		import '@material/mwc-drawer'
		import '@material/mwc-top-app-bar'
		import '@material/mwc-icon-button'
		import './Main'
		import './About'

		import { Router } from '@vaadin/router'

		class
		App extends HTMLElement {
			constructor() {
				super();
				this.attachShadow( { mode: 'open' } ).innerHTML = `
					<mwc-drawer hasHeader type="dismissible">
						<span slot="title">Drawer Title</span>
						<span slot="subtitle">subtitle</span>
						<div>
							<p>Drawer content</p>
							<a href="/">Main</a><br />
							<a href="/About">About</a><br />
						</div>
						<div slot="appContent">
							<mwc-top-app-bar>
								<mwc-icon-button slot="navigationIcon" icon="menu"></mwc-icon-button>
								<div slot="title">Title</div>
							</mwc-top-app-bar>
							<div id="outlet" />
						</div>
					</mwc-drawer>
				`

				const router = new Router( this.shadowRoot.getElementById( 'outlet' ) )
				router.setRoutes(
					[	{ path: '/'			, component: 'jp-main'	}
					,	{ path: '/about'	, component: 'jp-about'	}
					]
				)

				const drawer = this.shadowRoot.querySelector( 'mwc-drawer' )
				drawer.addEventListener(
					'MDCTopAppBar:nav'
				,	_ => drawer.open = !drawer.open
				)
			}
		}
		customElements.define( 'jp-app', App )

	</script>
	<jp-app />
</body>
```

# 勘所

### navigationIcon

mwc-top-app-bar の navigationIcon スロットに入れられたボタンが押されると、mwc-drawer に対して MDCTopAppBar:nav イベントが送られます。

### shadowRoot

WebComponent は constructor の中で attachShadow ってやると、shadowRoot が使えるようになります。プライベートな DOM みたいなものです。これを実際の DOM に connect して表示するようになります。
constructor の中ではまだ実際の DOM にコネクトされていないので、shadowRoot に対して getElement...　とか querySelector とか発行して、エレメントをとります。

shadowRoot に対して、今回の記事では innerHTML をスタティックテキストでセットしているだけですが、実際にはダイナミックに生成したい場合が多いと思われます。そんなときは polymer の [lit-element](https://lit-element.polymer-project.org) を使うという選択肢があります。lit-html という HTML テンプレートライブラリをラップしたものです。

# 古いブラウザのサポート

Edge とか IE11 とかをサポートするにはポリフィルが必要です。詳しくは以下を参照してください。

https://github.com/material-components/material-components-web-components/blob/master/README.md

# 最後に

[lit-element](https://lit-element.polymer-project.org) を使うと、reactive になります。また別記事で紹介させてもらいたいと思います。