Material-UIのAvatarを使用する(最近の更新編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
Material-UIを使用して最近の更新にユーザアイコンを表示します。
"最近の更新”にAvatarを表示する
Cloud Functionsの変更
"最近の更新”にユーザアイコン(Avatar)を表示します。
先に、Cloud Functionsを変更して、/recentUpdatedTodos
パスに登録する内容にavatarUrl
を追加します。
functions/index.js
const addRecentUpdate = (uid, todoId, todo, eventType) => { return admin.database().ref('/users/' + uid).once('value').then((snapshot) => { // パスを '/users/' + uid に変更 const user = snapshot.val(); // 変更 return (admin.database().ref('/recentUpdatedTodos/' + todoId).set({ uid, displayName: user.displayName, // 変更 avatarUrl: user.avatarUrl, // 追加 text: todo.text, eventType, _updatedAt: todo._updatedAt })); }); }
デプロイします。
$ cd todo-sample $ firebase deploy --only functions
動作確認します。
タスク完了フラグを切り替えます
データベースのデータを確認します。recentUpdatedTodos以下のデータにavatarUrlが追加されました。
Avatarを表示する
Avaterを最近の更新に表示します。
src/components/dashboard/recentUpdatedTodos/UserUpdatedTodo.js
({/* xxx */}
のコメントは実行前に削除してください)
import Avatar from '@material-ui/core/Avatar' // 追加 import ListItemAvatar from '@material-ui/core/ListItemAvatar' // 追加 import PersonIcon from '@material-ui/icons/Person' // 追加 const UserUpdatedTodo = ({text, eventType, uid, displayName, avatarUrl, _updatedAt}) => ( // avatarUrlを追加 <ListItem divider button component={Link} to={`/users/${uid}/todos`}> {/* 追加ここから */} <ListItemAvatar> {avatarUrl ? // #1 <Avatar alt={displayName} src={avatarUrl} /> {/* #2 */} : <Avatar> <PersonIcon /> {/* #3 */} </Avatar> } </ListItemAvatar> {/* 追加ここまで */} <ListItemText secondary={moment(_updatedAt).fromNow()}> {displayName}さんが {text} を{ eventType === 'CREATE' ? '作成' : '更新'}しました。 </ListItemText> </ListItem> ) UserUpdatedTodos.propTypes = { // 略 avatarUrl: PropTypes.string, // 追加 }
- avatarUrlの有無によって処理を分けます(#1)。
- avatarUrlが存在する場合は、そのUrlの画像を表示します(#2)。Avatarコンポーネントを使用します。
- avatarUrlが存在しない場合は、PersonIconを表示します(#3)。
実行結果です。リストにユーザアイコン(Avatar)が追加されました。 avatarUrlが無い場合にはPersonIconが表示されます。
以上で最近の更新リストにAvatarを表示することができました。
参考
★次回の記事
★目次
Material-UIのMenuを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
ログアウトのボタンの常時表示をやめ、ユーザ名がクリックされた時にログアウトメニューが出るように変更します。Material-UIのMenuを使用します。
Loginコンポーネントの内部stateにanchor(アンカー:ここではメニューの表示対象)を持つので、関数コンポーネントからReact.Componentをを拡張したクラスへと変更します。
公式のMenuのデモを参考に変更していきます。
src/components/header/Login.js
({/* xxx */}
のコメントは実行前に削除してください)
import { compose } from 'redux' // 追加 import { withStyles } from '@material-ui/core/styles' // 追加 import Menu from '@material-ui/core/Menu' // 追加 import MenuItem from '@material-ui/core/MenuItem' // 追加 // stylesを追加 const styles = { noTransform: { textTransform: 'none', // #1 }, } // 関数コンポーネントからReact.Componentを拡張したクラスに変更 class Login extends React.Component { state = { anchorEl: null, // #2 } handleClick = event => { // #3 this.setState({ anchorEl: event.currentTarget }) } handleClose = () => { // #4 this.setState({ anchorEl: null }) } render() { const { auth, loginWithGoogle, logout, classes } = this.props // classesを追加 const { anchorEl } = this.state if (!isLoaded(auth)) { return <CircularProgress color="inherit" /> } if (isEmpty(auth)) { return ( <Button variant="contained" color="primary" onClick={loginWithGoogle} className={classes.noTransform}>Googleアカウントでログイン</Button> {/* classNameを追加 */} ) } return ( <React.Fragment> <Button color="inherit" aria-owns={anchorEl ? 'user-menu' : undefined} aria-haspopup="true" onClick={this.handleClick} className={classes.noTransform}> {/* #5 */} {auth.displayName} さん </Button> <Menu // #6 id="user-menu" anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={this.handleClose} > <MenuItem onClick={logout}>ログアウト</MenuItem> {/* #7 */} </Menu> </React.Fragment> ) } } Login.propTypes = { // 略 classes: PropTypes.object.isRequired, // 追加 } // 略 // composeを使用してwithStylesとconnectを合成するように変更 export default compose( withStyles(styles), connect( mapStateToProps, mapDispatchToProps ))(Login)
- Material-UIのボタンは、デフォルトでは全てのアルファベットは大文字に変換されます。小文字を小文字のまま表示したい場合は
textTransform: 'none'
を指定します(#1)。 anchorEl
はメニューを表示する対象です。初期値はnull
を設定します(#2)。- ユーザ名がクリックされた時の動作を定義します(#3)。
anchorEl
にクリックされた要素を設定します。 - メニューが閉じられる時の動作を定義します(#4)。
anchorEl
にnull
を設定し、表示対象をなくします。 - ユーザ名の表示(
{auth.displayName}さん
)をButton
で囲みます(#5)。aria-owns
はメニュー要素のidを指定し、aria-haspopup
はtrueを指定します。詳しくは参考のリンクを参照ください。 - 公式のデモを参考にメニューの設定をします(#6)。
anchorEl
要素にstate
のanchorEl
を指定します。open
はanchorEl
が存在するかを指定しています。 - メニューの各項目を
MenuItem
として追加します(#7)。
実行結果です。ログアウトボタンは通常では見えません。
ユーザ名をクリックするとログアウトメニューが表示されました。
"ログアウト"をクリックすると正常にログアウトします。
以上でMenuを使用してログアウトメニューを表示することができました。
参考
★次回の記事
★目次
Material-UIのAppBarを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
Appbarはいわゆるwebページのヘッダーで、ロゴやメニューなどを表示するのに使用します。 今回はログイン機能とナビゲーション機能をAppbarに載せます。ディレクトリ階層の変更とファイル名の変更も行います。
変更前と変更後のコンポーネントの位置関係は以下のようになります。
ディレクトリ階層の変更
Appbarのコンポーネントをheaderディレクトリにまとめます。
src/components/header
ディレクトリを作成します。src/components/login/index.js
をsrc/components/header/Login.js
にリネームします。src/components/navbar/index.js
をsrc/components/header/MenuIcons.js
にリネームします。
src/components/header/MenuIcons.js
のNavbar
変数をMenuIcons
変数にリネームします。
const MenuIcons = ({uid}) => { // 変更 // 略 } MenuIcons.propTypes = { // 変更 // 略 } MenuIcons = withRouter(connect( // 変更 mapStateToProps )(MenuIcons)) // 変更 export default MenuIcons // 変更
headerにAppbarとToolbarを使用する
Appbarを作成していきます。
src/components/header/index.js
(新規作成)
import React from 'react' import AppBar from '@material-ui/core/AppBar' import Toolbar from '@material-ui/core/Toolbar' import MenuIcons from './MenuIcons' import Login from './Login' const Header = () => ( <AppBar> <Toolbar> <MenuIcons /> <Login /> </Toolbar> </AppBar> ) export default Header
headerコンポーネントをAppコンポーネントに追加します。
src/components/App.js
import PropTypes from 'prop-types' // 追加 import { withStyles } from '@material-ui/core/styles' // 追加 import Header from './header/' // 追加 // import Navbar from './navbar/' は削除 // import Login from './login/' は削除 // stylesを追加 const styles = theme => ({ toolbar: theme.mixins.toolbar, }) const App = ({classes}) => ( // classesを追加 <BrowserRouter> <div> <CssBaseline /> {/* <Login /> は削除 */} {/* <Navbar /> は削除 */} <Header /> {/* 追加 */} <div className={classes.toolbar} /> {/* #1 */} <Switch> {/* 略 */} </Switch> </div> </BrowserRouter> ) // propTypesを追加 App.propTypes = { classes: PropTypes.object.isRequired, } export default withStyles(styles)(App) // 変更
- Headerの下に、
classes.toolbar
を適用した<div>
を追加します(#1)。Appbarはフローティングしているので、下のメニューを、toolbarの高さぶん下にする必要があるため、この要素を追加しています。試しにこのdiv要素を外してみると、最近の更新とAppbarが重なってしまうことがわかります。
ここまでの実行結果です。
今回はAppbarのpositionがデフォルト設定のfixedなので、 スクロールしてもAppbarが上部に表示されます。
Appbarとブラウザの端の間に空白が開いてしまう場合は、<CssBaseline>
の設定が足りていないので、下記記事を参考に設定してください。
タイトルの追加とMenuIconの変更
左側にタイトルを表示してHomeへのリンクを作成します。右側にタスク一覧へのリンクとログイン機能を配置します。
src/components/header/index.js
import { Link } from 'react-router-dom' // 追加 import PropTypes from 'prop-types' // 追加 import { withStyles } from '@material-ui/core/styles' // 追加 import Typography from '@material-ui/core/Typography' // 追加 // stylesを追加 const styles = theme => ({ grow: { flexGrow: 1, }, }) const Header = ({ classes }) => ( // classesを追加 <AppBar> <Toolbar> {/* Typographyを追加 */} <Typography variant="h6" color="inherit" component={Link} to="/"> タスク管理アプリ </Typography> <div className={classes.grow}></div> {/* 追加 */} <MenuIcons /> <Login /> </Toolbar> </AppBar> ) // propTypesを追加 Header.propTypes = { classes: PropTypes.object.isRequired, } // withStylesで囲む export default withStyles(styles)(Header)
タスク編集へのリンクはIconButton
を使用します。Tooltipを使用して、ボタンにマウスカーソルが重なると"編集"というテキストが現れるようにします。
src/components/header/MenuIcons.js
({/* xxx */}
のコメントは実行前に削除してください)
import { Link } from 'react-router-dom' // 追加 // import { NavLink, withRouter } from 'react-router-dom' は削除 import IconButton from '@material-ui/core/IconButton' // 追加 import Tooltip from '@material-ui/core/Tooltip' // 追加 import EditIcon from '@material-ui/icons/Edit' // 追加 const MenuIcons = ({uid}) => ( {/* 既存のコードは削除して書き直す */} <React.Fragment> { uid && <Tooltip title="編集"> <IconButton color="inherit" component={Link} to={`/users/${uid}/todos`} aria-label="編集"> <EditIcon /> </IconButton> </Tooltip> } </React.Fragment> ) // withRouterを削除 export default connect( mapStateToProps )(MenuIcons)
実行結果です。Appbarの左側にタイトル、右側に編集へのリンクとログイン表示がされました。リンクが機能していることを確認してください。
以上でログイン機能とナビゲーション機能をMaterial-UIのAppBarに載せることができました。
参考
★次回の記事
★目次
withStylesによるスタイルの適用(タスク一覧編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
タスク一覧にスタイルを適用する
前回と同様にタスク一覧にもスタイルを適用します。Paperの周りにマージンを設定し、Paperの中に余白を設定します。
src/components/todos/index.js
import { compose } from 'redux' // 追加 import { withStyles } from '@material-ui/core/styles' // 追加 // stylesを追加 const styles = theme => ({ todoListRoot: { padding: theme.spacing.unit * 3, }, todoListContent: { maxWidth: 950, padding: theme.spacing.unit * 3, marginLeft : 'auto', marginRight : 'auto', }, }) class TodoComponent extends React.Component { // 略 render() { const {isOwnTodos, match: { params: {uid}}, classes} = this.props; // classesを追加 return ( <div className={classes.todoListRoot}> {/* classNameを追加 */} <Paper className={classes.todoListContent}> {/* classNameを追加 */} {/* 略 */} </Paper> </div> ) } } TodoComponent.propTypes = { // 略 classes: PropTypes.object.isRequired, // 追加 } // composeを使用してwithStylesとconnectを合成するように変更 export default compose( withStyles(styles), connect( mapStateToProps, mapDispatchToProps ))(TodoComponent)
実行結果です。Paperとナビボタンとの間が空きました。Paperの幅が最大950pxに制限され、左右均等に余白ができました。Paperの内部にも余白ができています。
続いて読み込み中のぐるぐるとタスクなしの文言の周りにも余白を追加します。
src/components/todos/TodoList.js
import { withStyles } from '@material-ui/core/styles' // 追加 // stylesを追加 const styles = theme => ({ message: { marginTop: theme.spacing.unit * 2, marginBottom: theme.spacing.unit * 2, }, }) const TodoList = ({displayName, todos, isOwnTodos, onTodoClick, classes}) => { // classesを追加 if (!isLoaded(todos)) { return <CircularProgress className={classes.message} /> // classNameを追加 } if (isEmpty(todos)) { return <Typography variant="body1" className={classes.message}>タスクがありません。</Typography> // classNameを追加 } // 略 } TodoList.propTypes = { // 略 classes: PropTypes.object.isRequired, // 追加 } export default withStyles(styles)(TodoList) // 変更
実行結果です。ぐるぐるとタスクなしの文言の周りに余白が追加されました。
以上でwithStylesを使用してタスク一覧にスタイルを適用することができました。
参考
★次回の記事
★目次
withStylesによるスタイルの適用(最近の更新編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
Material-UIにおいて個別にスタイルを適用するには、stylesオブジェクト(またはstyles関数)とwithStyleを使用します。
前回適用したPaperの周りにマージンと中に余白(padding)を設定します。
ダッシュボードにスタイルを適用する
ダッシュボートのマージンと余白を設定します。
src/components/dashboard/index.js
({/* xxx */}
のコメントは実行前に削除してください)
import PropTypes from 'prop-types' // 追加 import { withStyles } from '@material-ui/core/styles' // 追加 // stylesを追加 const styles = theme => ({ // #1 root: { padding: theme.spacing.unit * 5, }, content: { maxWidth: 800, marginLeft : 'auto', marginRight : 'auto', }, }) const Dashboard = ({classes}) => ( {/* #2 */} <div className={classes.root}> {/* #3 */} <div className={classes.content}> {/* #4 */} <RecentUpdatedTodos /> </div> </div> ) // propTypesを追加 Dashboard.propTypes = { classes: PropTypes.object.isRequired, } export default withStyles(styles)(Dashboard) // #5
theme
を引数に取る関数styles
を定義します(#1)。theme
はUIの設定が入ったオブジェクトです。デフォルトの設定は Default Theme - Material-UI に書いてあります。- (#1)で設定したcss設定を
classes
という名前で下位コンポーネントに渡します(#5)。withStyles
という関数を使用します。 - Dashboardコンポーネントで
classes
プロパティを受け取ります(#2)。 className
にstylesで定義した内容を指定します(#3, #4)。
実行結果です。Paperとナビボタンとの間が空きました。Paperの幅が最大800pxに制限され、左右均等に余白ができました。
Google Cromeのデベロッパーツールでも確認できました。
引き続き”最近の更新”の文字周りと読み込み中表示のCircularProgress、"データがありません。”の表示の周りにmargin及びpaddingを適用します。
withStyles関数をfirebaseConnectやconnectと同時に使う時は、compose関数の中に並べます。
src/components/dashboard/recentUpdatedTodos/index.js
import { withStyles } from '@material-ui/core/styles' // 追加 // stylesを追加 const styles = theme => ({ title: { paddingTop: theme.spacing.unit * 3, paddingRight: theme.spacing.unit * 3, paddingLeft: theme.spacing.unit * 3, }, message: { marginTop: theme.spacing.unit * 2, marginBottom: theme.spacing.unit * 2, marginRight: theme.spacing.unit * 3, marginLeft: theme.spacing.unit * 3, } }) const RecentUpdatedList = (todos, classes) => { // classesを追加 if (!isLoaded(todos)) { return <CircularProgress className={classes.message} /> // classNameを追加 } if (isEmpty(todos)) { return <Typography variant="body1" className={classes.message}>データがありません。</Typography> // classNameを追加 } // 略 } let RecentUpdatedTodos = ({todos, classes}) => { // classesを追加 return ( <Paper> <Typography variant="h5" className={classes.title}>最近の更新</Typography> {/* classNameを追加 */} {RecentUpdatedList(todos, classes)} {/* classesを引数に追加 */} </Paper> ) } RecentUpdatedTodos.propTypes = { // 略 classes: PropTypes.object.isRequired, // 追加 } RecentUpdatedTodos = compose( withStyles(styles), // 追加 firebaseConnect(firebaseQueries), connect( mapStateToProps ))(RecentUpdatedTodos)
実行結果です。”最近の更新”の文字とPaperの縁の間に余白ができました。
読み込み中のぐるぐるの周りにも余白を追加しました。
以上でwithStylesを使用してダッシュボードにスタイルを適用することができました。
★次回の記事
★目次
Material-UIのPaperを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
Material-UIのPaperを使用し、コンテンツが紙の上に載っているようなUIにします。 最新の更新一覧とタスク一覧をPaperの上に載せます。
最新の更新一覧でPaperを使う
最新の更新をPaperで囲みます。<div>
を<Paper>
に変更します。
src/components/dashboard/recentUpdatedTodos/index.js
({/* xxx */}
のコメントは実行前に削除してください)
import Paper from '@material-ui/core/Paper' // 追加 let RecentUpdatedTodos = ({todos}) => { return ( <Paper> {/* 変更 */} <Typography variant="h5">最近の更新</Typography> {RecentUpdatedList(todos)} </Paper> {/* 変更 */} ) }
実行結果です。白い背景になりました。次回でマージンを入れると背景から浮いた感じがわかるようになります。
タスク一覧でPaperを使う
タスク一覧をPaperで囲みます。
今後の実装のため、ルート要素の<div>
は残しておいて、その下に<Paper>
コンポーネントを挿入します。
src/components/todos/index.js
import Paper from '@material-ui/core/Paper' // 追加 class TodoComponent extends React.Component { // 略 render() { const {isOwnTodos, match: { params: {uid}}} = this.props; return ( <div> <Paper> {/* 追加 */} <Title isOwnTodos={isOwnTodos} uid={uid} /> {isOwnTodos && <AddTodo uid={uid} />} <Notice /> <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} /> <Footer /> </Paper> {/* 追加 */} </div> ) } }
実行結果です。こちらも白い背景になり、グレーの全体部分から浮いたようになりました。
以上でコンテンツをPaperに載せることができました。
参考
★次回の記事
★目次
Material-UIのTypographyを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)
★前回の記事
Material-UIのコンポーネントで囲まれていない文字列をTypographyで囲っていきます。 なお、一部はこの後のページでMaterial-UIのコンポーネントに変換するので、変換しない文字列もあります。
NoMatchページ
始めにNoMatch.jsを変更します。variantにh2
を指定します。
src/components/NoMatch.js
({/* xxx */}
のコメントは実行前に削除してください)
import Typography from '@material-ui/core/Typography' // 追加 const NoMatch = () => ( <Typography variant="h2">ページが見つかりません</Typography> {/* 変更 */} )
実行結果です。
ダッシュボード画面
"最近の更新”と”データがありません”の文字列をTypographyで囲みます。
src/components/dashboard/recentUpdatedTodos/index.js
import Typography from '@material-ui/core/Typography' // 追加 const RecentUpdatedList = (todos) => { // 略 if (isEmpty(todos)) { return <Typography variant="body1">データがありません。</Typography> // 変更 } // 略 } let RecentUpdatedTodos = ({todos}) => { return ( <div> <Typography variant="h5">最近の更新</Typography> {/* 変更 */} {RecentUpdatedList(todos)} </div> ) }
実行結果です。
タスク一覧のコンポーネントの順番変更
タスク一覧の文字列をTypographyに変更する前にコンポーネントの順番を変更します。
タスク追加フォーム “{ユーザ名}のタスク一覧” タスク一覧
という順番から
“{ユーザ名}のタスク一覧” タスク追加フォーム タスク一覧
という順番に変更します。
ユーザ名を表示するのコンポーネントはTitle
という名前のコンポーネントにします。
src/components/todos/Title.js
(新規作成)
import React from 'react' import { compose } from 'redux' import { connect } from 'react-redux' import { firebaseConnect } from 'react-redux-firebase' import PropTypes from 'prop-types' const Title = ({displayName, isOwnTodos}) => { const name = isOwnTodos ? 'あなた' : `${displayName} さん`; return ( <React.Fragment> {displayName && <div>{name}のタスク一覧</div>} </React.Fragment> ) } Title.propTypes = { displayName: PropTypes.string, isOwnTodos: PropTypes.bool.isRequired, } const firebaseQueries = ({uid}) => ( [ {path: `users/${uid}/displayName`, type: 'once'}, ] ) const mapStateToProps = ({firebase: {data : {users}}}, {uid}) => ({ displayName: users && users[uid] && users[uid].displayName, }) export default compose( firebaseConnect(firebaseQueries), connect( mapStateToProps ))(Title)
src/components/todos/index.js
import Title from './Title' // 追加 class TodoComponent extends React.Component { // 略 render() { const {isOwnTodos, match: { params: {uid}}} = this.props; return ( <div> <Title isOwnTodos={isOwnTodos} uid={uid} /> {/* 追加 */} {isOwnTodos && <AddTodo uid={uid} />} <Notice /> <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} /> <Footer /> </div> ) } }
src/containers/todos/VisibleTodoList.js
const mapStateToProps = ({visibilityFilter, firebase: {data : {users, todos}}}, {uid}) => { // #2 return { // displayName: users && users[uid] && users[uid].displayName, を削除 todos: getVisibleTodos(todos && todos[uid], visibilityFilter) } } const firebaseQueries = ({uid}) => ( [ // {path: `users/${uid}/displayName`, type: 'once'}, を削除 `todos/${uid}` ] )
src/components/todos/TodoList.js
const TodoList = ({todos, isOwnTodos, onTodoClick}) => { // displayNameを削除 // 略 // const name = isOwnTodos ? 'あなた' : `${displayName} さん`; を削除 return ( {/* <div> {displayName && <div>{name}のタスク一覧</div>} の削除 */} <List> {Object.keys(todos).map( (key) => ( <Todo key={key} isOwnTodos={isOwnTodos} {...todos[key]} onClick={isOwnTodos ? (() => onTodoClick(key)) : (() => {})} /> ) )} </List> {/* </div> の削除 */} ) } TodoList.propTypes = { // displayName: PropTypes.string, は削除 isOwnTodos: PropTypes.bool.isRequired, // 略 }
以上でユーザ名の表示をタスク追加フォームの上に移動することができました。
タスク一覧画面
"{ユーザ名}のタスク一覧"と"タスクがありません"の文字列をTypographyで囲みます。"{ユーザ名}のタスク一覧"の方は、gutterBottom
を指定して、下に余白を設定しています。
src/components/todos/Title.js
({/* xxx */}
のコメントは実行前に削除してください)
import Typography from '@material-ui/core/Typography' // 追加 const Title = ({displayName, isOwnTodos}) => { const name = isOwnTodos ? 'あなた' : `${displayName} さん`; return ( <React.Fragment> {displayName && <Typography variant="h5" gutterBottom>{name}のタスク一覧</Typography>} {/* 変更 */} </React.Fragment> ) }
src/components/todos/TodoList.js
import Typography from '@material-ui/core/Typography' // 追加 const TodoList = ({todos, isOwnTodos, onTodoClick}) => { // 略 if (isEmpty(todos)) { return <Typography variant="body1">タスクがありません。</Typography> // 変更 } // 略 }
実行結果です。
以上で通常のテキストをTypographyに変更できました。
参考
★次回の記事
★目次