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

requirejs/cajon · GitHub

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を開くと下のように表示される。

f:id:kk6:20120922000441p:plain

ちゃんとlib.js経由でmodel.js/view.js/controller.js/template.jsがそれぞれ読み込まれているのがわかる。