メモを揉め

お勉強の覚書。

Stylus の変数に JS からアクセスする方法を考える

transitionanimationが使えるようになり、擬似セレクタの種類も増えてきたことで、スタイル周りの多くのことがCSSだけ(あるいはclassListaddremove程度の操作)で書けるようになってきた気がする。

それでも、どうしても JavaScript で操作したい時もまだあって、 Stylus で利用している変数を JavaScript 内でも参照したくなることがある。

いくつか方法を試した結果、結局 Stylus のプラグインを自作することにした。

www.npmjs.com

Atom + pigments

やりたかったことをまとめると、

  • Stylus の変数定義ファイルが存在すること
  • JS からは "20px" のような文字列ではなく、可能な限り 20 のように使用しやすい形で取得できること
  • 実際に変数の定義を書く場所は JS でも Stylus でも良い
  • JS 側、 Stylus 側共にコーディングの制約が少ないこと

という感じです。

Stylus の変数定義ファイルが欲しい理由は、

この pigments というプラグインが Stylus の構文をある程度解釈してくれるので、変数にもハイライトが効くようになる。これが大変便利。 (ここでいう変数定義ファイルとはdefine(key, value)ではなく、key = valueの構文で代入しているコード)

f:id:alluser:20160925214505j:plain

検討した他の手段

調べてみると既にいろんな方法で同じような試みがあった。

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'

エクスポートできる変数のタイプ

stringunitidentrgbahslacubic-beziertuplelisthash を書き出すことが出来る。
unitpx%emmsmm …等の単位付き(あるいは無し)の数値のことで、 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

tuplelisthash に関しては、配列・オブジェクトの各要素がそれぞれ typecss を持つ FoovarValueインスタンスになっている。
各要素は単項の値と同じように typecss へアクセス出来る。

// 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日追記

Safariflex-basis の挙動が ChromeFirefox、Edge と同じなっていたので、記事とサンプルをアップデートしました。

See the Pen Case Studies of Flexbox by Keita Okamoto (@all-user) on CodePen.

縦のストライプの幅が10pxflexコンテナの幅が360pxとなっていて、flexアイテムとそのコンテンツの大きさはそれぞれのラベルに表示されます。

コンテナの上には当てているスタイルの要約と最終的なサイズを割り出す計算式があります。
全ブラウザで結果が同じ場合は緑、ブラウザ間で結果に差異がある場合は赤の文字になっています。

f:id:alluser:20160828194455p:plain f:id:alluser:20160828194518p:plain

一応仕様的にはこうなるべきだろうという計算式を書いているつもりですが、解釈が間違っているかも知れません。
なんか違うなと思ったら教えていただけると助かります。

ベストプラクティス(暫定)

flexboxの各アイテムが最終的にどのサイズで描画されるのかは領域の分配ルールに様々な要素が絡んでいます。
そのため、適当にプロパティを組み合わせていくと、なかなか思い通りのサイズになりません。

スコープを絞るために話をflex-direction: rowに限定するとして、サイズの計算に影響を与えるプロパティはこれだけあります。

  • flex-shrink
  • flex-grow
  • flex-basis
  • width
  • min-width
  • max-width
  • margin
  • padding
  • box-sizing

さらにブラウザ間の誤差やflexboxの抱える様々なバグを考慮して全てをコントロールするのはかなり難しいと思われます。(この実験に関して言えば、実際にはブラウザの誤差はあるひとつの挙動に起因していたため、そこまで辛くなかった。後述)

色々試した結果、暫定的なベストプラクティスとして以下のルールを守るのが良さそうだという結論になりました。

  • flex-basisauto(初期値)
  • width0に設定する(ただしflex-direction: column + heightの時は要注意。下に追記あり)
    f:id:alluser:20160828222041p:plain
  • width0以外にしたい場合はmin-widthをそれより小さい値で設定する
    f:id:alluser:20160828223132p:plain

min-widthを設定すればflex-basisでもブラウザ間の差異は無くなりますが、あえてflex-basisを使うメリットも特に無さそうなのでwidthを使うのが良いかなと思ってます。

  • flex-basisとmin-widthを組み合わせた例
    f:id:alluser:20160828223924p:plain

あくまで厳密にサイズをコントロールしたい時のベストプラクティスです。(flexboxでテーブルみたいに縦を揃えたい、とか)
コンテンツのサイズが予測できたり、サイズに応じてよしなにやって欲しい時はwidthflex-basisともにauto(必要に応じてmax-widthmin-width)でも良いと思います。

幅の計算をするときに気をつけること

paddingmarginの扱い

paddingmarginflex-shrinkflex-growの影響を受けないようです。
常に設定された値になる必要があるので、スペースを分配した結果が設定した値を下回る場合はスペースを再分配する必要があります。

  • 左右のpadding 80pxを下回るため再分配が行われる
    f:id:alluser:20160828223132p:plain

box-sizingの影響

paddingwidthに含まれるかどうかはbox-sizingの値によって決まります。
初期値であるcontent-boxの場合は以下のようになります。

  • width: autoのためコンテンツサイズ分の40px * 3を除く240pxflex-growの値に基いて分配される
    f:id:alluser:20160828231139p:plain

  • コンテンツサイズ分の40px * 3に加え、左右のpadding 40px * 3を除く120pxflex-growの値に基いて分配される
    f:id:alluser:20160828230822p:plain

  • flexアイテムのwidth 160px * 3とコンテナサイズ360pxの差-120pxflex-shrinkに基いて分配される
    f:id:alluser:20160828234942p:plain

  • flexアイテムのwidth 160px * 3に左右のpadding 80px * 3を加えた720pxとコンテナサイズ360pxの差-360pxflex-shrinkに基いて分配される
    f:id:alluser:20160828223132p:plain

min-widthの効果

widthの設定を0以上に設定していて、その値よりコンテンツサイズが小さい場合、flex-shrinkはコンテンツサイズを下回って縮小することができなくなります。(flex-basisの場合は0に設定してもコンテンツサイズを下回るサイズにはなれないことがある。後述)

min-widthを設定することで、その値まではコンテンツサイズを下回るサイズに縮小することが出来ます。

  • コンテンツサイズの160pxを下回って縮小することが出来ない(Safari、IE11は縮小する) f:id:alluser:20160829001831p:plain

  • min-width: 0によりコンテンツサイズの160pxを下回って縮小することが出来る
    f:id:alluser:20160829001849p:plain

ブラウザ間の差異(2016年8月28日現在)

この実験に絞って言えばブラウザ間で結果に差異が出る原因は、flex-basisがコンテンツサイズを保証するかどうか、ということに集約できました。

ChromeFirefox、Edgeはコンテンツのサイズを維持。
Safari、IE11はコンテンツサイズを無視する。

コンテンツサイズを保証するというのはどういう事かというと、flex-shrinkflex-growを適用した後に、もしflexアイテムのサイズがコンテンツサイズを下回っていた場合に、そのflexアイテム以外のflexアイテムの間でネガティブなスペースを再分配し、必ずコンテンツサイズを下回らない様にします。(それでも下回る場合はそれ以上縮小しない)

  • 再分配してもコンテンツサイズを下回るのでそれ以上縮小しない例
    f:id:alluser:20160828202921p:plain

どちらが仕様的に正しいのかはわかりませんが、IEを除くとコンテンツサイズを無視するのはSafariだけなのでChromeFirefox、Edgeの動作がより最新の仕様に沿っているのかもしれません。
現在は SafariChromeFirefox、Edge と同じ動作です。

追記(2016年8月31日)height0指定について

flex-direction: column時にheight0を指定すると、たとえflexアイテムがflex-growによってコンテナいっぱいに広がっていたとしても、flexアイテム内の要素からはheight0に見えてしまうらしく、%指定が出来なくなるというデメリットがあることが分かりました。(Safariのみ)

heightの代わりにmin-height0にすれば大丈夫でした。

  • Safariのスクショ。左: height: 0、右: height: 100% + min-height: 0
    f:id:alluser:20160830235946p:plain
    枠線付きがflexコンテナ、濃いグレーがflexアイテム、薄いグレーがflexアイテム内のブロック要素

http://codepen.io/all-user/pen/RGbKRd

追記(2016年9月5日)flex-basiscontent について

flex-content に指定できる値 content についての調査を追加しました。

結論から書くとブラウザ間の挙動がバラバラ。
今はまだ使わないほうが良さそうです。

以下、実験で分かったこと。

ショートハンドが効かない(Chrome, Firefox, Safari, IE11)

ショートハンドで指定した flex-growflex-shrink がなぜか無効になります。(Edge は有効)
flex-grow0Chrome, Firefox, Safari, IE11)、 flex-shrink1Chrome, Firefox, Safari)と解釈されているように見えます。

ロングハンドの指定は有効でした。

  • flex-grow
    • flex-basiscontent
      flex-grow123 をショートハンドで指定
      f:id:alluser:20160905233121p:plain
    • flex-basiscontentcontentauto
      flex-grow123 をショートハンドで指定
      f:id:alluser:20160905234039p:plain
    • flex-basiscontent
      flex-grow123 をロングハンドで指定
      f:id:alluser:20160905235419p:plain
  • flex-shrink
    • flex-basiscontent
      min-width0
      flex-shrink123 をショートハンドで指定
      f:id:alluser:20160906001341p:plain
    • flex-basiscontentcontentauto
      min-width0
      flex-shrink123 をショートハンドで指定
      f:id:alluser:20160906001658p:plain
    • flex-basiscontent
      min-width0
      flex-shrink123 をロングハンドで指定
      f:id:alluser:20160906003231p:plain

追記(2016年9月6日)Firefoxの表記について

FireFox から Firefox に変更しました。

width、heightがautoの要素にtransitionを適用する

CSS3のtransition。

一般的なウェブアプリのUIで動きが必要になる時って、始点と終点がある程度決まっていて、animation@keyframeを駆使するような動きは限定的なことが多いと思うのだけど。

このtransitionを使っていていつも悩ましいのが、widthheightautoに設定されている要素に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-bottomheightを指定できる。
インラインが取り得る文字数の範囲が予測できる場合は、widthの値も明示できる可能性がある。

// Stylus

.foo
  line-height 16px
  padding 6px 0
  width 200px
  height 0
  transition height .2s
  &.opened
    height @line-height+@padding-top*2

20pxのような絶対値の指定でなくても、80vhcalc(50% + 20px)のような指定であればtransitionは効くので、それらで解決できないかも検討してみる。

max-heightmax-widthを使用する

heightwidthの代わりにmax-heightmax-widthを使用する方法。
transition-propertyにはmax-heightmax-widthを指定する。

heightの場合

以下のようにmax-height100vhのように指定しておくと、コンテンツの高さに合わせて.fooの高さが決まる(ただしコンテンツが100vh以上の場合は100vhになる)。

// Stylus

.foo
  width 200px
  max-height 0
  transition max-height .2s
  &.opened
    max-height 100vh

widthの場合

display: blockの場合、widthは親要素いっぱいに広がろうとするので、コンテンツのサイズに合わせたい場合はdisplay: inline-blockdisplay: flexにする必要がある。

// Stylus

.foo
  display inline-block
  max-width 0
  height 500px
  transition max-width .2s
  &.opened
    max-width 100%

ただし、以下のようなデメリットがある為、きめ細く動きを調整したい場合には向いていない。

  • transitionの始点、終点はmax-heightmax-widthの値で計算される
    • そのため、実際の高さ、幅とmax-heightmax-widthの差が大きいほどtransitionの効果は不自然になる
      • 基本的に設定したtransition-durationより短くなる(早くtransitionが終了する)
      • transition-timing-functionheightwidthの始点、終点とズレるので綺麗に適用されない
      • サイズが小さくなる動きの場合、クリックなどのユーザアクションに対して反応が遅れる

簡単なので間に合わせでとりあえず動きを付けたいという時は良いかも知れない。

JavaScriptでコンテンツサイズを調べる

getComputedStylegetBoudingClientRectなどでコンテンツのサイズを調べる方法。

ここではgetBoundingClientRectを使うようにする。(getComputedStyleだとautoの値がうまく取れなかった事があったはずだけどうまく再現できず)

ラッパー要素を用意する

コンテンツ全体を包む要素を用意し、position: absoluteを適用する。
この要素のheightwidthautoを指定して、ドロップボックスがエキスパンドした状態などを維持させておき、常にコンテンツサイズに合わせておく。
このラッパー要素からgetBoundingClientRectでサイズを取得してリサイズする要素に適用する。

今のところこの方法が一番良さそうに感じる。

autoの値を正しく得るために、外側のheightwidthが固定値の場合はそれをラッパー要素にも適用する。

// 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を一旦無効にしてサイズを調べる方法

やってみたけど、ボツにした方法。

あえて利点を挙げるとすれば、コンテンツがテキストノードだけの場合でも使えるので、ラッパー要素を用意する必要が無い。
ブラウザ間の誤差を気持ち悪いハックで誤魔化しているので、いつ動かなくなっても驚かない。
使わないほうが良い。

  1. インラインスタイルでtransitionnoneを設定
  2. heightwidthautoを適用
  3. getBoundingClientRectheightwidthを調べる
  4. heightwidthを元の値に戻す
  5. transitionを元の値に戻す
  6. setTimeoutで非同期にheightwidthに適用する

FireFoxChromeでは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);
  }
});

今日の結論

heightwidthautoが設定してある要素にtransitionを適用したい場合は、ラッパー要素を用意してJavaScriptでサイズを取得するのが良いと思う。
CSSだけだと厳しそうなので、JavaScriptで少しカバーしてあげるとバランスが良い。