Polymer1.0におけるStylingとCSS Variables


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
<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の仕様です)

```html
<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>
```

![image](https://qiita-image-store.s3.amazonaws.com/0/20253/3d90f091-abd9-1d02-a6a1-00f7fb93d4ce.png "localhost_63342_polymer-angular_styling_html.png")

## 分散ノード
Custom Elementsのテンプレート内に書かれる子要素ではなく、使用される際に挿入される要素を分散ノードと呼びます。

```html:分散ノードの例
<my-element>
  <p>Content</p>
</my-element>
```

しかし前項の`<my-element>`には分散ノードを表示する仕組みがないので、これは何も変化しません。
分散ノードをCustom Elementsで使うには`<content>`タグを使用します。

```html
  <template>
    <div id="child-element">In local DOM!</div>
    <content></content>
  </template>
```

これで分散ノードがテンプレートの一部として描画されるようになりました。

![image](https://www.evernote.com/l/AleunL8_eKVCfbAQe0Eyk-NjqDUA53Kni8QB/image.png)

このcontentはCustom Elementsの一部として描画されますが、属しているDOMはグローバルなので、Local DOMのCSS Scopeの適用外です。

```html:my-element.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 id="child-element">In local DOM!</div>
    <content></content>
  </template>

  <script>
    Polymer({
      is: 'my-element'
    });
  </script>
</dom-module>
```

```html:index.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"/>
</head>
<body>
  <my-element>
    <div>Inner Content</div>
  </my-element>
</body>
</html>
```

![image](https://www.evernote.com/l/AlceJb_t91dHzIS1VDJUa_nzf10ZTFI7c_4B/image.png)


Polymer 0.5では分散ノードに対してStylingを行うためには`::content`セレクタを使いましたが、Polymer 1.0が使用しているShady DOMでは`::content`擬似要素はレンダリング時に削除されてしまうため、`::content`によるStylingはできなくなりました。分散ノードへLocal DOM側からStylingするには何かしらのラッパー要素を定義する必要があるでしょう。

```html
  <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>
```

![image](https://www.evernote.com/l/AlfF_FvCXelHT5PIFtHHaChgvuwXLGK1Ea0B/image.png)

# 外部からの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よりも **低い** 優先度で内部のレンダリングに影響します。

```html:index.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"/>
  <style>
    div, p {
      color: orange;
    }
  </style>
</head>
<body>
  <my-element>
    <div>Light Div</div>
    <p>Light Paragraph</p>
  </my-element>
</body>
</html>
```

```html:my-element.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>
```

![image](https://www.evernote.com/l/AlcoyKLNQ69DDqcM6y8svXTzVcWXKaeok2AB/image.png)

本来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`はデフォルト値です)

```html:index.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"/>
  <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>
```

```html:my-element.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>
```

![image](https://www.evernote.com/l/AlcWjnowH6ZK_q0ypVHbwP0v9O52XEiMWfoB/image.png)

`<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()`関数で変数を呼び出します。

```html:index.html
  <style is="custom-style">
    div, p {
      color: orange;
    }

    :root {
      --my-element-div-theme: {
         color: blue;
       };
    }
  </style>
```

```my-element.html
  <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>`タグを使います。

```html
<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>
```

```css
: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を読み込みます。

```html:index.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>
```

```html:my-styling.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飛ばしてあげればなおよし)