yucatio@システムエンジニア

趣味で作ったものいろいろ

Material-UIのDrawerを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Material-UIのDrawerを使用して、タスクのフィルタを行うボタンを右側に配置します。

ファイル名とコンポーネント名の変更

タスクのフィルタ用のボタンがフッターではなくなるので、Footer.jsのファイル名を変更します。また、Linkコンポーネントもより内容を表す名前に変更します。

  • src/components/todos/Footer.jssrc/components/todos/FilterNav.jsにリネームします。
  • src/components/todos/Link.jssrc/components/todos/FilterButton.jsにリネームします。
  • src/containers/todos/FilterLink.jssrc/containers/todos/FilterItem.jsにリネームします。

コンポーネント名を書き換えます。

src/components/todos/FilterNav.js({/* xxx */}のコメントは実行前に削除してください)

// import FilterLink from '../../containers/todos/FilterLink'  を削除
import FilterItem from '../../containers/todos/FilterItem'  // 追加

const FilterNav = () => (  {/* 変更 */}
  <p>
    Show:
    <FilterItem filter={VisibilityFilters.SHOW_ALL}>  {/* 変更 */}
      All
    </FilterItem>  {/* 変更 */}
    <FilterItem filter={VisibilityFilters.SHOW_ACTIVE}>  {/* 変更 */}
      Active
    </FilterItem>  {/* 変更 */}
    <FilterItem filter={VisibilityFilters.SHOW_COMPLETED}>  {/* 変更 */}
      Completed
    </FilterItem>  {/* 変更 */}
  </p>
)

export default FilterNav  // 変更

src/components/todos/FilterButton.js({/* xxx */}のコメントは実行前に削除してください)

const FilterButton = ({ active, children, onClick }) => (  {/* 変更 */}
  // 略
)

FilterButton.propTypes = {  // 変更
  // 略
}

export default FilterButton  // 変更

src/containers/todos/FilterItem.js

// import Link from '../../components/todos/Link'  を削除
import FilterButton from '../../components/todos/FilterButton'  // 追加

const FilterItem = connect( // 変更
  mapStateToProps,
  mapDispatchToProps
)(FilterButton) // 変更

export default FilterItem // 変更

src/components/todos/index.js

//import Footer from './Footer' を削除
import FilterNav from './FilterNav'

class TodoComponent extends React.Component {
  // 略
  render() {
    const {isOwnTodos, match: { params: {uid}}, classes} = this.props;
    return (
      <div className={classes.todoListRoot}>
        <Paper className={classes.todoListContent}>
          {/* 略 */}
          <FilterNav />  {/* FooterをFilterNavに変更 */}
        </Paper>
      </div>
    )
  }
}

変更後、エラーが出ていないことを確認してください。

Drawerの使用

画面右側にタスクのフィルタを行うボタンを配置します。

きちんと書くとすれば、画面の横幅に合わせて、Drawerを常時表示するかアクションがあった時のみ表示するかを切り替えるべきですが、今回は簡単のため、常時右側に表示します。 デフォルトではDrawerはAppbarよりも上に表示されますが、今回はAppbarよりも下に表示します。

Appbarよりも下に表示されるDrawerの導入

src/components/todos/index.js({/* xxx */}のコメントは実行前に削除してください)

const styles = theme => ({
  root: {  // 追加
    display: 'flex',  // #1
  },
  todoListRoot: {
    flexGrow: 1,  // #2
    padding: theme.spacing.unit * 3,
  },
  // 略
})

class TodoComponent extends React.Component {
  // 略
  render() {
    const {isOwnTodos, match: { params: {uid}}, classes} = this.props;
    return (
      <div className={classes.root}>  {/* 追加 */}
        <div className={classes.todoListRoot}>
          <Paper className={classes.todoListContent}>
            <Title isOwnTodos={isOwnTodos} uid={uid} />
            {isOwnTodos && <AddTodo uid={uid} />}
            <VisibleTodoList uid={uid} isOwnTodos={isOwnTodos} />
            {/* <FilterNav /> は下に移動 */}
          </Paper>
        </div>
        <FilterNav />  {/* ここに移動 */}
      </div>  {/* 追加 */}
    )
  }
}
  • display: 'flex’を使用して、タスク一覧とタスクフィルタを横に並べます(#1)。
  • flexGrow: 1を指定して、タスク一覧の幅をを可能な限り多く取ります(#2)。

src/components/todos/FilterNav.js({/* xxx */}のコメントは実行前に削除してください)

import PropTypes from 'prop-types'  // 追加
import { withStyles } from '@material-ui/core/styles'  // 追加
import Drawer from '@material-ui/core/Drawer'  // 追加

const drawerWidth = 240  // 追加

// stylesを追加
const styles = theme => ({
  drawer: {
    width: drawerWidth,
    flexShrink: 0,  // #1
  },
  drawerPaper: {
    width: drawerWidth,
  },
  toolbar: theme.mixins.toolbar,
})

const FilterNav = ({ classes }) => (  // classesを追加
  <Drawer variant="permanent" anchor="right"
                 className={classes.drawer} classes={{ paper: classes.drawerPaper, }}>  {/* #2 */}
    <div className={classes.toolbar} />  {/* #3 */}
    Show:
    <FilterItem filter={VisibilityFilters.SHOW_ALL}>
      All
    </FilterItem>
    {/* 略 */}
  </Drawer>  {/* 変更 */}
)

// propTypesを追加
FilterNav.propTypes = {
  classes: PropTypes.object.isRequired,
}

export default withStyles(styles)(FilterNav)  // 変更
  • flexShrink: 0を指定して、Drawerが縮まないようにしています(#1)。
  • Drawerでフィルタメニューを囲みます(#2)。variant="permanent"を指定することで常時表示し、anchor="right"で画面右側に表示します。
  • divを追加します(#3)。classes.toolbarを指定して、toolbarぶんの余白を設けます。

src/components/header/index.js

const styles = theme => ({
  appBar: {  // 追加
    zIndex: theme.zIndex.drawer + 1,  // #1
  },
  // 略
})

const Header = ({ classes }) => (
  <AppBar className={classes.appBar}>  {/* classNameを追加 */}
    {/* 略 */}
  </AppBar>
)
  • zIndexをdrawerより大きくすることで、AppbarをDrawarより前に表示します(#1)。

ここまでの実行結果です。画面右側にフィルタメニューが表示されました。

f:id:yucatio:20181213145240p:plain

フィルタメニューをListに変更する

reactのbuttonを使用している部分をMaterial-UIのListとListItemに変更します。

src/components/todos/FilterNav.js

import List from '@material-ui/core/List'  // 追加
import ListSubheader from '@material-ui/core/ListSubheader'  // 追加

const FilterNav = ({ classes }) => (
  <Drawer variant="permanent" anchor="right"
                 className={classes.drawer} classes={{ paper: classes.drawerPaper, }}>
    <div className={classes.toolbar} />
    <List subheader={<ListSubheader component="div">表示</ListSubheader>}>  {/* #1 */}
      <FilterLink filter={VisibilityFilters.SHOW_ALL}>
        全て  {/* 日本語に変更 */}
      </FilterLink>
      <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>
        未完了  {/* 日本語に変更 */}
      </FilterLink>
      <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>
        完了  {/* 日本語に変更 */}
      </FilterLink>
    </List>  {/* 追加 */}
  </Drawer>
)
  • Listコンポーネントでフィルタメニューを囲みます(#1)。subheaderでリストのヘッダを設定します。

src/components/todos/FilterButton.js({/* xxx */}のコメントは実行前に削除してください)

import ListItem from '@material-ui/core/ListItem'  // 追加
import ListItemIcon from '@material-ui/core/ListItemIcon'  // 追加
import ListItemText from '@material-ui/core/ListItemText'  // 追加
import Done from '@material-ui/icons/Done'  // 追加

const FilterButton = ({ active, children, onClick }) => (
  <ListItem button  // #1
    onClick={onClick}
    disabled={active}
    >
    {active &&
      <ListItemIcon>   {/* #2 */}
        <Done />
      </ListItemIcon>}
    <ListItemText inset primary={children} />   {/* #3 */}
  </ListItem>
)
  • buttonをListItemに変更し、buttonのインターフェースを設定します(#1)。styleは削除します。
  • activeがtrueの時は、Doneアイコン(チェックマークのアイコン)を表示します(#2)。
  • テキストはListItemText要素を使用します(#3)。insetでテキストの位置をそろえます。

実行結果です。 現在使用されているフィルタはdisabledかつ左側にチェックマークがつきました。

f:id:yucatio:20181213144722p:plain

以上でMaterial-UIのDrawerを使用してフィルタメニューを変更することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのAvatarを使用する(ログイン・タスク一覧編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

前回に引き続き、ログイン名の横とタスク一覧に表示されるユーザ名の横にAvatarを追加します。

ログイン名の横にAvatarを表示する

ログイン時、AppbarにAvatarを表示します。今後の実装のために、ユーザ名とavatarUrlをstate.firebase.profileから取得するように変更します。

src/components/header/Login.js({/* xxx */}のコメントは実行前に削除してください)

import Avatar from '@material-ui/core/Avatar'  // 追加

const styles = theme => ({  // themeを引数とする関数に変更
  // 略
  avatar: {  // 追加
    margin: theme.spacing.unit,
  },
})

class Login extends React.Component {
  // 略
  render() {
    const { auth, profile, loginWithGoogle, logout, classes } = this.props  // profileを追加
    // 略
    return (
        <React.Fragment>
          <Button color="inherit" aria-owns={anchorEl ? 'user-menu' : undefined} aria-haspopup="true"
          onClick={this.handleClick} className={classes.userName}>
            {profile.displayName} さん  {/* 変更 */}
          </Button>
          {/* 略 */}
          {profile.avatarUrl && <Avatar alt={profile.displayName} src={profile.avatarUrl} className={classes.avatar} />}  {/* 追加 */}
        </React.Fragment>
    )
  }
}

Login.propTypes = {
  // 略
  profile: PropTypes.shape({  // 追加
    displayName: PropTypes.string,
    avatarUrl: PropTypes.string
  }).isRequired,
}

const mapStateToProps = state => ({
  auth: state.firebase.auth,
  profile: state.firebase.profile,  // 追加
})

実行結果です。Avatarが画面右上の、ユーザ名の右隣に表示されました。

f:id:yucatio:20181211221022p:plain

タスク一覧にAvatarを表示する

続いてタスク一覧画面のユーザ名の横にAvatarを表示します。

src/components/todos/Title.js({/* xxx */}のコメントは実行前に削除してください)

import { withStyles } from '@material-ui/core/styles'  // 追加
import Avatar from '@material-ui/core/Avatar'  // 追加

// stylesを追加
const styles = theme => ({
  row: {
    display: 'flex',  // #1
    alignItems: 'center',  // #2
  },
  avatar: {
    margin: theme.spacing.unit,
  },
})

const Title = ({displayName, avatarUrl, isOwnTodos, classes}) => {  // avatarUrlとclassesを追加
  // 略
  return (
    <div className={classes.row}>  {/* divに変更し、classNameを追加 */}
      {avatarUrl && <Avatar alt={displayName} src={avatarUrl} className={classes.avatar} />}  {/*  追加 */}
      {displayName && <Typography variant="h5">{name}のタスク一覧</Typography>}
    </div>
  )
}

Title.propTypes = {
  // 略
  avatarUrl: PropTypes.string,  // 追加
  classes: PropTypes.object.isRequired,  // 追加
}

const firebaseQueries = ({uid}) => (
  [
    {path: `users/${uid}/displayName`, type: 'once'},
    {path: `users/${uid}/avatarUrl`, type: 'once'},  // 追加
  ]
)

const mapStateToProps = ({firebase: {data : {users}}}, {uid}) => ({
  displayName: users && users[uid] && users[uid].displayName,
  avatarUrl: users && users[uid] && users[uid].avatarUrl,  // 追加
})

export default compose(
  withStyles(styles),  // 追加
  firebaseConnect(firebaseQueries),
  connect(
    mapStateToProps
))(Title)
  • display: 'flex’を指定することで、Avaterとユーザ名を横に並べます(#1)。
  • alignItems: 'center’を指定することで、Avaterとユーザ名のを上下中央に揃えます(#2)。

実行結果です。Avatarがタスク一覧のユーザ名の左隣に表示されました。

f:id:yucatio:20181211221038p:plain

以上で、ログイン名の横とタスク一覧に表示されるユーザ名の横にAvatarを追加できました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのAvatarを使用する(最近の更新編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

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

動作確認します。

タスク完了フラグを切り替えます

f:id:yucatio:20181210110744p:plain

データベースのデータを確認します。recentUpdatedTodos以下のデータにavatarUrlが追加されました。

f:id:yucatio:20181210110804p:plain

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が表示されます。

f:id:yucatio:20181210110837p:plain

以上で最近の更新リストにAvatarを表示することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのMenuを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

ログアウトのボタンの常時表示をやめ、ユーザ名がクリックされた時にログアウトメニューが出るように変更します。Material-UIのMenuを使用します。

f:id:yucatio:20190428224042p:plain

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)。anchorElnullを設定し、表示対象をなくします。
  • ユーザ名の表示({auth.displayName}さん)をButtonで囲みます(#5)。aria-ownsはメニュー要素のidを指定し、aria-haspopupはtrueを指定します。詳しくは参考のリンクを参照ください。
  • 公式のデモを参考にメニューの設定をします(#6)。anchorEl要素にstateanchorElを指定します。openanchorElが存在するかを指定しています。
  • メニューの各項目をMenuItemとして追加します(#7)。

実行結果です。ログアウトボタンは通常では見えません。

f:id:yucatio:20181209174215p:plain

ユーザ名をクリックするとログアウトメニューが表示されました。

f:id:yucatio:20181209174236p:plain

"ログアウト"をクリックすると正常にログアウトします。

f:id:yucatio:20181209174332p:plain

以上でMenuを使用してログアウトメニューを表示することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

Material-UIのAppBarを使用する(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Appbarはいわゆるwebページのヘッダーで、ロゴやメニューなどを表示するのに使用します。 今回はログイン機能とナビゲーション機能をAppbarに載せます。ディレクトリ階層の変更とファイル名の変更も行います。

変更前と変更後のコンポーネントの位置関係は以下のようになります。

f:id:yucatio:20181208154126p:plain

ディレクトリ階層の変更

Appbarのコンポーネントをheaderディレクトリにまとめます。

  1. src/components/headerディレクトリを作成します。
  2. src/components/login/index.jssrc/components/header/Login.jsにリネームします。
  3. src/components/navbar/index.jssrc/components/header/MenuIcons.jsにリネームします。

src/components/header/MenuIcons.jsNavbar変数を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が重なってしまうことがわかります。

ここまでの実行結果です。

f:id:yucatio:20181208154517p:plain

今回はAppbarのpositionがデフォルト設定のfixedなので、 スクロールしてもAppbarが上部に表示されます。

f:id:yucatio:20181208154532p:plain

Appbarとブラウザの端の間に空白が開いてしまう場合は、<CssBaseline>の設定が足りていないので、下記記事を参考に設定してください。

yucatio.hatenablog.com

タイトルの追加と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の左側にタイトル、右側に編集へのリンクとログイン表示がされました。リンクが機能していることを確認してください。

f:id:yucatio:20181208154723p:plain

以上でログイン機能とナビゲーション機能をMaterial-UIのAppBarに載せることができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

withStylesによるスタイルの適用(タスク一覧編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

タスク一覧にスタイルを適用する

前回と同様にタスク一覧にもスタイルを適用します。Paperの周りにマージンを設定し、Paperの中に余白を設定します。

f:id:yucatio:20190428222832p:plain

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の内部にも余白ができています。

f:id:yucatio:20181206165029p:plain

続いて読み込み中のぐるぐるとタスクなしの文言の周りにも余白を追加します。

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)  // 変更

実行結果です。ぐるぐるとタスクなしの文言の周りに余白が追加されました。

f:id:yucatio:20181206091232p:plain

f:id:yucatio:20181206091246p:plain

以上でwithStylesを使用してタスク一覧にスタイルを適用することができました。

参考

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com

withStylesによるスタイルの適用(最近の更新編)(STEP 4 : Material-UIの導入 - React + Redux + Firebase チュートリアル)

★前回の記事

yucatio.hatenablog.com

Material-UIにおいて個別にスタイルを適用するには、stylesオブジェクト(またはstyles関数)とwithStyleを使用します。

前回適用したPaperの周りにマージンと中に余白(padding)を設定します。

f:id:yucatio:20190428222344p:plain

ダッシュボードにスタイルを適用する

ダッシュボートのマージンと余白を設定します。

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に制限され、左右均等に余白ができました。

f:id:yucatio:20181205105358p:plain

Google Cromeのデベロッパーツールでも確認できました。

f:id:yucatio:20181205105417p:plain

引き続き”最近の更新”の文字周りと読み込み中表示の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の縁の間に余白ができました。

f:id:yucatio:20181205105435p:plain

読み込み中のぐるぐるの周りにも余白を追加しました。

f:id:yucatio:20181210102400p:plain

以上でwithStylesを使用してダッシュボードにスタイルを適用することができました。

★次回の記事

yucatio.hatenablog.com

★目次

yucatio.hatenablog.com