laco0416です。Polymer 1.0におけるShadow DOMとStyling、CSS Variablesの話をします。
Shadow DOM
PolymerはWebComponentsの三本柱の1つ、Shadow DOMを用いることで、Custom Elements内外のDOMを隔離します。隔離されたCustom Elements内のDOMを Local DOM と呼びます。
Local DOMを生成するのにPolymerは2つの実装を使い分けることができます。基本的にはPolymerに含まれる Shady DOM というShadow DOM実装を使用しますが、ユーザーが明示的に指定することでブラウザにビルトインのShadow DOM実装を使ってLocal DOMを生成することができます。(将来的にはデフォルトがビルトイン実装の使用になる予定です)
ビルトイン実装を使おうとして見つからなかった場合は自動でShady DOMに切り替えられます。しかし、基本的にはこの2つの実装の違いをユーザーが気にする必要はありません。
<html>
<head>
<meta charset="utf-8">
<script src="components/webcomponentsjs/webcomponents-lite.js"></script>
<script>
//明示的にビルトインShadow DOM実装を指定する
window.Polymer = window.Polymer || {};
window.Polymer.dom = 'shadow';
</script>
<!-- import a component that relies on Polymer -->
<link rel="import" href="elements/my-app.html">
</head>
<body>
Local DOMはCSSのScopeを作り、内部からのStylingの漏洩を防ぐことができます。この恩恵によりどこで再利用しても外部のアプリケーションに影響を与えないコンポーネントを作成することができます。これはコンポーネントに更新があってもアプリケーションを壊さないということが保証されるWebComponentsの重要なファクターです。
Local DOM内部でのStyling
まずLocal DOMをどのようにStylingするのかをまとめます。
Local DOMのStyling
Local DOM内部では<dom-module>
の子として<style>
タグを使うことでStyleを適用することができます。内部の子要素に関しては普通のCSSと同様に#child-element
のようにセレクタを使うことができます。<template>
の位置、つまりLocal DOM自身にStyleを適用するには特別な:host
セレクタを使います。(これはShadow DOMの仕様です)
<dom-module id="my-element">
<style>
:host {
display: block;
border: 1px solid red;
}
#child-element {
background: yellow;
}
</style>
<template>
<div id="child-element">In local DOM!</div>
</template>
<script>
Polymer({
is: 'my-element'
});
</script>
</dom-module>
分散ノード
Custom Elementsのテンプレート内に書かれる子要素ではなく、使用される際に挿入される要素を分散ノードと呼びます。
<my-element>
<p>Content</p>
</my-element>
しかし前項の<my-element>
には分散ノードを表示する仕組みがないので、これは何も変化しません。
分散ノードをCustom Elementsで使うには<content>
タグを使用します。
<template>
<div id="child-element">In local DOM!</div>
<content></content>
</template>
これで分散ノードがテンプレートの一部として描画されるようになりました。
このcontentはCustom Elementsの一部として描画されますが、属しているDOMはグローバルなので、Local DOMのCSS Scopeの適用外です。
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<dom-module id="my-element">
<style>
:host {
display: block;
border: 1px solid red;
}
div {
color: red;
}
</style>
<template>
<div id="child-element">In local DOM!</div>
<content></content>
</template>
<script>
Polymer({
is: 'my-element'
});
</script>
</dom-module>
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
<link rel="import" href="elements/my-element.html"/>
</head>
<body>
<my-element>
<div>Inner Content</div>
</my-element>
</body>
</html>
Polymer 0.5では分散ノードに対してStylingを行うためには::content
セレクタを使いましたが、Polymer 1.0が使用しているShady DOMでは::content
擬似要素はレンダリング時に削除されてしまうため、::content
によるStylingはできなくなりました。分散ノードへLocal DOM側からStylingするには何かしらのラッパー要素を定義する必要があるでしょう。
<style>
:host {
display: block;
border: 1px solid red;
}
.content-wrapper {
background: orange;
}
</style>
<template>
<div id="child-element">In local DOM!</div>
<div class="content-wrapper">
<content></content>
</div>
</template>
外部からのStyling
ところで、本来Shadow DOMに定められた仕様では、Shadow DOMは内からのCSSの漏洩だけでなく、外からの侵入も遮断します。Polymer 0.5ではこの仕様に則り、グローバルのDOM( Light DOM とも呼ばれる)からCustom ElementsのStyleを変更するには/deep/
セレクタや、::shadow
セレクタを使っていました。そのため、例えばTwitter BootstrapなどのテーマCSSはPolymerと同時に使うことが困難でした。
Polymer 1.0を開発するにあたり、開発チームはこれらの再実装を保留しています。複雑なCSSセレクタによる内部Styleの変更はコンポーネント内部の変更に対して脆弱であり、Polymerの実用性・有用性に悪影響があると判断されたためです。現在のPolymer 1.0では外部からのStylingの遮断を取り払い、 Local DOMは外部に対して暴露されています 。Light DOMでのStylingはLocal DOMよりも 低い 優先度で内部のレンダリングに影響します。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
<link rel="import" href="elements/my-element.html"/>
<style>
div, p {
color: orange;
}
</style>
</head>
<body>
<my-element>
<div>Light Div</div>
<p>Light Paragraph</p>
</my-element>
</body>
</html>
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<dom-module id="my-element">
<style>
:host {
display: block;
border: 1px solid red;
}
div {
color: red;
}
</style>
<template>
<div>Shadow Div</div>
<p>Shadow Paragraph</p>
<content></content>
</template>
<script>
Polymer({
is: 'my-element'
});
</script>
</dom-module>
本来Themingを行うのであればLight DOMのCSS ScopeはLocal DOMよりも上位でなければならないですが、現在の仕様では内部で決められているStyleに関しては変更できません。これは過渡的な処置であり、WebComponentsの仕様から外れていることも問題です。
CSS Variablesを用いた解決策
外部からLocal DOMよりも高い優先度でStylingしたい場合、コンポーネント開発者はCSS Variablesによる独自CSS変数を変更手段として用意するのが現在のスマートな手法です。
次の例では外部からCSS変数--my-element-div-color
を設定し、コンポーネント側でvar(--my-element-div-color, red)
によって呼び出しています。(red
はデフォルト値です)
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
<link rel="import" href="elements/my-element.html"/>
<style is="custom-style">
div, p {
color: orange;
}
:root {
--my-element-div-color: blue;
}
</style>
</head>
<body>
<my-element>
<div>Light Div</div>
<p>Light Paragraph</p>
</my-element>
</body>
</html>
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<dom-module id="my-element">
<style>
:host {
display: block;
border: 1px solid red;
}
div {
color: var(--my-element-div-color, red);
}
</style>
<template>
<div>Shadow Div</div>
<p>Shadow Paragraph</p>
<content></content>
</template>
<script>
Polymer({
is: 'my-element'
});
</script>
</dom-module>
<style is="custom-style">
はとても重要な宣言です。Custom Elementsではない場所(=<dom-module>
外)でPolymerによるCSS Variablesのshimを使うためにはこの宣言が必須です。逆に言えば、Custom Elementsの中では暗黙的にCSS Variablesを使うことができます。
この例ではルートセレクタ:root
を用いています。このセレクタはそのドキュメントの最上位ルートのスコープでStyleを設定します。CSS VariablesによるThemingを行うのであれば:root
を使うのがベターでしょう。もちろんmy-element
をセレクタに指定しても大丈夫です。
Mixins
PolymerのCSS Variables shimでは独自機能としてCSS VariablesのMixinが可能です。個別の変数をvar()
で呼び出すのではなく、ひとかたまりのStyleを適用することができます。
次の例ではさっきと同じStylingをMixinを用いて行っています。Mixinを作成するには変数名でネストされたStyleを記述し、利用する側は@apply()
関数で変数を呼び出します。
<style is="custom-style">
div, p {
color: orange;
}
:root {
--my-element-div-theme: {
color: blue;
};
}
</style>
<style>
:host {
display: block;
border: 1px solid red;
}
div {
@apply(--my-element-div-theme)
}
</style>
外部CSSファイルを読み込む
ここまでのStylingはすべて<style>
タグをドキュメントやコンポーネントに直接書いていましたが、実際に開発を行う際はCSSファイルは分離していたほうがバージョン管理やデザイナーとの分業を考えた時に便利です。
Custom ElementsからCSSファイルを読み込む
Custom Elementsから外部のCSSファイルを読み込むには<link rel="import" type="css" href="my-element.css">
のように、HTML Importsと同じように<link>
タグを使います。
<link rel="import" href="../bower_components/polymer/polymer.html"/>
<dom-module id="my-element">
<link rel="import" type="css" href="my-element.css"/>
<template>
<div>Shadow Div</div>
<p>Shadow Paragraph</p>
<content></content>
</template>
<script>
Polymer({
is: 'my-element'
});
</script>
</dom-module>
:host {
display: block;
border: 1px solid red;
}
div {
@apply(--my-element-div-theme)
}
外部CSSの中であっても呼び出し先がLocal DOMであればそのままCSS VariablesもMixinsも使用可能です。
ドキュメントからCSS Variablesを用いたスタイルシートを読み込む
Light DOM(index.html)へCSSを読み込むには普通<link rel="stylesheet" href="style.css"/>
のように書きますが、Light DOMにはCSS VariablesやMixinのshimは適用されていません(そのため先述のis="custom-style
が存在する)。
Light DOMでCSS Variablesを外部ファイル化するにはHTML Importsを利用し、<style is="custom-style">
をルート要素とするHTMLを読み込みます。
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<script src="bower_components/webcomponentsjs/webcomponents.min.js"></script>
<link rel="import" href="elements/my-element.html"/>
<link rel="import" href="my-styling.html"/>
</head>
<body>
<my-element>
<div>Light Div</div>
<p>Light Paragraph</p>
</my-element>
</body>
</html>
<style is="custom-style">
div, p {
color: orange;
}
:root {
--my-element-div-theme: {
color: blue;
};
}
</style>
まとめ
コンポーネント開発者がStylingに関してやるべきこと
- CSS Variablesを使って外部からのStylingを可能な窓口を作る
- どうしても絶対に変えられたくない部分に関してはハードコーディングする
- CSS変数の命名規則を統一し、ユーザーがドキュメントを読まなくても推測しやすいようにする(
--<要素名>-<要素|クラス>-<プロパティ>
がスタンダードになりそう) - コンポーネントのCSS変数のドキュメントを整備する(リポジトリのREADMEなど)
コンポーネント利用者がStylingに関してやるべきこと
- ドキュメントを読んで使えるCSS変数を知る
- ソースを読んで使えるCSS変数を知る
- 変更できてしかるべきプロパティがハードコーディングされていたら開発者を殴りに行くか、フォークして自分でカスタマイズする(PR飛ばしてあげればなおよし)