Material UIを使ってカッコいいUIのReactアプリケーションを作ってみた
この記事はQiita React.js Advent Calendar 2015の11日目に投稿させて頂いた記事です。
背景
Material UIというプロジェクトをご存知だろうか?これはGoogle Material Designが提供しているUIパーツをReactのコンポーネントとして配布するプロジェクトである。自分はフロントエンドエンジニアじゃないし、デザインに時間かけたくないからとりあえずBootstrapでも適用しておくかみたいなケースは結構あると思う。僕もRailsで書いたAPIサーバの管理画面を開発した時、最初はBootstrapを使おうと思っていた。でも、Bootstrapのデザインも飽きてきたし他に気軽に使えるものはないかな、と探していた時に見つけたのがMaterial UI。Reactも書けるようになってきたので使ってみることにした。
成果物
React + Redux + Material UIのboilerplateを作った。理由は2015年9月頃はReactとReduxとMaterial UIのバージョンの制限が厳しく少しバージョンが変わるだけで利用できなかったり、Webpack, ESLint、Redux Routerなどの設定を毎回するのが面倒だったから。このboilerplateでちょっとしたアプリを作る時は以下に置きますので使って下さい。
Material UIで遊ぶ
Materia UIのウェブページにアクセスして、COMPONENTS
にアクセスすると利用可能なMeterial Designなコンポーネントが表示される。コンポーネントの挙動を確認するために実際に手元でコードをいじっても良いが、以下のようにサイト内でインタラクティブに各コンポーネントの動作を確認できるので、まずは遊んで見ることをお薦めする。
Material UIを使ってみる
実際にプロジェクトの開始からステップバイステップでReactアプリにMaterial UIを組み込み以下のような構成のReactアプリを作ってみる。[1]
<App> <Header /> <MainSection/> </App>
また、最終的なフォルダ構成はこうなる。
. ├── .eslintrc ├── components │ ├── Header.jsx │ └── MainSection.jsx ├── containers │ └── App.jsx ├── package.json ├── src │ ├── index.html │ ├── index.jsx │ ├── main.css │ └── material_ui_raw_theme_file.jsx ├── static │ ├── bundle.js │ ├── index.html │ └── main.css └── webpack.config.js
アプリ作成時のnodeのバージョンは3.3.6。
プロジェクトの準備
$ mkdir material_ui_sample $ cd material_ui_sample $ npm init $ vim package.json $ npm install $ touch webpack.config.js $ touch .eslinrc
ここで、Javascriptのモジュール管理はnpmで行うためvim package.json
の時に以下の内容を記述する。
{
"name": "react-redux-material_ui-boilerplate",
"version": "1.0.0",
"description": "A boilerplate for React + Redux + Material UI + ES6 syntax application",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --hot --inline --progress --colors",
"build": "webpack --progress --colors"
},
"repository": {
"type": "git",
"url": ""
},
"keywords": [
"React",
"webpack",
"Redux",
"Bable",
"ES6"
],
"author": "https://github.com/takanabe",
"license": "MIT",
"bugs": {
"url": ""
},
"homepage": "",
"devDependencies": {
"babel-core": "^5.8.25",
"babel-eslint": "^4.1.3",
"babel-loader": "^5.3.2",
"babel-runtime": "^5.8.25",
"css-loader": "^0.19.0",
"eslint": "^1.6.0",
"eslint-loader": "^1.1.0",
"eslint-plugin-react": "^3.5.1",
"fbjs": "^0.2.1",
"file-loader": "^0.8.4",
"style-loader": "^0.12.4",
"webpack": "^1.12.2",
"webpack-dev-server": "^1.12.0",
"webpack-hot-middleware": "^2.2.0"
},
"dependencies": {
"classnames": "^2.1.2",
"fbjs": "^0.3.2",
"material-ui": "^0.13.0",
"react": "^0.14.0",
"react-addons-create-fragment": "^0.14.0",
"react-addons-pure-render-mixin": "^0.14.0",
"react-addons-transition-group": "^0.14.0",
"react-addons-update": "^0.14.0",
"react-dom": "^0.14.0",
"react-hot-loader": "^1.3.0",
"react-redux": "^3.1.0",
"react-tap-event-plugin": "^0.2.0",
"redux": "^3.0.2"
}
}
また、Javascriptのビルド、ES6からES5へのトランスパイルはWebpack + babelを利用するものとして以下の設定を記載する。
module.exports = {
context: __dirname + "/src",
entry: {
jsx: "./index.jsx",
css: "./main.css",
html: "./index.html",
},
output: {
path: __dirname + "/static",
filename: "bundle.js",
},
module: {
preLoaders: [
//Eslint loader
{ test: /\.(js|jsx)$/, exclude: /node_modules/, loader: "eslint-loader"},
],
loaders: [
{ test: /\.html$/, loader: "file?name=[name].[ext]" },
{ test: /\.css$/, loader: "file?name=[name].[ext]" },
{ test: /\.(js|jsx)$/, exclude: /node_modules/, loaders: ["react-hot","babel-loader?stage=0&optional=runtime"]},
],
},
resolve: {
extensions: ['', '.js', '.jsx']
},
eslint: {
configFile: './.eslintrc'
},
};
この設定ファイルでsrc
ディレクトリにビルド前のファイルを、static
ディレクトリにビルド後のファイルを置くようにしている。また、JavascriptのLintingにはESLintを使うので以下を記載する。
{
"env": {
"es6": true,
"browser": true,
"node": true
},
"rules": {
"curly": 0,
"comma-dangle": [2, "never"],
"comma-spacing": 0,
"eqeqeq": [2, "allow-null"],
"key-spacing": 0,
"no-underscore-dangle": 0,
"no-unused-expressions": 0,
"no-shadow": 0,
"no-shadow-restricted-names": 0,
"no-extend-native": 0,
"no-var": 2,
"new-cap": 0,
"quotes": 0,
"semi-spacing": 0,
"space-unary-ops": 0,
"space-infix-ops": 0,
"consistent-return": 0,
"strict": 0
},
"parser": "babel-eslint",
"plugins": [
"react"
],
"ecmaFeatures": {
"arrowFunctions": true,
"jsx": true
}
}
最後にビルドのベースとなるファイルを作成する。
$ mkdir src $ echo "index.html\nindex.jsx\nmain.css" | xargs -I% touch src/%
ここで一度ビルドが通ることを確認する。
$ npm run build
コマンド実行後にstatic
ディレクトリが出来ており、その中にbundle.js
、index.html
、main.css
があれば準備完了。
Appコンポーネントの準備をする
src/index.jsx
を以下のように編集する。この時、Material UIで提供しているコンポーネントでタップイベントをキャッチするのにreact-tap-event-pluginが必要なことがあるので忘れずにimportする。これは、reactv1.0がリリースされるタイミングで必要なくなるとのこと。
import React from "react";
import ReactDOM from "react-dom";
import injectTapEventPlugin from "react-tap-event-plugin";
import App from '../containers/App';
//Needed for React Developer Tools
window.React = React;
//Needed for onTouchTap
//Can go away when react 1.0 release
//Check this repo:
//https://github.com/zilverline/react-tap-event-plugin
injectTapEventPlugin();
ReactDOM.render(
<App />,
document.getElementById("root")
);
このsrc/index.jsx
は`src/index.html
から読み込むようにする。
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Boilerplate</title>
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<div id="root"></div>
<!-- This script adds the Roboto font to our project. For more detail go to this site: http://www.google.com/fonts#UsePlace:use/Collection:Roboto:400,300,500 -->
<script>
var WebFontConfig = {
google: { families: [ 'Roboto:400,300,500:latin' ] }
};
(function() {
var wf = document.createElement('script');
wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
'://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
})();
</script>
<script src="./bundle.js"></script>
</body>
</html>
また、CSS周りを設定するときはsrc/main.css
を利用する。
html {
font-family: 'Roboto', sans-serif;
}
body {
font-size: 13px;
line-height: 20px;
margin: 0px;
}
続いて、Reactコンポーネントを作成する。
$ mkdir containers $ touch containers/App.jsx
ここで、HeaderとMainSectionは子コンポーネントとするためApp.jsx
は親コンポーネントを以下のように編集する。
import { connect } from 'react-redux';
import Header from '../components/Header';
import MainSection from '../components/MainSection';
class App extends Component {
render() {
return (
<div>
<Header />
<MainSection />
</div>
);
}
}
export default App;
Material UIを子コンポーネントで使う
お待たせしました、ようやくMaterial UIを使います!ここからがこの記事のメインです。
$ mkdir components $ echo "Header.jsx\nMainSection.jsx\n" | xargs -I% touch %
Header.jsx
ではMaterial UIのAppBarコンポーネントを使ってみる。Material UIのコンポーネントを使うときはimport文 + {AppBar}
のようにして使いたいコンポーネントを指定する。また、render
関数内では、Material UIの各コンポーネントのサンプルコードを真似てコンポーネントを使えば良い。
import React, { PropTypes, Component } from 'react';
import mui, {AppBar} from 'material-ui';
const ThemeManager = require('material-ui/lib/styles/theme-manager');
import MyRawTheme from '../src/material_ui_raw_theme_file'
class Header extends Component {
static get childContextTypes() {
return { muiTheme: React.PropTypes.object };
}
getChildContext(){
return { muiTheme: ThemeManager.getMuiTheme(MyRawTheme)};
}
render() {
return (
<header className="header">
<h1>AppBar Component</h1>
<AppBar title="React + Redux + Material UI Boilerplate" />
</header>
);
}
}
export default Header;
この時、src/material_ui_raw_theme_file.jsx
を以下のように用意すればMaterial UIの細かい設定も変更出来る。
let Colors = require('material-ui/lib/styles/colors');
let ColorManipulator = require('material-ui/lib/utils/color-manipulator');
let Spacing = require('material-ui/lib/styles/spacing');
module.exports = {
spacing: Spacing,
fontFamily: 'Roboto, sans-serif',
palette: {
primary1Color: Colors.cyan500,
primary2Color: Colors.cyan700,
primary3Color: Colors.lightBlack,
accent1Color: Colors.pinkA200,
accent2Color: Colors.grey100,
accent3Color: Colors.grey500,
textColor: Colors.darkBlack,
alternateTextColor: Colors.white,
canvasColor: Colors.white,
borderColor: Colors.grey300,
disabledColor: ColorManipulator.fade(Colors.darkBlack, 0.3)
}
};
同様に、MainSection.jsx
も編集する。MainSection.jsx
ではProgress,Tab,DataPikcerコンポーネントを使ってみる。それぞれimport文で呼び出してrender
関数内で使うだけである。
import React, { Component, PropTypes } from 'react';
import mui, {CircularProgress,
Tabs,
Tab,
DatePicker
} from 'material-ui';
class MainSection extends Component {
render() {
return (
<div>
<h1>Progress Component</h1>
<CircularProgress mode="indeterminate" size={1.5} />
<CircularProgress mode="indeterminate" color={"red"} size={2} />
<br/>
<h1>Tab Component</h1>
<Tabs>
<Tab label="Tab One" value="0" />
<Tab label="Tab Two" value="1" />
<Tab label="Tab Three" value="2" />
</Tabs>
<br/>
<h1>DatePicker Component</h1>
<DatePicker hintText="Portrait Dialog" />
<br/>
</div>
);
}
}
export default MainSection;
作ったReactアプリにアクセスしてMaterial UIコンポーネントを確認する。nodeの開発用サーバを使って、作ったReactアプリにアクセスするには以下を実行後、ブラウザを開きlocalhost:8080
にアクセスする。
$ npm start
すると、このようにMaterial UIのコンポーネントが並んで表示される。
便利だ!
使ってみた感想
別件で、Material UIを使ってこんな感じの管理画面アプリを作った。こいつがReact + Redux + Material UIな構成で初めて作ったフロントエンドアプリケーションになった。
ご覧の通りカッコいいUIが実現できる。ただし、Material UIコンポーネントに細かい挙動を追加したりする場合、material-ui/docs/src/app/components/pages/components at master · callemall/material-ui · GitHub を見たり、コンポーネントごとのコードを読んだりする必要があり結構苦労した。そういう所で時間を無駄にしたくない人は枯れているBootStrapを使った方がいいと思う。
参考
[1] 若干冗長だが自分の備忘録とアウトプットを兼ねている。