Stylus の変数に JS からアクセスする方法を考える
transition
、animation
が使えるようになり、擬似セレクタの種類も増えてきたことで、スタイル周りの多くのことがCSSだけ(あるいはclassList
のadd
、remove
程度の操作)で書けるようになってきた気がする。
それでも、どうしても JavaScript で操作したい時もまだあって、 Stylus で利用している変数を JavaScript 内でも参照したくなることがある。
いくつか方法を試した結果、結局 Stylus のプラグインを自作することにした。
Atom + pigments
やりたかったことをまとめると、
- Stylus の変数定義ファイルが存在すること
- JS からは
"20px"
のような文字列ではなく、可能な限り20
のように使用しやすい形で取得できること - 実際に変数の定義を書く場所は JS でも Stylus でも良い
- JS 側、 Stylus 側共にコーディングの制約が少ないこと
という感じです。
Stylus の変数定義ファイルが欲しい理由は、
この pigments というプラグインが Stylus の構文をある程度解釈してくれるので、変数にもハイライトが効くようになる。これが大変便利。
(ここでいう変数定義ファイルとはdefine(key, value)
ではなく、key = value
の構文で代入しているコード)
検討した他の手段
調べてみると既にいろんな方法で同じような試みがあった。
rosetta
Stylusだけでなく、Sass、LESSで使うことも出来る。 JS => Stylus でも Stylus => JSでもなく、.rose という拡張子の rosetta ファイルから JS と Stylus(もしくは Sass、LESS)を書き出す仕組みになっている。 最初 README を読んだ時、Stylus で変数定義して rosetta を require すればあら不思議、Stylus の変数が使えちゃう、みたいなノリで書いてあるから勘違いしたがどうやら違う。
それでも rosetta ファイルが Stylus と同じ様に書けるならまあ問題ないかと思い、実際に rosetta ファイルを作ってコンパイルしてみたが、コンパイルエラーになった。
rosetta ファイルは Stylus とシンタックスは同じと書いてあるが、変数名の-
や、タプルの代入などはシンタックスエラーになる。
ということは、つまり Stylus の ファイルをそのまま使えないということになるし、define メソッドなどで定義されたものも共有出来ない。
既存の Stylus が流用できないのは面倒くさそうなので使うのはやめた。
stylus-export-loader
webpack 用ではあるがやりたいことには概ねマッチしている感じがする。 なぜか npm に登録されてないので github リポジトリを package.json に書いてインストールする。
変数定義用の Stylus ファイルを用意しておき、 !stylus-export-loader!
+ そのパスを webpack.ProvidePlugin
経由で読み込むことで任意のネームスペースから Stylus の変数にアクセスできるようになる(と思ったけど結局うまく動かなかったよ。)
stylus-vars, stylus-var
どちらも Stylus の変数を JS で定義出来るよーというもの。 Stylus の変数定義ファイルが生成されないのでこれも見送った。
そこで foovar ですよ
foovar を使うと Stylus の変数を JS ファイルに書き出す事ができる。
Stylus の実行時に変数スコープを調べるので、Stylus 側では制約無く変数を定義して使うことが出来る。
変数定義ファイルも用意出来る。
これでやりたかったことは全部できるようになった。
インストール
$ npm i -D foovar
使い方
CLI の場合
$ stylus -u foovar path/to/file.styl
webpack + stylus-loader の場合
// webpack.config.js module.exports = { stylus: { use: [require('foovar')()] } };
Stylus ファイルの任意の場所で foovar(path)
を呼び出す。
その時点で定義されている変数が JS ファイルにエクスポートされる。
foo = 10px foovar('src/StyleDefinitions.js')
エクスポートされた JS は require
して使うことが出来る。
const StyleDefinitions = require('./src/StyleDefinitions.js'); StyleDefinitions.foo(); // 10 StyleDefinitions.foo.type; // 'px' StyleDefinitions.foo.css; // '10px'
エクスポートできる変数のタイプ
string
、 unit
、 ident
、 rgba
、 hsla
、 cubic-bezier
、 tuple
、 list
、 hash
を書き出すことが出来る。
unit
は px
、 %
、em
、 ms
、 mm
…等の単位付き(あるいは無し)の数値のことで、 varName.type
はその単位を返す(無しの場合は undefined
)。
Stylus:$var-name |
JS:varName() |
JS:varName.type |
JS:varName.css |
---|---|---|---|
'some text' |
'some text' |
'string' |
'some text' |
20px |
20 |
'px' |
'20px' |
50% |
50 |
'%' |
'50%' |
255 |
255 |
undefined |
'255' |
auto |
'auto' |
'ident' |
'auto' |
#112233 |
[17,34,51,1] |
'rgba' |
'#112233' |
#11223344 |
[17,34,51,0.26666666666666666] |
'rgba' |
'#11223344' |
rgba(11,22,33,.4) |
[11,22,33,0.4] |
'rgba' |
'rgba(11,22,33,0.4)' |
hsl(11,22%,33%) |
[11,22,33,1] |
'hsla' |
'hsla(11,22%,33%,1)' |
hsla(11,22%,33%,.4) |
[11,22,33,0.4] |
'hsla' |
'hsla(11,22%,33%,0.4)' |
cubic-bezier(1,0,1,0) |
[1,0,1,0] |
'cubic-bezier' |
'cubic-bezier(1,0,1,0)' |
10px 20px 30px 40px |
[FoovarValue instance x 4] |
'tuple' |
undefined |
1em, 2em, 3em, 4em |
[FoovarValue instance x 4] |
'list' |
undefined |
{ foo: 1em } |
{ foo: FoovarValue instance } |
'hash' |
undefined |
tuple
、 list
、 hash
に関しては、配列・オブジェクトの各要素がそれぞれ type
、 css
を持つ FoovarValue
のインスタンスになっている。
各要素は単項の値と同じように type
、 css
へアクセス出来る。
// foo = 10px 20px 30px 40px // bar = { baz: 1em } const StyleDefinitions = require('./src/StyleDefinitions.js'); StyleDefinitions.foo()[0]() // 10 StyleDefinitions.foo()[1].type // 'px' StyleDefinitions.foo()[2].css // '30px' StyleDefinitions.bar().baz() // 1 StyleDefinitions.bar().baz.type // 'em' StyleDefinitions.bar().baz.css // '1em'
普段よく自分がアクセスしたくなりやすいものを中心に実装した。URL とかはまだ対応していない。
いずれ欲しくなったときに追加するかも知れないししないかもしれない。
Stylus の変数にアクセスしたくなった時は foovar をぜひに。
Case Studies in Flexbox - flex-grow, flex-shrink, flex-basis
flexboxのプロパティの組み合わせを比較できるカタログが欲しいなーと思ったので作ってみました。
各プロパティーの値がどのように解釈されるか、ブラウザ間での違いなどが分かります。
あれもこれもと入れていたら無駄に長くなりました。
2017年8月17日追記
Safari の flex-basis
の挙動が Chrome、Firefox、Edge と同じなっていたので、記事とサンプルをアップデートしました。
See the Pen Case Studies of Flexbox by Keita Okamoto (@all-user) on CodePen.
縦のストライプの幅が10px
、flexコンテナの幅が360px
となっていて、flexアイテムとそのコンテンツの大きさはそれぞれのラベルに表示されます。
コンテナの上には当てているスタイルの要約と最終的なサイズを割り出す計算式があります。
全ブラウザで結果が同じ場合は緑、ブラウザ間で結果に差異がある場合は赤の文字になっています。
一応仕様的にはこうなるべきだろうという計算式を書いているつもりですが、解釈が間違っているかも知れません。
なんか違うなと思ったら教えていただけると助かります。
ベストプラクティス(暫定)
flexboxの各アイテムが最終的にどのサイズで描画されるのかは領域の分配ルールに様々な要素が絡んでいます。
そのため、適当にプロパティを組み合わせていくと、なかなか思い通りのサイズになりません。
スコープを絞るために話をflex-direction: row
に限定するとして、サイズの計算に影響を与えるプロパティはこれだけあります。
flex-shrink
flex-grow
flex-basis
width
min-width
max-width
margin
padding
box-sizing
さらにブラウザ間の誤差やflexboxの抱える様々なバグを考慮して全てをコントロールするのはかなり難しいと思われます。(この実験に関して言えば、実際にはブラウザの誤差はあるひとつの挙動に起因していたため、そこまで辛くなかった。後述)
色々試した結果、暫定的なベストプラクティスとして以下のルールを守るのが良さそうだという結論になりました。
flex-basis
はauto
(初期値)width
を0
に設定する(ただしflex-direction: column
+height
の時は要注意。下に追記あり)
width
を0
以外にしたい場合はmin-width
をそれより小さい値で設定する
min-width
を設定すればflex-basis
でもブラウザ間の差異は無くなりますが、あえてflex-basis
を使うメリットも特に無さそうなのでwidth
を使うのが良いかなと思ってます。
- flex-basisとmin-widthを組み合わせた例
あくまで厳密にサイズをコントロールしたい時のベストプラクティスです。(flexboxでテーブルみたいに縦を揃えたい、とか)
コンテンツのサイズが予測できたり、サイズに応じてよしなにやって欲しい時はwidth
、flex-basis
ともにauto
(必要に応じてmax-width
、min-width
)でも良いと思います。
幅の計算をするときに気をつけること
padding
、margin
の扱い
padding
、margin
はflex-shrink
、flex-grow
の影響を受けないようです。
常に設定された値になる必要があるので、スペースを分配した結果が設定した値を下回る場合はスペースを再分配する必要があります。
- 左右の
padding
80px
を下回るため再分配が行われる
box-sizing
の影響
padding
がwidth
に含まれるかどうかはbox-sizing
の値によって決まります。
初期値であるcontent-box
の場合は以下のようになります。
width: auto
のためコンテンツサイズ分の40px * 3
を除く240px
がflex-grow
の値に基いて分配される
コンテンツサイズ分の
40px * 3
に加え、左右のpadding
40px * 3
を除く120px
がflex-grow
の値に基いて分配される
flexアイテムの
width
160px * 3
とコンテナサイズ360px
の差-120px
がflex-shrink
に基いて分配される
flexアイテムの
width
160px * 3
に左右のpadding
80px * 3
を加えた720px
とコンテナサイズ360px
の差-360px
がflex-shrink
に基いて分配される
min-width
の効果
width
の設定を0
以上に設定していて、その値よりコンテンツサイズが小さい場合、flex-shrink
はコンテンツサイズを下回って縮小することができなくなります。(flex-basis
の場合は0
に設定してもコンテンツサイズを下回るサイズにはなれないことがある。後述)
min-width
を設定することで、その値まではコンテンツサイズを下回るサイズに縮小することが出来ます。
コンテンツサイズの
160px
を下回って縮小することが出来ない(Safari、IE11は縮小する)min-width: 0
によりコンテンツサイズの160px
を下回って縮小することが出来る
ブラウザ間の差異(2016年8月28日現在)
この実験に絞って言えばブラウザ間で結果に差異が出る原因は、flex-basis
がコンテンツサイズを保証するかどうか、ということに集約できました。
Chrome、Firefox、Edgeはコンテンツのサイズを維持。
Safari、IE11はコンテンツサイズを無視する。
コンテンツサイズを保証するというのはどういう事かというと、flex-shrink
やflex-grow
を適用した後に、もしflexアイテムのサイズがコンテンツサイズを下回っていた場合に、そのflexアイテム以外のflexアイテムの間でネガティブなスペースを再分配し、必ずコンテンツサイズを下回らない様にします。(それでも下回る場合はそれ以上縮小しない)
- 再分配してもコンテンツサイズを下回るのでそれ以上縮小しない例
どちらが仕様的に正しいのかはわかりませんが、IEを除くとコンテンツサイズを無視するのはSafariだけなのでChrome、Firefox、Edgeの動作がより最新の仕様に沿っているのかもしれません。
現在は Safari も Chrome、Firefox、Edge と同じ動作です。
追記(2016年8月31日)height
の0
指定について
flex-direction: column
時にheight
に0
を指定すると、たとえflexアイテムがflex-grow
によってコンテナいっぱいに広がっていたとしても、flexアイテム内の要素からはheight
が0
に見えてしまうらしく、%
指定が出来なくなるというデメリットがあることが分かりました。(Safariのみ)
height
の代わりにmin-height
を0
にすれば大丈夫でした。
- Safariのスクショ。左:
height: 0
、右:height: 100%
+min-height: 0
枠線付きがflexコンテナ、濃いグレーがflexアイテム、薄いグレーがflexアイテム内のブロック要素
http://codepen.io/all-user/pen/RGbKRd
追記(2016年9月5日)flex-basis
の content
について
flex-content
に指定できる値 content
についての調査を追加しました。
結論から書くとブラウザ間の挙動がバラバラ。
今はまだ使わないほうが良さそうです。
以下、実験で分かったこと。
ショートハンドが効かない(Chrome, Firefox, Safari, IE11)
ショートハンドで指定した flex-grow
、 flex-shrink
がなぜか無効になります。(Edge は有効)
flex-grow
は 0
(Chrome, Firefox, Safari, IE11)、 flex-shrink
は 1
(Chrome, Firefox, Safari)と解釈されているように見えます。
ロングハンドの指定は有効でした。
flex-grow
flex-basis
にcontent
flex-grow
に1
、2
、3
をショートハンドで指定
flex-basis
にcontent
、content
、auto
flex-grow
に1
、2
、3
をショートハンドで指定
flex-basis
にcontent
flex-grow
に1
、2
、3
をロングハンドで指定
flex-shrink
flex-basis
にcontent
min-width
に0
flex-shrink
に1
、2
、3
をショートハンドで指定
flex-basis
にcontent
、content
、auto
min-width
に0
flex-shrink
に1
、2
、3
をショートハンドで指定
flex-basis
にcontent
min-width
に0
flex-shrink
に1
、2
、3
をロングハンドで指定
追記(2016年9月6日)Firefoxの表記について
width、heightがautoの要素にtransitionを適用する
CSS3のtransition。
一般的なウェブアプリのUIで動きが必要になる時って、始点と終点がある程度決まっていて、animation
の@keyframe
を駆使するような動きは限定的なことが多いと思うのだけど。
このtransitionを使っていていつも悩ましいのが、width
やheight
がauto
に設定されている要素にtransition
を適用したい時です。
transition
を有効にする場合、始点と終点の値を100px
のように絶対値で指定するか、50%
(もちろんheightなどで親のautoを引継いでしまっている場合はダメ)のように相対値で指定することはできるけど、auto
だとtransition
が効かない。
// Stylus .foo width 200px height 0 transition height .2s &.opened height auto
上の例の場合、
// JavaScript document.querySelector('.foo').classList.add('opened');
のようにclass="opened"
になるとheight
の値は0
からauto
に変わるが、transition
による効果は適用されない。
同様に、
// JavaScript document.querySelector('.foo').classList.remove('opened');
のようにopened
を取り去ると、height
の値はauto
から0
に変わるが、この場合もtransition
の効果は発生しない。
ドロップダウンメニューや開閉可能なペインのように、コンテンツの内容に応じてリサイズしたい場合に困る。
これを解決する良い方法はまだよく分からないのですが、実際に試してみた幾つかのアプローチをメモしておきます。
基本方針
JavaScriptによる操作は最低限にしたい。
class
の付け外しなどにとどめ、インラインスタイルの値を直接いじるのは最終手段にしたい。
auto
を使わない
いきなり解決して無いですが、サイズを明示的に指定できる場合は極力指定する、という方法。
例えば、(折り返しを許可しない)一行だけのインラインが含まれるような要素の場合line-height
+padding-top
+padding-bottom
でheight
を指定できる。
インラインが取り得る文字数の範囲が予測できる場合は、width
の値も明示できる可能性がある。
// Stylus .foo line-height 16px padding 6px 0 width 200px height 0 transition height .2s &.opened height @line-height+@padding-top*2
20px
のような絶対値の指定でなくても、80vh
、calc(50% + 20px)
のような指定であればtransition
は効くので、それらで解決できないかも検討してみる。
max-height
、max-width
を使用する
height
、width
の代わりにmax-height
、max-width
を使用する方法。
transition-property
にはmax-height
、max-width
を指定する。
height
の場合
以下のようにmax-height
を100vh
のように指定しておくと、コンテンツの高さに合わせて.foo
の高さが決まる(ただしコンテンツが100vh
以上の場合は100vh
になる)。
// Stylus .foo width 200px max-height 0 transition max-height .2s &.opened max-height 100vh
width
の場合
display: block
の場合、width
は親要素いっぱいに広がろうとするので、コンテンツのサイズに合わせたい場合はdisplay: inline-block
かdisplay: flex
にする必要がある。
// Stylus .foo display inline-block max-width 0 height 500px transition max-width .2s &.opened max-width 100%
ただし、以下のようなデメリットがある為、きめ細く動きを調整したい場合には向いていない。
transition
の始点、終点はmax-height
、max-width
の値で計算される- そのため、実際の高さ、幅と
max-height
、max-width
の差が大きいほどtransition
の効果は不自然になる- 基本的に設定した
transition-duration
より短くなる(早くtransition
が終了する) transition-timing-function
もheight
、width
の始点、終点とズレるので綺麗に適用されない- サイズが小さくなる動きの場合、クリックなどのユーザアクションに対して反応が遅れる
- 基本的に設定した
- そのため、実際の高さ、幅と
簡単なので間に合わせでとりあえず動きを付けたいという時は良いかも知れない。
JavaScriptでコンテンツサイズを調べる
getComputedStyle
、getBoudingClientRect
などでコンテンツのサイズを調べる方法。
ここではgetBoundingClientRect
を使うようにする。(getComputedStyle
だとauto
の値がうまく取れなかった事があったはずだけどうまく再現できず)
ラッパー要素を用意する
コンテンツ全体を包む要素を用意し、position: absolute
を適用する。
この要素のheight
、width
にauto
を指定して、ドロップボックスがエキスパンドした状態などを維持させておき、常にコンテンツサイズに合わせておく。
このラッパー要素からgetBoundingClientRect
でサイズを取得してリサイズする要素に適用する。
今のところこの方法が一番良さそうに感じる。
auto
の値を正しく得るために、外側のheight
、width
が固定値の場合はそれをラッパー要素にも適用する。
// Stylus .foo width 200px height 0 transition height .2s .bar position absolute width @width height auto
// JavaScript const foo = document.querySelector('.foo'); const bar = document.querySelector('.bar'); document.documentElement.addEventListener('click', e => { if(foo.classList.contains('opened')) { foo.classList.remove('opened'); foo.style.height = ''; } else { const contentHeight = bar.getBoundingClientRect().height; foo.style.height = `${contentHeight}px`; foo.classList.add('opened'); } });
唯一の欠点は、ドロップダウンメニューなどが開閉する際、コンテンツ自体はサイズが伸び縮みしないので、もしそういう風に動きを付けたい場合は、もう一手間必要になる、という点。
個人的な感覚では、コンテンツのサイズを伸び縮みさせたくないことのほうが多い(トランジションの途中で文字の折り返し部分が変わっていったりするとキモい)ので、あまり困らないかなと思う。
transition
を一旦無効にしてサイズを調べる方法
やってみたけど、ボツにした方法。
あえて利点を挙げるとすれば、コンテンツがテキストノードだけの場合でも使えるので、ラッパー要素を用意する必要が無い。
ブラウザ間の誤差を気持ち悪いハックで誤魔化しているので、いつ動かなくなっても驚かない。
使わないほうが良い。
- インラインスタイルで
transition
にnone
を設定 height
、width
にauto
を適用getBoundingClientRect
でheight
、width
を調べるheight
、width
を元の値に戻すtransition
を元の値に戻すsetTimeout
で非同期にheight
、width
に適用する
FireFox、ChromeではsetTimeout
で非同期に適用しないとtransition
が効かない、またChromeでは第二引数を0
にすると効かないことがある。
Safariでは同期処理中にもチラつきがある。
また、元の値を0
以外にすると、auto
の値を適用した時に、一度0
になってしまう。
// Stylus .foo width 200px height 0 transition height .2s
// JavaScript const foo = document.querySelector('.foo'); document.documentElement.addEventListener('click', e => { if(foo.classList.contains('opened')) { foo.classList.remove('opened'); foo.style.height = ''; } else { foo.style.transition = 'none'; foo.style.height = 'auto'; const contentHeight = foo.getBoundingClientRect().height; foo.style.height = ''; foo.style.transition = ''; setTimeout(() => { foo.style.height = `${contentHeight}px`; foo.classList.add('opened'); }, 30); } });
今日の結論
height
、width
にauto
が設定してある要素にtransition
を適用したい場合は、ラッパー要素を用意してJavaScriptでサイズを取得するのが良いと思う。
CSSだけだと厳しそうなので、JavaScriptで少しカバーしてあげるとバランスが良い。