BootstrapのGridをReactに持ってきてみた。

material-uiを気にいって、それでReactアプリ書いてます。発展途上ではあるも開発者の方々がこまめにがっつり頑張ってくれてて、日々npm updateをかけるのが楽しみです。そんなmaterial-uiも今の所はGridレイアウトのコンポーネントはありません。Google謹製マテリアルデザインのCSSライブラリであるMaterial Design Lightは画面幅を12分割するというBootstrap近似仕様のGridレイアウトがありますので、まあ、対抗してmaterial-uiにもそのうち作られるかな。

Reactコンポーネントとしてもいくつかあるようですが、今の所良さそうなものが見つからなかったので暫定的にBootstrapを持ち込んで軽め対応してみました。スタイルはインラインで書くようにしていたので、Gridだけとはいえcssファイルに外だしちゃうのがちょっと嫌だったけど、手もかけたくなかったので素に近い使い方に止めてます。

$ npm install --save bootstrap-sass

いろいろ試行錯誤してみた結果、SASS版Bootstrapから必要箇所を取り込んで使うことにしました。上記npmインストールでnodo_modules配下にSASSソースの最新版が管理されます。

/* /src/sass/style.scss */
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap-sprockets";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/mixins";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/grid";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/scaffolding";
@import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities";

自分のスタイルシートを作って必要なだけimport。これをgulp-sassでコンパイル

// gulpfile.js
'use strict';
var gulp = require('gulp');
var sass = require('gulp-sass');

gulp.task('SASS', function() {
    gulp.src('src/sass/*.scss')
        .pipe(sass())
        .pipe(gulp.dest('dest/public/css'));
});

コンポーネントはcontainer-row-colの組み合わせ。

// BootstrapResponsive.js
'use strict';
const React = require('react');

function ResponsiveContainer(props) {
    return (
        <div className={props.fullWidth ? 'container-fluid' : 'container'}>
            {props.children}
        </div>
    );
}

ResponsiveContainer.propTypes = {
    fullWidth: React.PropTypes.bool,
    children: React.PropTypes.node.isRequired
};

function ResponsiveRow(props) {
    return (
        <div className='row'>
            {props.children}
        </div>
    );
}

ResponsiveRow.propTypes = {
    children: React.PropTypes.node.isRequired
};

function ResponsiveCol(props) {
    const className = ['xs', 'sm', 'md', 'lg'].map((type) => {
        const value = props[type];
        var result = [];
        if(value) {
            var valueSize = 0;
            if (typeof value === 'number') {
                valueSize = value;
            } else {
                const {size, offset, visible, block, inline, hidden} = value;
                valueSize = size;
                if (0 <= offset && offset <= 12) {
                    result.push('col-' + type + '-offset-' + offset);
                }
                if (visible) {
                    result.push('visible-' + type + '-' + (inline ? (block ? 'inline-block' : 'inline') : 'block'));
                }
                if (hidden) {
                    result.push('hidden-' + type);
                }
            }
            if (1 <= valueSize && valueSize <= 12) {
                result.unshift('col-' + type + '-' + valueSize);
            }
        }
        return result.length > 0 ? result.join(' ') : '';
    }).join(' ');
    return (
        <div className={className}>
            {props.children}
        </div>
    );
}

const responsivePropsType = React.PropTypes.oneOfType([
    React.PropTypes.number,
    React.PropTypes.object
]);

ResponsiveCol.propTypes = {
    xs: responsivePropsType,
    sm: responsivePropsType,
    md: responsivePropsType,
    lg: responsivePropsType,
    children: React.PropTypes.node.isRequired
};

export {ResponsiveContainer, ResponsiveRow, ResponsiveCol};

今回はステートレスなコンポーネントだったのでシンプルに関数で書いてみました。公式ドキュメントReusable Components | React)の説明以外に見ない書き方ですが不要なお決まりイディオム抜きで良い感じだと思います。今回は一発で書いちゃってますがロジックを細かく関数分割すればテストもしやすいでしょう。

およそBootstrapのGridシステムについてのドキュメント(CSS · Bootstrap #gridと CSS · Bootstrap #responsive-utilities-classes)に書かれていたものの8割ぐらいは実装しました。まあdivタグにクラス文字列埋めるだけですからね。残したのはclearfixとpull/push。やっても直接divタグ書くのと何が違うのかって感じだからもっと抽象度を高める利用アイディアなきゃいらないかなと思います。

const React = require('react');
import {Card, CardText} from 'material-ui';
import {ResponsiveContainer, ResponsiveRow, ResponsiveCol} from './BootstrapResponsive';

/*中略*/

<ResponsiveContainer fullWidth={true}>
    <ResponsiveRow>
        <ResponsiveCol xs={{size: 12}} sm={6} md={{size: 8, offset: 2}} lg={{size: 4, offset: 0}}>
            <Card>
                <CardText>
                    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                    Donec mattis pretium massa. Aliquam erat volutpat. Nulla facilisi.
                    Donec vulputate interdum sollicitudin. Nunc lacinia auctor quam sed pellentesque.
                    Aliquam dui mauris, mattis quis lacus id, pellentesque lobortis odio.
                </CardText>
            </Card>
        </ResponsiveCol>
        <ResponsiveCol xs={{hidden: true}} sm={6} md={{size: 8}} lg={{size: 4}}>
            <Card>
                <CardText>
                    Lorem ipsum dolor sit amet, consectetur adipiscing elit.
                    Donec mattis pretium massa. Aliquam erat volutpat. Nulla facilisi.
                    Donec vulputate interdum sollicitudin. Nunc lacinia auctor quam sed pellentesque.
                    Aliquam dui mauris, mattis quis lacus id, pellentesque lobortis odio.
                </CardText>
            </Card>
        </ResponsiveCol>
    </ResponsiveRow>
</ResponsiveContainer>
  • xsサイズでは全幅の一つと、もう一つは消す。
  • smサイズでは横二分割。
  • mdサイズでは左右余白2で8幅。縦に並べる。
  • lgサイズでは4幅で横に並べる。

やってることは原始的だけど意外に使える。外出しになってるBootstrapのCSSコンポーネントにインライン化できればいいんじゃないかな。css擬似要素とメディアクエリをインライン縛りにどう移植するか。。。擬似要素はspanを動的に足してくかだな。メディアクエリはScriptで幅をとって計算。。。サーバ描画でうまくないなあ。