lit-html と ShadyCSS
lit-html が v0.9.0 で ShadyCSS をサポートするようになったということなので、いろいろと調べてみた。
TL;DR
- 現時点では、Shadow DOM のスタイルカプセル化に対応するには ShadyCSS を使う必要がある。
- lit-html 経由で ShadyCSS を使うと便利。
Shadow DOM の Polyfill
2018 年 2 月現在、各ブラウザの Shadow DOM v1 のサポート状況は次のようになっている。
- Chrome と Opera は済み 🙆
- Safari と iOS Safari は一部バグあり 🤔
- Firefox と Edge はまだ 🙅
(参考:https://caniuse.com/#feat=shadowdomv1 )
したがって、幅広いブラウザに対応するには、 現段階では Web Components の Polyfill (webcomponents.js
) の利用が前提となる。
Polyfill には webcomponents-lite.js
や webcomponents-sd-ce.js
などターゲットブラウザとサポートしたい仕様に応じていくつかのファイルが公開されているが、これらのうち「Shady DOM/CSS をサポート」となっているものには、実は「Shadow DOM のスタイルがカプセル化されない」という問題がある。(Shady CSS をサポートって書いてあるのに!!)
Shadow DOM のスタイルカプセル化とは
例えば <my-avatar>
という次のような Custom Element があったとする。
import { html, render } from 'lit-html';
class MyAvatar extends HTMLElement {
//...
_render() {
const template = html`
<style>
img {
border-radius: 50%;
border: 1px solid #ccc;
}
</style>
<img src="${this.src}" width="160" height="160" />
`;
render(template, this.attachShadow({ mode: 'open' }));
}
}
element.attachShadow()
で Shadow DOM を生成しているので、ここで定義した <img>
のスタイルは Shadow DOM 内にのみ適用される = カプセル化されているというのが期待する挙動だ。
それを確認するために、次のような HTML を用意してみる。
<img src="https://randomuser.me/api/portraits/lego/7.jpg" width="160" />
<my-avatar src="https://randomuser.me/api/portraits/lego/7.jpg"></my-avatar>
Chrome 64 と Firefox 58 でそれぞれ確認した結果がこれ。
Chrome では、期待どおり <my-avatar>
にだけスタイルが適用されているのがわかる。
しかし、Firefox の場合、<my-avatar>
で定義したスタイルが通常の <img>
にも適用されてしまっている。Shadow DOM 内のスタイルが外に漏れ出しているのだ。逆に Shadow DOM の外で定義されたスタイルも Shadow DOM 内を汚染することになる。
ShadyCSS とは
この問題を解決するためのライブラリ(Polyfill)が ShadyCSS だ。「スタイルのカプセル化」をサポートしたい場合、webcomponents.js
の Polyfill に加えて、ShadyCSS も使う必要がある。
https://github.com/webcomponents/shadycss
ShadyCSS の処理を通すと、Firefox などのブラウザでは、上記の <my-avatar>
のコードが次のように擬似的な Scoped Style に変換される。
<head>
<style scope="my-avatar">
img.kz-avatar {
border-radius: 50%;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<kz-avatar src="https://randomuser.me/api/portraits/lego/7.jpg">
<img class="style-scope kz-avatar" src="https://randomuser.me/api/portraits/lego/7.jpg" height="160" width="160" />
</kz-avatar>
</body>
ただ、Usage にあるように素で使おうとすると結構めんどくさいコードを書く必要がある。
でも、lit-html 経由で ShadyCSS を利用するととても簡単に書くことができる。
lit-html で ShadyCSS を使う
というわけで、ようやく本題。
lit-html で ShadyCSS を利用するには、 lit-html
の render()
の代わりに lit-html/lib/shady-render.js
の render()
を使う。
import { html, render } from 'lit-html/lib/shady-render.js';
class MyAvatar extends HTMLElement {
static get is() {
return 'my-avatar';
}
//...
_render() {
const template = html`
<style>
img {
border-radius: 50%;
border: 1px solid #ccc;
}
</style>
<img src="${this.src}" width="160" height="160" />
`;
render(template, this.attachShadow({ mode: 'open' }), MyAvatar.is);
}
}
window.customElements.define(MyAvatar.is, MyAvatar);
lit-html
の render()
との違いは、第三パラメータにスコープ名を渡す必要がある点。ここで渡した値が <style>
の scope 属性にセットされる。これは、タグ名と一致している必要がある。いずれにせよ、最小限のコードで ShadyCSS が使えるのはうれしい。
ただし、制約もあって、例えば、
const template = html`
<style>
img {
width: ${this.size}px;
}
</style>
<img src="${this.src}" width="160" height="160" />
`;
というように、 <style>
の中で変数を展開しようとすると、
<style scope="my-avatar">
img.kz-avatar {
width: <!-- {
lit-8364277839110217
}
}
</style>
というように変換されてしまってうまくいかない。こういうユースケースに対応したいならば、おとなしく Polymer などのライブラリを利用するほうがよいだろう。
まとめ
というわけで、Firefox と Edge が Shadow DOM をサポートするまでは、lit-html + ShadyCSS の組み合わせが有力な選択肢になりそう。