RSCSS という CSS 設計について
(追記)
実践例あった
http://ricostacruz.com/rscss/index.html
追記終わり。
https://github.com/rstacruz/rscss
しばらく CSS とか追ってなかったので、触るにあたって「むやみにCSS書いてたら後で確実に死ぬし、そういえばなんかOOCSSとかあったな」と思っていろいろ調べてたら OOCSS の他にも SMACSS とか BEM とか SuitCSS とか FLOCSS とかなんかいろいろ出てきて大変でした。たしか SMACSS くらいまでは記憶があるんだけど…。
で、どうもどれもしっくり来ないのでさらに調べてみると RSCSS というものを発見。「フレームワークじゃなくてあくまでもアイデア集だよ」ってことらしく、ルールガチガチでもないしなんだかよさ気。
で、RSCSS の README を読みながらメモしたんでせっかくだし公開します。ほぼただ訳しただけなんですが…。英語あまり得意ではないので間違いなど指摘いただけると助かります。
Components
検索フォームのようなひとかたまりをひとつのコンポーネントとして考える
Components の命名規則
Components は ダッシュで区切られた 少なくとも2つの単語 からなる。
- Like ボタン (
.like-button
) - 検索フォーム (
.search-form
) - ニュースカード (
.article-card
)
Elements
Elements とは Component を構成する内部要素。
Elements の命名規則
それぞれの Component は通常、複数の Element を持つ。 Element の名前は 1単語 にする。
.search-form { > .field { /* ... */ } > .action { /* ... */ } }
Element のセレクター
可能な限り子セレクタ >
を使う。Componentのネスト防止になるし、子孫セレクタよりパフォーマンスがいい。
.article-card { .title { /* これでもいいけど */ } > .author { /* ✓ こっちのほうがいい */ } }
複数の単語が使いたい
2つ以上の単語からなる名前をつけたい場合は、ダッシュやアンダースコアを使わないで単純に連結する。
.profile-box { > .firstname { /* ... */ } > .lastname { /* ... */ } > .avatar { /* ... */ } }
タグセレクタを避ける
可能な限りクラス名を使用する。タグセレクタはよいものだが、若干のパフォーマンス上のペナルティがあり、また説明的ではないかもしれない。
.article-card { > h3 { /* ✗ これは避ける */ } > .name { /* ✓ こうするほうがいい */ } }
Variant
Component も Element もそれぞれ variant を持つことができる。
Variant の命名
variant のためのクラス名にはダッシュ ( -
) を prefix としてつける
.like-button { &.-wide { /* ... */ } &.-short { /* ... */ } &.-disabled { /* ... */ } }
Element の variant
Element もまた variant をもつことができる
.shopping-card { > .title { /* ... */ } > .title.-small { /* ... */ } }
ダッシュ・プレフィックス
ダッシュは variant のプレフィックス(接頭辞)として使用できる
- Element との曖昧さ防止
- CSSのクラス名はアルファベットそして
_
または-
で始めることができる - ダッシュはアンダースコアよりタイプしやすい
- it kind of resembles switches in UNIX commands (
gcc -O2 -Wall -emit-last
)
(ダッシュやアンダースコアではじまるクラス名ってOKなの知らなかった)
In CSS, identifiers (including element names, classes, and IDs in selectors) can contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_); they cannot start with a digit, two hyphens, or a hyphen followed by a digit. Identifiers can also contain escaped characters and any ISO 10646 character as a numeric code (see next item). For instance, the identifier "B&W?" may be written as "B&W\?" or "B\26 W\3F".
http://www.w3.org/TR/CSS21/syndata.html#characters
ネストした Component
<div class='article-link'> <div class='vote-box'> ... </div> <h3 class='title'>...</h3> <p class='meta'>...</p> </div>
時には Component をネストする必要がでてくる
ネストした Component の Variant
Component は何らかの方法で他のComponentにネストする必要が生じるかもしれない。ネストしたコンポーネントを、それを内包するコンポーネントに突っ込むような変更は避ける。
.article-header { > .vote-box > .up { /* ✗ これは避ける */ } }
かわりに、ネストした Component に Variant を追加し、それを含む component からそれを適用するのが好ましい。
<div class='article-header'> <div class='vote-box -highlight'> ... </div> ... </div>
.vote-box { &.-highlight > .up { /* ... */ } }
ネストした Component の単純化
<div class='search-form'> <input class='input' type='text'> <button class='search-button -red -large'></button> </div>
CSS のプリプロセッサーの @extend
によって単純化することができる
<div class='search-form'> <input class='input' type='text'> <button class='submit'></button> </div>
.search-form { > .submit { @extend .search-button; @extend .search-button.-red; @extend .search-button.-large; } }
Layout
ポジションに関するプロパティを避ける
Component は異なるコンテキストで再利用できるべきなので、以下のようなプロパティを書くのを避ける
- Positioning (position, top, left, right, bottom)
- Floats (float, clear)
- Margins (margin)
- Dimensions (width, height) *
固定寸法
アバターやロゴのような width/height が固定される要素については例外とする
親要素でポジションを定義する
これらを定義したいなら、それらが所属するコンテキストに定義する。下記の例で言うと、 list コンポーネントに適用するのであって、 .article-card 自身に定義するのではない。
.article-list { & { @include clearfix; } > .article-card { width: 33.3%; float: left; } } .article-card { & { /* ... */ } > .image { /* ... */ } > .title { /* ... */ } > .category { /* ... */ } }
Helpers
._unmargin { margin: 0 !important; } ._center { text-align: center !important; } ._pull-left { float: left !important; } ._pull-right { float: right !important; }
汎用クラスはアンダースコアで始まる名前で別ファイルに置かれ、値を上書きする。通常、 !important
でタグ付けされる。慎重に使うこと。
Helper の命名
アンダースコアをクラス名のプレフィックスとして付ける。これは component に定義された modifier と容易に見分けることができる。アンダースコアはちょっと不格好で内部的な副作用がある。Helperの使い過ぎには嫌気が差すだろう。
<div class='order-graphs -slim _unmargin'> </div>
Helper の整理
helpers というひとつのファイルに収める。複数ファイルに分割することもできるが、たくさんの helper を最小限に維持するのが好ましい。
CSS structure
ファイル単位でひとつの Component
それぞれの Component はそれぞれひとつのファイルに収める
/* css/components/search-form.scss */ .search-form { > .button { /* ... */ } > .field { /* ... */ } > .label { /* ... */ } // variants &.-small { /* ... */ } &.-wide { /* ... */ } }
Use glob matching
@import 'components/*';
過剰なネストを避ける
できるだけ1階層のネストにとどめる。ネストし過ぎは簡単に迷子になる。
/* ✗ Avoid: 3 levels of nesting */ .image-frame { > .description { /* ... */ > .icon { /* ... */ } } } /* ✓ Better: 2 levels */ .image-frame { > .description { /* ... */ } > .description > .icon { /* ... */ } }
落とし穴
ネストした Component で死亡
ネストした Component が似たような名前の Element を持つ場合に気をつける。
<article class='article-link'> <div class='vote-box'> <button class='up'></button> <button class='down'></button> <span class='count'>4</span> </div> <h3 class='title'>Article title</h3> <p class='count'>3 votes</p> </article>
.article-link { > .title { /* ... */ } > .count { /* ... (!!!) */ } } .vote-box { > .up { /* ... */ } > .down { /* ... */ } > .count { /* ... */ } }
この場合、 .article-link > .count
は >
セレクタを持っておらず、おそらく .vote-box .count
に適用される。これは子セレクタが好ましい理由のひとつ。
懸念事項
ダッシュとか最悪
: このルールを無視して普通の単語をつかってもいい。ただ、 Component - Element - Variant という考えは忘れないで欲しい2単語なんて思いつかない
: alert のような、いくつかの Component は表現上の理由から1単語であることを要する。これらはサフィックス(接尾辞)を使用することでブロック要素だということをより明確にすることを検討したほうがよい。.alert-box
.alert-card
.alert-block
Or for inlines:
.link-button
.link-span
その他のリソース
その他のソリューション
BEM
BEM は素晴らしいけどシンタックスがちょっとキモいという人もいると思う。RSCSSはBEMとはシンタックスが違うだけで、慣習にはほぼ従っている。
<!-- BEM --> <form class='site-search site-search--full'> <input class='site-search__field' type='text'> <button class='site-search__button'></button> </form>
<!-- rscss --> <form class='site-search -full'> <input class='field' type='text'> <button class='button'></button> </form>
用語
同様のコンセプトはその他のCSS設計にもみられる
RSCSS | BEM | SMACSS |
---|---|---|
Component | Block | Module |
Element | Element | Sub-Component |
Layout | ? | Layout |
Variant | Modifier | Sub-Module & State |
まとめ
DjangoでImageFieldを持つフォームのテスト
DjangoでImageFieldを持ったFormがあるとします。こんな感じ。ModelFormでもいい(というか実際のコードはそっちで書いてる)。
class UploadForm(forms.Form): title = forms.CharField() photo = forms.ImageField()
これをテストする際にStringIOで適当に作ったダミーを食わせたら「画像じゃないよ!」って怒られた。ダミー画像をテスト用に置いておくのもなんだかなぁ…と思ってたら、そうだ、PIL使ってるんだからPILで生成すればいいじゃないか。
#-*- coding:utf-8 -*- import io from PIL import Image from django.test import TestCase from django.core.files.uploadedfile import SimpleUploadedFile class UploadFormTest(TestCase): def _get_form_class(self): from .forms import UploadForm return UploadForm def _make_dummy_image(self): file_obj = io.BytesIO() im = Image.new('RGBA', size=(10, 10), color=(256, 0, 0)) im.save(file_obj, 'png') file_obj.name = 'test.png' file_obj.seek(0) return file_obj def test_it(self): img = self._make_dummy_image() Form = self._get_form_class() form = Form( data={'title': 'test title'}, files={'photo': SimpleUploadedFile( img.name, img.read(), content_type='image/png', )}, ) self.assertTrue(form.is_valid())
ちなみに環境は Python 3.3 + Django 1.7.0.a です。ソースを追ってみると、form.is_valid() を呼ぶと内部で self.ImageField.to_python() が呼ばれ、さらにそのメソッド内で from django.utils.image import Image; Image.open() しているんですね。
というわけで一年以上ぶりの記事でした。
cajonでjavascriptを分割管理
JavaScript module loader for the browser that can load CommonJS/node and AMD modules
cajon(カホンって読むっぽい。ペルー発祥の打楽器の名前らしい)なんてものを見つけた。require.js上に構築されてるとかなんとか。require.jsもよく知らないんだけど、これはこれで簡単だしいいかも。
構成
.
├── index.html
└── js
├── cajon.js
├── controller.js
├── jquery.js
├── lib.js
├── model.js
├── template.js
└── view.js
index.html
<!DOCTYPE HTML> <html lang="ja"> <head> <meta charset="UTF-8"> <title>cajon test</title> <script src="js/cajon.js"></script> <script> require.config({ baseUrl: 'js' }); require(['jquery', 'lib'], function($, lib) { $.each(lib, function() { $('<li>').text(this.say()).appendTo('#hoge'); }); }); </script> </head> <body> <ul id="hoge"> </ul> </body> </html>
lib.js
module.exports = { template: require('./template'), model: require('./model'), controller: require('./controller'), view: require('./view') };
model.js
module.exports = { say: function() { return "I'm model.js" } };
view.js
module.exports = { say: function() { return "I'm view.js" } };
controller.js
module.exports = { say: function() { return "I'm controller.js" } };
template.js
module.exports = { say: function() { return "I'm template.js" } };
ブラウザでhtmlを開くと下のように表示される。
ちゃんとlib.js経由でmodel.js/view.js/controller.js/template.jsがそれぞれ読み込まれているのがわかる。