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


はじめに

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

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

オフィシャルの material.ioGitHub にはメジャーな

  • material-components-android
  • material-components-ios
  • material-components-web
  • material-components-flutter

の他に

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

また router として、

を使わせてもらいます。

早速作ってみましょう。

準備

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

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

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

$ mkdir wc
$ cd wc
$ yarn init

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

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

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

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

作成

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

$ 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 を使うという選択肢があります。lit-html という HTML テンプレートライブラリをラップしたものです。

古いブラウザのサポート

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

最後に

lit-element を使うと、reactive になります。また別記事で紹介させてもらいたいと思います。