溝畑です。
随分間が空いてしまいました。前回は去年のVue Fesの投稿・・・。
今年のVue Fesは台風で中止になっちゃいましたね。。。仕方ない。
Qiita Teamにはやたら落書きしてたり、個人ブログにはちょいちょい書いてるんですけど、こっちには書かずでした。
Web Components
さて、本題。Web Components使ってますか?
自分もやっと触ろうかなーと言ったレベルで、今回触ってみたまでです。
そもそもWeb Componentsって何ですか?という方向けに。
こんなこと思ったことないですか?
・自作タグ作ってHTML書けたらなー!
・HTML書きまくると読みにくいし、もうしんどいー嫌やー!
読みにくいかどうかは書き手の問題感が否めないですが、それは置いておいて。
こんな思いに応えてくれるのが「Web Components」です。
詳しいことはこっち読んでもらって↓
Web Components | MDN
Web Components は、再利用可能なカスタム要素を作成し、ウェブアプリの中で利用するための、一連のテクノロジーです。
こういうことです。
JSのフレームワーク触ってる方なら馴染み深い、Componentを作って組み合わせるあの感覚です。
あれに近いことができるよ!というイメージが近いと思います。
身近で使われているところをあげると、Youtubeなんかではバリバリ使われてます。
要素の検証なりしてみると、ytb-xxxx、yt-xxxxみたいなタグが見えるはず。
Web Componentsの使い方(1)
基本的な使い方から見てみましょう。<kp-link></kp-link>というタグで、弊社のHPへのリンクを表示するものを作ってみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class KPLink extends HTMLElement { constructor() { super() const shadowRoot = this.attachShadow({ mode: 'open' }) shadowRoot.innerHTML = ` <style> a { background: steelblue; color: white; padding: 10px; border-radius: 4px; } </style> <a href="http://www.key-p.com/" target="_blank">キー・ポイントのHPはここをクリック!</a> ` } } customElements.define('kp-link', KPLink) |
1 |
<kp-link></kp-link> |
これで、↓みたいなのが表示されます。
まず、classを作って、HTMLElementを継承。
constructorではthis.attachShadow({ mode: 'open' })として、shadowRootを作ります。
これが作るComponentの根本になります。
あとはshadowRootのinnerHTMLにHTMLとスタイルを定義。
最後にcustomElements.defineで使うタグ名と作ったクラスを入れて完了。
これで使えるようになります。
意外と簡単。
さらっと出てきているattachShadowのmodeですが、openとclosedがあります。
違いは「外部のJavaScriptからアクセスできるかどうか」です。
外から触られたくないわー!ならclosedってな感じでしょうか。
Web Componentsの使い方(2)
(1)だけだと、中で書いたHTMLしか表示できません。例えば、タブなんかを作るときはタブのタイトル部分とコンテンツ部分は外部から挿入したいはずです。
そういった場合は<slot></slot>を使うと解決できます。
(Vueにもslotありますね)
次は<my-tab></my-tab>で簡単なタブを表示してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
class MyTab extends HTMLElement { constructor() { super() const shadowRoot = this.attachShadow({ mode: 'open' }) this.selectedTabIndex = 0 this.tabTitleNodes this.tabContentNodes const style = ` <style> #tab-nav { display: flex; } #tab-nav ::slotted(*) { padding: 5px; border: 1px solid #ccc; border-top-left-radius: 5px; border-top-right-radius: 5px; cursor: pointer; } #tab-nav ::slotted(.selected) { background: #454545; color: white; } #tab-nav ::slotted(:hover) { background: #ccc; } #tab-content ::slotted(*) { border: 1px solid #ccc; padding: 5px; display: none; } #tab-content ::slotted(.active) { display: block; } </style> ` const tabTemplate = ` <div> <div id="tab-nav"> <slot name="tab-title"></slot> </div> <div id="tab-content"> <slot name="tab-content"></slot> </div> </div> ` shadowRoot.innerHTML = style + tabTemplate } connectedCallback() { const slot = this.shadowRoot.querySelectorAll('slot') this.tabTitleNodes = slot[0].assignedNodes() this.tabContentNodes = slot[1].assignedNodes() this.tabTitleNodes.forEach(($elm, i) => { $elm.addEventListener('click', e => { this.selectedTabIndex = i this.selectTab() }) }) this.selectTab() } selectTab() { this.tabTitleNodes.forEach(($elm, i) => { if (this.selectedTabIndex === i) { $elm.classList.add('selected') } else { $elm.classList.remove('selected') } }) this.tabContentNodes.forEach(($elm, i) => { if (this.selectedTabIndex === i) { $elm.classList.add('active') } else { $elm.classList.remove('active') } }) } } customElements.define('my-tab', MyTab) |
1 2 3 4 5 6 |
<my-tab> <div slot="tab-title" id="tab1">Tab1</div> <div slot="tab-title" id="tab2">Tab2</div> <div slot="tab-content" id="tab-content1">Tab Content1</div> <div slot="tab-content" id="tab-content2">Tab Content2</div> </my-tab> |
これでこんな感じのタブが表示されます。
ちょっと長いのと、多分もっとスッキリ書けそうですが・・・。
ざっくりやってることだけ。
全体的な流れは同じです。
selectedTabIndexが選択中のタブのindex。
tabTitleNodes、tabContentNodesにはそれぞれのNodeを入れてます。
はじめにstyleを見てみると、見慣れない::slottedという疑似要素を使っています。
その名の通り、slotで放り込まれた要素に対してスタイルをあてることができます。その他hostなんかもありますが、調べてみてください!
次にconnectedCallback。
いわゆる、ライフサイクルメソッドです。(React, Vue, Angularなんかでも、Componentのライフサイクルってありますよね)
これは、要素がDOMに追加されたら走ります。
最後にassignedNodes()。
slotで追加された要素を引っ張り出せます。
あとは、tabTitleにclickイベントを仕込んで、selectTabでclassListを操作しています。
ちょっとややこしいですかね。。。
とはいえ、これで外部から挿入する方法が分かりました。
さいごに
これだけだとまだ不安はありますが、しっかりやればUIライブラリ的なのが作れる!?とにかく、複雑なHTMLやCSSを中に閉じ込めることができるので、冒頭で書いたような「読みにくいし」といったことも軽減できます。
例えば、弊社のWebFileなら<wf-button>みたいなイメージでやると、サービス内ではそれを使っておけば、誰がやっても見た目に関する揺れも少なく済みそうです。
これからWeb Componentsが浸透していくのでしょうか。楽しみです。