ここ1年ほど、アトミックデザインやコンポーネント指向を用いたウェブ開発において、いかにしてスタイルを親コンポーネントから子コンポーネントへと伝播させていくかを考えていた。そして、ようやく結論が出たので共有する。
(私はAngularしか用いていないが、おそらくReactやVue.jsでも考え方は役に立つはず...)
1.はじめに(直感的なアイディア)
この問題は単純化していけば、子コンポーネントとそれを内包する親コンポーネントの2つのコンポーネントにおいて、どうすれば直感的かつシンプルに値を渡していけるかという問題である。
これについて、私ははじめに、各コンポーネントの高さや幅、色の値等をCSS変数としてHostに定義し、それを子に渡していく方式を考えた。
:host {
--width: 300px;
--height: 200px;
--base-color: red;
}
:host {
display: flex;
width: var(--width);
height: var(--height);
outline: 1px var(--base-color);
}
:host {
--width: 1200px;
--height: 200px;
--base-color: blue;
}
:host {
display: flex;
width: var(--width);
height: var(--height);
outline: 1px var(--base-color);
* {
--width: calc(var(--width) / 2); // ERROR
--height: var(--height); // ERROR
--base-color: var(--base-color); // ERROR
}
}
一見、直感的に値を子に渡していけるように見えるが、この書き方では--width: var(--width)のように、同じセレクタ内で同名のCSS変数を用いた変数定義を行ってしまっているため、意図したとおりには動作しない。
2.Component Name + CSS Variable Name
そこでCSS変数にコンポーネント名をプレフィックスすることにした。
:host {
--child-width: 300px;
--child-height: 200px;
--child-base-color: red;
}
:host {
display: flex;
width: var(--child-width);
height: var(--child-height);
outline: 1px var(--child-base-color);
}
:host {
--parent-width: 1200px;
--parent-height: 200px;
--parent-base-color: blue;
}
:host {
display: flex;
width: var(--parent-width);
height: var(--parent-height);
outline: 1px var(--parent-base-color);
* {
--child-width: calc(var(--parent-width) / 2); // --child-width: 600px;
--child-height: var(--parent-height); // --child-height: 200px;
--child-base-color: var(--parent-base-color); // --child-base-color: blue;
}
}
これで上手く親コンポーネントから子コンポーネントに値を渡すことができるようになったが、再利用性の高いコンポネント指向においては、自身の用意したコンポーネント名と外部から取得したコンポーネント名が被る可能性がある。
3.Component Name + Suffix + CSS Variable Name
そこで、ランダムなidを生成し[コンポーネント名]-[ランダム英数]-[変数名]という形をとることにした。
:host {
--child-5ds3-width: 300px;
--child-5ds3-height: 200px;
--child-5ds3-base-color: red;
}
:host {
display: flex;
width: var(--child-5ds3-width);
height: var(--child-5ds3-height);
outline: 1px var(--child-5ds3-base-color);
}
:host {
--parent-94dm-width: 1200px;
--parent-94dm-height: 200px;
--parent-94dm-base-color: blue;
}
:host {
display: flex;
width: var(--parent-94dm-width);
height: var(--parent-94dm-height);
outline: 1px var(--parent-94dm-base-color);
* {
--child-5ds3-width: calc(var(--parent-94dm-width) / 2); // --child-width: 600px;
--child-5ds3-height: var(--parent-94dm-height); // --child-height: 200px;
--child-5ds3-base-color: var(--parent-94dm-base-color); // --child-base-color: blue;
}
}
これでまずCSS変数の名称がかぶって予期しない動作が発生することはなくなる。しかしながら、親コンポネントが複数の子コンポネント達(下記では--child-5ds3と--child-us1p)をもつ場合、それぞれに対して変数を上書きしていくのは非常に手間がかかる。
:host {
//...
* {
--child-5ds3-width: calc(var(--parent-94dm-width) / 2); // --child-width: 600px;
--child-5ds3-height: var(--parent-94dm-height); // --child-height: 200px;
--child-5ds3-base-color: var(--parent-94dm-base-color); // --child-base-color: blue;
--child-us1p-width: calc(var(--parent-94dm-width) / 2); // --child-width: 600px;
--child-us1p-height: var(--parent-94dm-height); // --child-height: 200px;
--child-us1p-base-color: var(--parent-94dm-base-color); // --child-base-color: blue;
}
}
4.Host CSS Variable
そこで、一度、シンプルな変数名(ex:--width)を:hostで定義し、それを先程定義したCSS変数(ex:--child-5ds3-width)に渡すことで、より直感的に親から子へと値を渡せるようにした。この一連の動作によって定義されるCSS変数を本稿では**「ホストCSS変数」**と呼ぶ。
:host {
--width: 300px;
--height: 200px;
--base-color: red;
--child-5ds3-width: var(--width);
--child-5ds3-height: var(--height);
--child-5ds3-base-color: var(--base-color);
}
:host {
display: flex;
width: var(--child-5ds3-width);
height: var(--child-5ds3-height);
outline: 1px var(--child-5ds3-base-color);
}
:host {
--width: 1200px;
--height: 200px;
--base-color: blue;
--parent-94dm-width: var(--width);
--parent-94dm-height: var(--height);
--parent-94dm-base-color: var(--base-color);
}
:host {
display: flex;
width: var(--parent-94dm-width);
height: var(--parent-94dm-height);
outline: 1px var(--parent-94dm-base-color);
* {
--width: calc(var(--parent-94dm-width) / 2); // --child-width: 600px;
--height: var(--parent-94dm-height); // --child-height: 200px;
--base-color: var(--parent-94dm-base-color); // --child-base-color: blue;
}
}
これによって、より直感的に子コンポーネントに値を渡すことができるようになったが、流石に冗長すぎる。
5.Host CSS Variable by SCSS
そこで、この**「ホストCSS変数」**をより効率的に書くため、以下のSCSSファイルを用意した。
@function trip-initial($str) {
@return str-slice($str, 2);
}
$str: "0123456789abcdefghijklmnopqrstuvwxyz";
$chars: ();
@for $i from 1 through 36 {
$char: str-slice($str, $i, $i);
$chars: append($chars, $char);
}
@function random-char($n: 1) {
$res: "";
@for $i from 1 through $n{
$c: nth($chars, random(36));
$res: $res + $c;
}
@return $res;
}
@function host($name, $suffix: "@@@@", $len: 4) {
@if $suffix == "@@@@" {
$suffix: random_char($len);
}
@return "--" + $name + "-" + $suffix;
}
@mixin host-variable($args...) {
$host: $host;
$variable: false;
$default: false;
@if length($args) == 2 {
$variable: nth($args, 1);
$default: nth($args, 2);
} @else if length($args) == 3 {
$host: nth($args, 1);
$variable: nth($args, 2);
$default: nth($args, 3);
} @else {
@warn "length of host-var mixin $args must be 2 or 3!";
}
#{$variable}: $default;
#{$host}#{$variable}: var(#{$variable});
}
@function host-variable($args...) {
$host: $host;
$variable: false;
@if length($args) == 1 {
$variable: nth($args, 1);
} @else if length($args) == 2 {
$host: nth($args, 1);
$variable: nth($args, 2);
} @else {
@warn "length of host-var function $args must be 1 or 2!";
}
@return var(#{$host}#{trip-initial($variable)});
}
// Alias
@mixin host-var($args...) { @include host-variable($args...); }
@mixin hvar($args...) { @include host-variable($args...); }
@function host-var($args...) { @return host-variable($args...); }
@function hvar($args...) { @return host-variable($args...); }
ここで用意された@mixinと@functionを用いることで、先程のコードは非常にシンプルに書き直せる。
@import 'host-variable';
$host: host('child');
:host {
@include hvar(--width, 300px);
@include hvar(--height, 200px);
@include hvar(--base-color, red);
}
:host {
display: flex;
width: hvar(--width);
height: hvar(--height);
outline: 1px hvar(--base-color);
}
@import 'host-variable';
$host: host('parent');
:host {
@include hvar(--width, 1200px);
@include hvar(--height, 200px);
@include hvar(--base-color, blue);
}
:host {
display: flex;
width: hvar(---width);
height: hvar(---height);
outline: 1px hvar(--base-color);
* {
// SCSS関数はcalc()の中ではインターポレーションを使う必要がある
--width: calc(#{hvar(--width)} / 2);
--height: hvar(--height);
--base-color: hvar(--base-color);
}
}
これで冗長な部分はすべて隠蔽され、非常に直感的で実用に耐えうるものとなった。
しかしながら、ホストCSS変数をcalc()関数の中で呼ぶときのインターポレーション(#{})が非常に煩わしい。
この量なら問題ないと感じるかも知れないが、本気でアトミックデザインに**「ホストCSS変数」**を組み込もうとすると、骨が折れるものである。願わくば、var()と同じ感覚でhvar()を用いたい。
ただ、これが非常に難しい問題であった。
6.Host CSS Variable in Calc Function
結論からいえば、SCSSの機能ではどうにもならなかったので、webpackを用いてSCSSがコンパイルされる前にcalc()の中身を処理することにした。
詳細はこちらの記事に投げるが、かなりの力技である。
ただ、もちろん、パッケージ化してnpmで公開したので、読者の皆様は私と同じことをする必要はない。
npmからパッケージをインストールし、
$ npm i -D host-css-variable
webpack.config.jsにhost-css-valiable/loaderを追加してやれば良い。
module.exports = {
//......
module: {
rules: [
{
test: /\.scss$/,
use: [
// 'scss-loader'等の後に'host-css-valiable/loader'を追加する
'host-css-variable/loader'
],
}
],
},
}
これでインターポレーションを用いることなく**「ホストCSS変数」**をcalc()関数内で使えるようになる。
7.結論
上記のパッケージを用いることで**「ホストCSS変数」**を親コンポーネントから子コンポーネントへと直感的かつシンプルに値を渡していくことができるようになった。これが私のたどり着いたコンポーネント指向のウェブ開発におけるCSS変数のベストプラクティスである。
@import '~host-css-variable/host-variable';
$host: host('child');
:host {
@include hvar(--width, 300px);
@include hvar(--height, 200px);
@include hvar(--base-color, red);
}
:host {
display: flex;
width: hvar(--width);
height: hvar(--height);
outline: 1px hvar(--base-color);
}
@import '~host-css-variable/host-variable';
$host: host('parent');
:host {
@include hvar(--width, 1200px);
@include hvar(--height, 200px);
@include hvar(--base-color, blue);
}
:host {
display: flex;
width: hvar(---width);
height: hvar(---height);
outline: 1px hvar(--base-color);
* {
--width: calc(hvar(--width) / 2);
--height: hvar(--height);
--base-color: hvar(--base-color);
}
}