Meteor and Reactによるリアクティブシステム「ラーメン野郎を追いかけろ! @ Twitter」を作ってみた
Falcor+Reactフルスタック(開発環境)
Falcor+Reactフルスタック(react-router)
Falcor+Reactフルスタック(views層とcomponents層)
Falcor+Reactフルスタック(formsy-react)
Falcor+Reactフルスタック(エラー処理)
Falcor+Reactフルスタック(Material-UI)
今回はMaterial-UIについてです。小池都知事は都民ファーストですが、最近のJavaScriptアプリはモバイルファーストだそうです。ちなみに小沢一郎は「生活が第一」でした。
従来は、TwitterのBootstrapなどが多くの人に採用されて、モバイルでもPCでもきれいにデザインされたWebアプリが作られてきました。最近はGoogleがMaterial Design Liteというものを公開して、Bootstrapの置き換えに使われてきつつあるようです。それぞれのメリットだけを簡単に言えば、Bootstrapは実績と膨大なリソースがあり、Materialはモダンなデザインが売りなようです。しかし私はBootstrapもMaterialも生で使うには理解が難しいほど複雑で敬遠していました(コピペ文化なんでしょうけど)。しかしReactではどちらも使い易くcomponent化されたライブラリがあり、複雑さが隠蔽され気持ちよく使えるようになっています。その1つがMaterial-UIです。Reactプログラマは、これを使えば複雑で難解な「デザイン」(しかもモバイルファーストです)から解放されるといううわけです。積極的に使いたいものです。
http://www.material-ui.com/
今回のサンプルプログラムで新しいMaterial-UIのCard componentを導入してみたいと思います。まずはトップのホームページを一見してから、この続きを読まれることをお勧めします。
http://www.mypress.jp:3021/
前にも述べましたが、トップcomponentのroutes.jsでテーマを設定しています。短いソースなので再度リストします。テーマさえ設定すれば、後は好きな場所で、好きなMaterial-UI componentを使うことが可能になります。
import React from 'react';
import {render} from 'react-dom'
import {BrowserRouter} from 'react-router-dom'
//import darkBaseTheme from 'material-ui/styles/baseThemes/darkBaseTheme';
import lightBaseTheme from 'material-ui/styles/baseThemes/lightBaseTheme'; <==テーマをimport
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import AppBar from 'material-ui/AppBar';
import App from './App';
import PageNotFound from './components/PageNotFound'
const target = document.getElementById('appRoot');
const node =(
<MuiThemeProvider muiTheme={getMuiTheme(lightBaseTheme)}> <==テーマでアプリ全体をラッピング
<BrowserRouter>
<App />
</BrowserRouter>
</MuiThemeProvider>
);
render(node, target);
次に、ホームページを改良します。ホームページには以下のような記事(articles)を掲載することとしましょう。この記事表示のデザインにCardを使うつもりです。
[
{
name: '山田太郎',
avatar: 'avatar1.png',
title: '花火大会',
content: '今日は隅田川の花火大会でした。とても美しかったです。人が大勢いました。(本当は雨で辛かったけど)'
},
{
name: '鈴木花子',
avatar: 'avatar2.png',
title: 'プールで遊んだ',
content: '東武動物公園のプールに行きました。大きなプールでいっぱいありました。とても楽しかったです。(天気悪くて寒かったけど)'
},
{
name: '魔法使いサリー',
avatar: 'avatar3.png',
title: '魔法を使えるとしたら',
content: 'やはり、夏なので天気をよくする魔法を使いたいです。魔法で温暖化や氷河期の到来も防ぎたいです。'
}
]
この記事をコマンドラインからMongoDBにimportします
mongoimport --db qiita9db --collection articles --jsonArray initData.js --host=127.0.0.1
プログラムの修正です。まずはarticlesコレクションを追加しましたのでMongooseのスキーマを追加します。
const articleSchema = {
name:String,
avatar:String,
title:String,
content:String
};
const Article = mongoose.model('Article', articleSchema, 'articles');
クライアント側のホームページからarticlesを取得しますので、サーバ側のfalcor routerに以下の2つのパスを追加します。
{
route: 'articles.length',
get: () => Article.count({}, (err, count) => count)
.then ((articlesCountInDB) => {
return {
path: ['articles', 'length'],
value: articlesCountInDB
};
})
},
{
route: 'articles[{integers}]["id","name","avatar","title","content"]',
get: (pathSet) => {
const articlesIndex = pathSet[1];
return Article.find({}, (err, articlesDocs) => articlesDocs)
.then ((articlesArrayFromDB) => {
let results = [];
articlesIndex.forEach((index) => {
const singleArticleObject = articlesArrayFromDB[index].toObject();
const falcorSingleArticleResult = {
path: ['articles', index],
value: singleArticleObject
};
results.push(falcorSingleArticleResult);
});
return results;
});
}
},
この2つのパスにクライアント側のホームページからアクセスしてarticlesを取得します。falcor的に言ううとVirtual JSONにアクセスします。以下のようになります。
import React from 'react';
import Falcor from 'falcor';
import falcorModel from '../falcorModel.js';
import ArticleCard from '../components/ArticleCard';
export default class HomeApp extends React.Component {
constructor(props) {
super(props);
this.state = {
articles: null
};
}
componentWillMount() {
this._fetch();
}
async _fetch() {
const articlesLength = await falcorModel
.getValue('articles.length')
.then((length) => length);
const articles = await falcorModel
.get(['articles', {from: 0, to: articlesLength-1}, ['_id','name','avatar','title', 'content']])
.then((articles) => articles.json.articles);
this.setState({articles: articles});
}
render () {
let articlesJSX = [];
for (let idx in this.state.articles) {
const article = this.state.articles[idx];
let currentArticleJSX = (
<div key={idx}>
<ArticleCard name={article.name} avatar={article.avatar} title={article.title} content={article.content} />
</div>
);
articlesJSX.push(currentArticleJSX);
}
return (
<div>
<h1>私のホームページへようこそ !!!</h1>
<div style={{height: '100%', width: '75%', margin: 'auto'}}>
{articlesJSX}
</div>
</div>
);
}
}
HomeApp.jsの修正には2つのポイントがあります。1つ目はfalcorを使ってarticlesを取得していること。2つ目はArticleCardという下層のcomponentを呼び出して、取得したarticlesを描画していること、です。前々回で述べた言葉で言えば、HomeApp.jsはViews層に属し、下層のComponents層のArticleCardを呼び出している。ArticleCardではMaterial-UIのCard componentを利用して具体的な描画を定義している、ということです。
Material-UIのCard componentは、関係ある複数のコンテンツをまとめて表示するためのものです。このサンプルでは記事タイトル、内容、アバター画像、名前、背景画像などをカードの枠組のなかに表示しています。ArticleCard componentを確認してください。CSSの記述を除けば短いものです。
import React from 'react';
import { Card, CardHeader, CardMedia, CardTitle, CardText } from 'material-ui/Card';
import { Paper } from 'material-ui';
class ArticleCard extends React.Component {
constructor(props) {
super(props);
}
render() {
let {name,avatar,title,content} = this.props;
let avatar1 = "/"+avatar;
let paperStyle = {
padding: 10,
width: '100%',
height: 300
};
let leftDivStyle = {
width: '30%',
float: 'left'
}
let rightDivStyle = {
width: '60%',
float: 'left',
padding: '10px 10px 10px 10px'
}
return (
<Paper style={paperStyle}>
<CardHeader title={name} subtitle="サブタイトル(名前)" avatar={avatar1}/>
<div style={leftDivStyle}>
<Card >
<CardMedia overlay={<CardTitle title={title} subtitle="サブタイトル(画像)" />}>
<img src="/placeholder.jpg" height="140" />
</CardMedia>
</Card>
</div>
<div style={rightDivStyle}>
<div dangerouslySetInnerHTML={{__html: content}} />
</div>
</Paper>);
}
};
export default ArticleCard;
以上で今回の説明は終わりますが、Falcorシリーズはひとまず今回で終わりたいと思います。
次回は Meteor + React について少し書きたいと思います。