React-材质-UI-秘籍-一-

47 阅读54分钟

React 材质 UI 秘籍(一)

原文:zh.annas-archive.org/md5/c4e5ed8c3a8a54c4065e4c907829dab6

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Material-UI 是全球最受欢迎的 React UI 框架。Material-UI 技能是一个宝贵的资产,这并不令人惊讶。在开源领域和商业领域,有无数的项目依赖于这个框架。那么,是什么让 Material-UI 如此受欢迎呢?

首先要说明的是,Material-UI 出色地将两种最好的前端技术结合在一起。简而言之,Material-UI 将谷歌的 Material Design 作为组件暴露在 Facebook 的 React 中。许多开发者对 React 有足够的了解来构建出能工作的东西。许多设计师对 Material Design 有足够的了解来设计出令人难以置信的体验。Material-UI 是这两个世界之间的桥梁,简化了交付令客户满意的产品的任务。

从高层次来看,这个销售点足以吸引每个级别和每个专业领域的开发者。让开发者保持对 Material-UI 的兴趣的是其功能的广泛性和可用资源的深度,这些资源可以帮助您应对任何场景。我希望这本书能为这些资源做出有价值的贡献。

本书面向的对象

这本书是为任何认为 Material-UI 可能帮助他们为应用程序创造更好的用户体验的开发者而写的。从经验丰富的专业人士到世界各地的初级开发者,这本书都有关于 Material-UI 可以教授您的内容。

假设没有 Material Design 知识。为了最大限度地利用这本书,您应该至少具备 React 和现代 JavaScript 的实际操作知识。虽然这本书不是用来教您 React 的,但我确实尝试在可能有助于阐明整个示例的情况下解释 React 特定的机制。

本书涵盖的内容

第一章,网格 – 在页面上放置组件,使用网格系统来放置页面上的组件。

第二章,应用栏 – 每个页面的顶层,将应用栏添加到 UI 的顶部。

第三章,抽屉 – 导航控制的位置,使用抽屉作为显示主要导航的位置。

第四章,标签页 – 将内容分组到标签部分,将您的内文组织到标签中。

第五章,扩展面板 – 将内容分组到面板部分,将您的内文组织到面板中。

第六章,列表 – 显示简单集合数据,渲染用户可以阅读和与之交互的项目列表。

第七章,表格 – 显示复杂集合数据,展示了数据集合的详细信息。

第八章,卡片 – 显示详细信息,使用卡片来显示特定实体/事物/对象的详细信息。

第九章,Snackbars – 临时消息,通知用户关于应用程序中正在发生的事情。

第十章,按钮 – 启动操作,解释了按按钮是用户执行操作最常见的方式。

第十一章,文本 – 收集文本输入,允许用户输入信息。

第十二章,自动完成和芯片 – 多项文本输入建议,在用户输入时提供选择。

第十三章,选择 – 从选项中进行选择,允许用户从预定义的选项集中进行选择。

第十四章,选择器 – 选择日期和时间,使用易于阅读的格式选择日期和时间值。

第十五章,对话框 – 用户交互的模态屏幕,显示模态屏幕以收集输入或显示信息。

第十六章,菜单 – 显示弹出操作,通过将操作放在菜单中来节省屏幕空间。

第十七章,排版 – 控制字体外观和感觉,以系统化的方式控制您的 UI 字体。

第十八章,图标 – 优化图标以匹配外观和感觉,自定义 Material-UI 图标并添加新的图标。

第十九章,主题 – 集中管理应用的外观和感觉,使用主题来改变组件的外观和感觉。

第二十章,样式 – 将样式应用于组件,使用多种样式解决方案之一来设计您的 UI。

要充分利用这本书

  1. 确保您理解 React 的基础知识。教程是一个良好的起点:reactjs.org/tutorial/tutorial.html

  2. 克隆这本书的存储库:github.com/PacktPublishing/Material-UI-Cookbook

  3. 通过切换到Material-UI-Cookbook目录并运行npm install来安装包。

  4. 通过运行npm run storybook来启动 Storybook。现在,您可以在阅读本书的同时浏览每个示例。一些示例在 Storybook UI 中具有属性编辑控件,但您在学习过程中可以随意调整代码!

下载示例代码文件

您可以从www.packt.com上的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com 登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载与勘误。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载后,请确保使用最新版本的软件解压缩或提取文件夹:

  • WinRAR/7-Zip for Windows

  • Zipeg/iZip/UnRarX for Mac

  • 7-Zip/PeaZip for Linux

本书代码包也托管在 GitHub 上,网址为 github.com/PacktPublishing/React-Material-UI-Cookbook。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有来自我们丰富的书籍和视频目录中的其他代码包可供选择,这些目录可在 github.com/PacktPublishing/ 上找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/9781789615227_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“将下载的 WebStorm-10*.dmg 磁盘镜像文件挂载为系统中的另一个磁盘。”

代码块设置如下:

const styles = theme => ({
  root: {
    flexGrow: 1
  },

粗体:表示新术语、重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“从管理面板中选择系统信息。”

警告或重要注意事项看起来像这样。

技巧和窍门看起来像这样。

部分

在本书中,您将找到一些频繁出现的标题(准备就绪如何操作...它是如何工作的...还有更多...,以及另请参阅)。

为了清楚地说明如何完成食谱,请按以下方式使用这些部分:

准备就绪

本节将向您介绍在食谱中可以期待的内容,并描述如何设置任何软件或任何为食谱所需的初步设置。

如何操作…

本节包含遵循食谱所需的步骤。

它是如何工作的…

本节通常包含对上一节发生情况的详细解释。

还有更多…

本节包含有关食谱的附加信息,以便您对食谱有更深入的了解。

另请参阅

本节提供了对食谱的其他有用信息的链接。

联系我们

我们欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并通过 customercare@packtpub.com 邮箱联系我们。

勘误表:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们非常感谢您能向我们报告。请访问www.packt.com/submit-erra…,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现任何形式的我们作品的非法副本,我们非常感谢您能提供位置地址或网站名称。请通过copyright@packt.com与我们联系,并附上材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评价

请留下您的评价。一旦您阅读并使用过这本书,为何不在购买它的网站上留下评价呢?潜在读者可以查看并使用您的客观意见来做出购买决定,我们 Packt 公司可以了解您对我们产品的看法,并且我们的作者可以查看他们对书籍的反馈。谢谢!

如需更多关于 Packt 的信息,请访问packt.com

第一章:网格 - 在页面上放置组件

在本章中,我们将介绍以下内容:

  • 理解断点

  • 填充空间

  • 抽象容器和项目

  • 固定列布局

  • 列方向

简介

Material-UI 网格用于控制应用中屏幕的布局。而不是实现自己的样式来管理 Material-UI 组件的布局,你可以利用Grid组件。在幕后,它使用 CSS flexbox 属性来处理灵活布局。

应用断点

断点被 Material-UI 用于确定在屏幕上何时打断内容流并继续到下一行。了解如何使用Grid组件应用断点是实现 Material-UI 应用程序布局的基本。

如何实现...

假设你想要在屏幕上均匀分布四个元素,并占据所有可用的水平空间。相应的代码如下:

import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  paper: {
    padding: theme.spacing(2),
    textAlign: 'center',
    color: theme.palette.text.secondary
  }
});

const UnderstandingBreakpoints = withStyles(styles)(({ classes }) => (
  <div className={classes.root}>
    <Grid container spacing={4}>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
      </Grid>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
      </Grid>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
      </Grid>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
      </Grid>
    </Grid>
  </div>
));

export default UnderstandingBreakpoints;

这将渲染四个Paper组件。标签指示了xssmmd属性使用的值。以下是结果的外观:

图片

它是如何工作的...

你可以传递给Grid组件的每个断点属性都对应于屏幕宽度,如下所示:

  • xs >= 0px

  • sm >= 600px

  • md >= 960px

  • lg >= 1280px

  • xl >= 1920px

之前显示的屏幕像素宽度为 725,这意味着Grid组件使用了sm断点。传递给此属性的值是6。这个值可以是112之间的数字,它定义了将有多少个项目放入网格中。这可能会令人困惑,因此将这些数字视为百分比可能会有所帮助。例如,6将是 50%,正如前面的截图所示,Grid元素占据了 50%的宽度。

例如,假设你希望在小型断点激活时,每个Grid元素的宽度占屏幕宽度的 75%。你可以将sm值设置为9(9/12 = 0.75),如下所示:

<div className={classes.root}>
  <Grid container spacing={4}>
    <Grid item xs={12} sm={9} md={3}>
      <Paper className={classes.paper}>xs=12 sm=9 md=3</Paper>
    </Grid>
    <Grid item xs={12} sm={9} md={3}>
      <Paper className={classes.paper}>xs=12 sm=9 md=3</Paper>
    </Grid>
    <Grid item xs={12} sm={9} md={3}>
      <Paper className={classes.paper}>xs=12 sm=9 md=3</Paper>
    </Grid>
    <Grid item xs={12} sm={9} md={3}>
      <Paper className={classes.paper}>xs=12 sm=9 md=3</Paper>
    </Grid>
  </Grid>
</div>

当屏幕宽度仍然是 725 像素时,这是结果:

图片

这种屏幕宽度和断点值的组合并不理想——右侧有大量的空间被浪费了。通过实验,你可以使sm值更大,以减少浪费的空间,或者你可以减小值,以便更多项目能适应一行。例如,6看起来更好,因为正好有 2 个项目适合屏幕。

让我们将屏幕宽度降低到 575 像素。这将激活xs断点,其值为12(100%):

图片

这种布局适用于较小的屏幕,因为它不会试图在一行中放置过多的网格项目。

更多内容...

如果你不确定使用哪个值,可以为每个断点值使用auto

<div className={classes.root}>
  <Grid container spacing={4}>
    <Grid item xs="auto" sm="auto" md="auto">
      <Paper className={classes.paper}>
        xs=auto sm=auto md=auto
      </Paper>
    </Grid>
    <Grid item xs="auto" sm="auto" md="auto">
      <Paper className={classes.paper}>
        xs=auto sm=auto md=auto
      </Paper>
    </Grid>
    <Grid item xs="auto" sm="auto" md="auto">
      <Paper className={classes.paper}>
        xs=auto sm=auto md=auto
      </Paper>
    </Grid>
    <Grid item xs="auto" sm="auto" md="auto">
      <Paper className={classes.paper}>
        xs=auto sm=auto md=auto
      </Paper>
    </Grid>
  </Grid>
</div>

这将尝试在每一行中尽可能多地放置项目。当屏幕尺寸变化时,项目会重新排列,以便相应地适应屏幕。以下是在屏幕宽度为 725 像素时的样子:

图片

我建议在某个时候将auto替换为112之间的一个值。auto的值已经足够好,你可以开始做其他事情,而不必过多担心布局,但它对你的生产应用来说远非完美。至少通过这样设置auto,你所有的Grid组件和断点属性都已经就位。你只需要调整数字,直到一切看起来都很好。

参见

填充空间

对于某些布局,让你的网格项占据整个屏幕宽度是不可能的。使用justify属性,你可以控制网格项如何填充行中的可用空间。

如何实现...

假设你需要在网格中渲染四个Paper组件。在每个Paper组件内部,你有三个Chip组件,它们是嵌套的网格项

这段代码看起来是这样的:

import React from 'react';

import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import Chip from '@material-ui/core/Chip';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  paper: {
    padding: theme.spacing(2),
    textAlign: 'center',
    color: theme.palette.text.secondary
  }
});

const FillingSpace = withStyles(styles)(({ classes, justify }) => (
  <div className={classes.root}>
    <Grid container spacing={4}>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>
          <Grid container justify={justify}>
            <Grid item>
              <Chip label="xs=12" />
            </Grid>
            <Grid item>
              <Chip label="sm=6" />
            </Grid>
            <Grid item>
              <Chip label="md=3" />
            </Grid>
          </Grid>
        </Paper>
      </Grid>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>
          <Grid container justify={justify}>
            <Grid item>
              <Chip label="xs=12" />
            </Grid>
            <Grid item>
              <Chip label="sm=6" />
            </Grid>
            <Grid item>
              <Chip label="md=3" />
            </Grid>
          </Grid>
        </Paper>
      </Grid>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>
          <Grid container justify={justify}>
            <Grid item>
              <Chip label="xs=12" />
            </Grid>
            <Grid item>
              <Chip label="sm=6" />
            </Grid>
            <Grid item>
              <Chip label="md=3" />
            </Grid>
          </Grid>
        </Paper>
      </Grid>
      <Grid item xs={12} sm={6} md={3}>
        <Paper className={classes.paper}>
          <Grid container justify={justify}>
            <Grid item>
              <Chip label="xs=12" />
            </Grid>
            <Grid item>
              <Chip label="sm=6" />
            </Grid>
            <Grid item>
              <Chip label="md=3" />
            </Grid>
          </Grid>
        </Paper>
      </Grid>
    </Grid>
  </div>
));

export default FillingSpace;

justify属性是在container Grid组件上指定的。在这个例子中,包含Chip组件作为项的container。每个container都使用flex-start值,这将使Grid项对齐到container的起始位置。结果是:

图片

它是如何工作的...

justify属性的flex-start值将所有Grid项对齐到container的起始位置。在这种情况下,四个容器中的每个容器中的三个Chip组件都挤在行的左侧。项目左侧的空间没有被填充。你不必更改这些项目的断点属性值,这会导致宽度变化,你可以更改justify属性值来告诉Grid容器如何填充空隙。

例如,你可以使用center值来将Grid项对齐到container的中心,如下所示:

<div className={classes.root}>
  <Grid container spacing={4}>
    <Grid item xs={12} sm={6} md={3}>
      <Paper className={classes.paper}>
        <Grid container justify="center">
          <Grid item>
            <Chip label="xs=12" />
          </Grid>
          <Grid item>
            <Chip label="sm=6" />
          </Grid>
          <Grid item>
            <Chip label="md=3" />
          </Grid>
        </Grid>
      </Paper>
    </Grid>
    <Grid item xs={12} sm={6} md={3}>
      <Paper className={classes.paper}>
        <Grid container justify="center">
          <Grid item>
            <Chip label="xs=12" />
          </Grid>
          <Grid item>
            <Chip label="sm=6" />
          </Grid>
          <Grid item>
            <Chip label="md=3" />
          </Grid>
        </Grid>
      </Paper>
    </Grid>
    <Grid item xs={12} sm={6} md={3}>
      <Paper className={classes.paper}>
        <Grid container justify="center">
          <Grid item>
            <Chip label="xs=12" />
          </Grid>
          <Grid item>
            <Chip label="sm=6" />
          </Grid>
          <Grid item>
            <Chip label="md=3" />
          </Grid>
        </Grid>
      </Paper>
    </Grid>
    <Grid item xs={12} sm={6} md={3}>
      <Paper className={classes.paper}>
        <Grid container justify="center">
          <Grid item>
            <Chip label="xs=12" />
          </Grid>
          <Grid item>
            <Chip label="sm=6" />
          </Grid>
          <Grid item>
            <Chip label="md=3" />
          </Grid>
        </Grid>
      </Paper>
    </Grid>
  </Grid>
</div>

以下截图显示了将justify属性值更改后的结果:

图片

这会将空隙均匀分布到Grid项的左右两侧。但是,由于它们之间没有空间,项目仍然显得拥挤。如果你使用justify属性的space-around值,它看起来是这样的:

图片

这个值在填充Grid容器中所有可用空间方面做得最好,而无需更改Grid项的宽度。

更多内容...

space-around值的一个变体是space-between值。这两个值在填充行中所有空间方面是相似的。以下是前一个示例部分使用space-between的效果:

图片

行中所有的多余空间都放在Grid项目之间,而不是周围。换句话说,当你想要确保每行左右没有空隙时,使用这个值。

参见

抽象容器和项目

在你的应用中,你有许多屏幕,每个屏幕都有许多Grid组件,用于创建复杂的布局。试图阅读包含大量<Grid>元素的源代码可能会令人望而却步。特别是当Grid组件既用于容器又用于项目时。

如何做到这一点...

Grid组件的containeritem属性决定了元素的角色。你可以创建两个使用这些属性的组件,并在有大量布局组件时创建一个更容易阅读的元素名称:

import React from 'react';

import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  paper: {
    padding: theme.spacing(2),
    textAlign: 'center',
    color: theme.palette.text.secondary
  }
});

const Container = props => <Grid container {...props} />;
const Item = props => <Grid item {...props} />;

const AbstractingContainersAndItems = withStyles(styles)(
  ({ classes }) => (
    <div className={classes.root}>
      <Container spacing={4}>
        <Item xs={12} sm={6} md={3}>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
        <Item xs={12} sm={6} md={3}>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
        <Item xs={12} sm={6} md={3}>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
        <Item xs={12} sm={6} md={3}>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
      </Container>
    </div>
  )
);

export default AbstractingContainersAndItems;

这就是结果布局的外观:

图片

它是如何工作的...

让我们更仔细地看看ContainerItem组件:

const Container = props => <Grid container {...props} />;
const Item = props => <Grid item {...props} />;

Container组件渲染一个具有container属性设置为 true 的Grid组件,而Item组件做同样的事情,只是将item属性设置为 true。每个组件将任何额外的属性传递给Grid组件,例如xssm断点。

当你有大量的Grid容器和组成你的布局的项目时,能够看到<Container><Item>元素之间的区别会使你的代码更容易阅读。与此相对的是,在所有地方都有<Grid>元素。

更多内容...

如果你发现你在布局中反复使用相同的断点,你可以在你的高阶Item组件中包含它们。让我们重写示例,以便除了Item属性外,还包括xssmmd属性:

const Container = props => <Grid container {...props} />;
const Item = props => <Grid item xs={12} sm={6} md={3} {...props} />;

const AbstractingContainersAndItems = withStyles(styles)(
  ({ classes }) => (
    <div className={classes.root}>
      <Container spacing={4}>
        <Item>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
        <Item>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
        <Item>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
        <Item>
          <Paper className={classes.paper}>xs=12 sm=6 md=3</Paper>
        </Item>
      </Container>
    </div>
  )
);

现在,你不再有四个<Item xs={12} sm={6} md={3}>实例,而是有四个<Item>实例。组件抽象是移除你的JavaScript XMLJSX)标记中多余语法的优秀工具。

任何需要覆盖你在Item组件中设置的任何断点属性的时候,你只需要将属性传递给Item。例如,如果你有一个特定的案例需要md6,你只需写<Item md={6}>。这之所以有效,是因为在Item组件中,{...props}是在默认值之后传递的,这意味着它们覆盖了具有相同名称的任何属性。

参见

固定列布局

当你使用Grid组件构建布局时,它们通常会根据你的断点设置和屏幕宽度发生变化。例如,如果用户将浏览器窗口缩小,你的布局可能会从两列变为三列。然而,有时你可能更喜欢固定列数,并且每列的宽度会根据屏幕大小变化。

如何实现...

假设你想要渲染八个Paper组件,但你还想确保不超过四列。使用以下代码来完成此操作:

import React from 'react';

import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  paper: {
    padding: theme.spacing(2),
    textAlign: 'center',
    color: theme.palette.text.secondary
  }
});

const FixedColumnLayout = withStyles(styles)(({ classes, width }) => (
  <div className={classes.root}>
    <Grid container spacing={4}>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
      <Grid item xs={width}>
        <Paper className={classes.paper}>xs={width}</Paper>
      </Grid>
    </Grid>
  </div>
));

export default FixedColumnLayout;

以下是在像素宽度为 725 时的结果:

以下是在像素宽度为 350 时的结果:

它是如何工作的...

如果你想要固定列数,你应该只指定xs断点属性。在这个例子中,3是屏幕宽度的 25%——或者 4 列。这永远不会改变,因为xs是最小的断点。任何更大的断点都会应用到xs上,除非你指定更大的断点。

假设你想要两列。你可以将xs值设置为6,如下所示:

<div className={classes.root}>
  <Grid container spacing={4}>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
  </Grid>
</div>

以下是在像素屏幕宽度为 960 时的结果:

因为你已经将xs值设置为6(50%),这些Grid组件将始终只使用两列。项目本身将改变其宽度以适应屏幕宽度,而不是改变每行的项目数。

还有更多...

你可以以固定方式组合不同的宽度。例如,你可以有使用全宽布局的页眉和页脚Grid组件,而中间的Grid组件使用两列:

<div className={classes.root}>
  <Grid container spacing={4}>
    <Grid item xs={12}>
      <Paper className={classes.paper}>xs=12</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={6}>
      <Paper className={classes.paper}>xs=6</Paper>
    </Grid>
    <Grid item xs={12}>
      <Paper className={classes.paper}>xs=12</Paper>
    </Grid>
  </Grid>
</div>

第一行和最后一行的Grid组件具有xs值为12(100%),而其他Grid组件的xs值为6(50%),以实现两列布局。以下是在像素宽度为 725 时的结果:

参见

改变列方向

当使用固定列数进行布局时,内容从左到右流动。第一个网格项位于第一列,第二个项位于第二列,依此类推。有时你可能需要更好地控制哪些网格项进入哪些列。

如何实现...

假设你有一个四列布局,但你希望第一和第二项位于第一列,第三和第四项位于第二列,依此类推。这涉及到使用嵌套的 Grid 容器,并更改 direction 属性,如下所示:

import React from 'react';

import { withStyles } from '@material-ui/core/styles';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
import Hidden from '@material-ui/core/Hidden';
import Typography from '@material-ui/core/Typography';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  paper: {
    padding: theme.spacing(2),
    textAlign: 'center',
    color: theme.palette.text.secondary
  }
});

const ColumnDirection = withStyles(styles)(({ classes }) => (
  <div className={classes.root}>
    <Grid container justify="space-around" spacing={4}>
      <Grid item xs={3}>
        <Grid container direction="column" spacing={2}>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>One</Typography>
            </Paper>
          </Grid>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Two</Typography>
            </Paper>
          </Grid>
        </Grid>
      </Grid>
      <Grid item xs={3}>
        <Grid container direction="column" spacing={2}>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Three</Typography>
            </Paper>
          </Grid>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Four</Typography>
            </Paper>
          </Grid>
        </Grid>
      </Grid>
      <Grid item xs={3}>
        <Grid container direction="column" spacing={2}>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Five</Typography>
            </Paper>
          </Grid>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Six</Typography>
            </Paper>
          </Grid>
        </Grid>
      </Grid>
      <Grid item xs={3}>
        <Grid container direction="column" spacing={2}>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Seven</Typography>
            </Paper>
          </Grid>
          <Grid item>
            <Paper className={classes.paper}>
              <Typography>Eight</Typography>
            </Paper>
          </Grid>
        </Grid>
      </Grid>
    </Grid>
  </div>
));

export default ColumnDirection;

在像素宽度为 725 时的结果如下:

与从左到右流动的值不同,你可以完全控制项目放置在哪个列中。

你可能已经注意到,与本章中的其他示例相比,字体看起来不同。这是因为使用了 Typography 组件来设置文本样式并应用 Material-UI 主题样式。大多数 Material-UI 组件在显示文本时不需要你使用 Typography,但 Paper 组件需要。

它是如何工作的...

这个示例中有很多内容,所以让我们先看看 Grid 代码中的第一个项目:

<Grid item xs={3}>
  <Grid container direction="column" spacing={2}>
    <Grid item>
      <Paper className={classes.paper}>
        <Typography>One</Typography>
      </Paper>
    </Grid>
    <Grid item>
      <Paper className={classes.paper}>
        <Typography>Two</Typography>
      </Paper>
    </Grid>
  </Grid>
</Grid>

Grid 项目使用 xs 值为 4 来创建四列布局。本质上,这些项目是列。接下来,你有一个嵌套的 Grid 容器。这个 containerdirection 属性值为 column。这是你可以放置属于此列的 Grid 项的地方,并且它们将从上到下流动,而不是从左到右。这个网格中的每一列都遵循这个模式。

还有更多...

有时候,隐藏最右侧的列比尝试适应屏幕宽度更有意义。你可以使用 Hidden 组件来实现这一点。它已经在示例中导入,如下所示:

import Hidden from '@material-ui/core/Hidden';

要使用它,你需要用它包裹最后一个 column。例如,以下是最后一个 column 现在的样子:

<Grid item xs={3}>
  <Grid container direction="column" spacing={2}>
    <Grid item>
      <Paper className={classes.paper}>
        <Typography>Seven</Typography>
      </Paper>
    </Grid>
    <Grid item>
      <Paper className={classes.paper}>
        <Typography>Eight</Typography>
      </Paper>
    </Grid>
  </Grid>
</Grid>

如果你想在某个断点隐藏这个 column,你可以用 Hidden 包裹这个 column,如下所示:

<Hidden smDown>
  <Grid item xs={3}>
    <Grid container direction="column" spacing={2}>
      <Grid item>
        <Paper className={classes.paper}>
          <Typography>Seven</Typography>
        </Paper>
      </Grid>
      <Grid item>
        <Paper className={classes.paper}>
          <Typography>Eight</Typography>
        </Paper>
      </Grid>
    </Grid>
  </Grid>
</Hidden>

smDown 属性指示 Hidden 组件在达到 sm 断点或更低时隐藏其子元素。以下是在像素宽度为 1000 时的结果:

最后一列被显示出来,因为 sm 断点比屏幕尺寸小。以下是在像素屏幕宽度为 550,且不显示最后一列的结果:

参见

第二章:App Bars - 每个页面的顶层

在本章中,你将学习以下内容:

  • 固定位置

  • 滚动时隐藏

  • 工具栏抽象

  • 带导航

简介

App Bars 是任何 Material-UI 应用的锚点。它们提供上下文,并且通常在用户在应用程序中导航时始终可见。

固定位置

你可能希望你的AppBar组件始终可见。通过使用fixed定位,AppBar组件即使在用户滚动页面时也保持可见。

如何实现...

你可以使用position属性的fixed值。以下是实现方法:

import React from 'react';

import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  }
});

const FixedPosition = withStyles(styles)(({ classes }) => (
  <div className={classes.root}>
    <AppBar position="fixed">
      <Toolbar>
        <IconButton
          className={classes.menuButton}
          color="inherit"
          aria-label="Menu"
        >
          <MenuIcon />
        </IconButton>
        <Typography
          variant="title"
          color="inherit"
          className={classes.flex}
        >
          Title
        </Typography>
        <Button color="inherit">Login</Button>
      </Toolbar>
    </AppBar>
    <ul>
      {new Array(500).fill(null).map((v, i) => (
        <li key={i}>{i}</li>
      ))}
    </ul>
  </div>
));

export default FixedPosition;

这是结果AppBar组件的样子:

它是如何工作的...

如果你向下滚动,你会看到AppBar组件如何保持固定,并且内容在其后面滚动。以下是在本例中滚动到页面底部时的样子:

position属性的默认值是fixed。然而,明确设置此属性可以帮助读者更好地理解你的代码。

还有更多...

当本例中的屏幕首次加载时,一些内容被隐藏在AppBar组件后面。这是因为位置是固定的,并且它比常规内容的z-index值更高。这是预期的,这样当滚动时,常规内容就会在AppBar组件后面。解决方案是为你的内容添加一个顶部边距。问题是,你并不一定知道AppBar的高度。

你可以设置一个看起来不错的值。更好的解决方案是使用toolbar mixin样式。你可以通过将styles设置为返回对象的函数来访问这个mixin对象。然后,你将能够访问主题参数,它包含一个toolbar mixin对象。

这是styles应该更改的样子:

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  },
  toolbarMargin: theme.mixins.toolbar
});

新增的样式是toolbarMargin。注意,这使用的是来自theme.mixins.toolbar的值,这就是你现在为什么使用函数的原因——这样你就可以访问theme。以下是theme.mixins.toolbar的值:

{
  "minHeight": 56,
  "@media (min-width:0px) and (orientation: landscape)": {
    "minHeight": 48
  },
  "@media (min-width:600px)": {
    "minHeight": 64
  }
}

最后一步是在AppBar组件下方的<div>元素中添加一个元素,以便可以应用这个新的toolbarMargin样式:

<div className={classes.root}>
  <AppBar position="fixed">
    <Toolbar>
      <IconButton
        className={classes.menuButton}
        color="inherit"
        aria-label="Menu"
      >
        <MenuIcon />
      </IconButton>
      <Typography
        variant="title"
        color="inherit"
        className={classes.flex}
      >
        Title
      </Typography>
      <Button color="inherit">Login</Button>
    </Toolbar>
  </AppBar>
  <div className={classes.toolbarMargin} />
  <ul>
    {new Array(500).fill(null).map((v, i) => <li key={i}>{i}</li>)}
  </ul>
</div>

现在,当屏幕首次加载时,内容的开头不再被AppBar组件隐藏:

参见

滚动时隐藏

如果你屏幕上有大量需要用户垂直滚动的内 容,App Bar 可能会分散用户的注意力。一种解决方案是在用户向下滚动时隐藏 AppBar 组件。

如何实现...

要在用户向下滚动时隐藏 AppBar 组件,你必须知道用户何时在滚动。这需要监听 window 对象上的 scroll 事件。你可以实现一个组件来监听此事件,并在滚动时隐藏 AppBar 组件。以下是实现方式:

import React, { Component } from 'react';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Fade from '@material-ui/core/Fade';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  },
  toolbarMargin: theme.mixins.toolbar
});

const ScrolledAppBar = withStyles(styles)(
  class extends Component {
    state = {
      scrolling: false,
      scrollTop: 0
    };

    onScroll = e => {
      this.setState(state => ({
        scrollTop: e.target.documentElement.scrollTop,
        scrolling:
          e.target.documentElement.scrollTop > state.scrollTop
      }));
    };

    shouldComponentUpdate(props, state) {
      return this.state.scrolling !== state.scrolling;
    }

    componentDidMount() {
      window.addEventListener('scroll', this.onScroll);
    }

    componentWillUnmount() {
      window.removeEventListener('scroll', this.onScroll);
    }

    render() {
      const { classes } = this.props;

      return (
        <Fade in={!this.state.scrolling}>
          <AppBar>
            <Toolbar>
              <IconButton
                className={classes.menuButton}
                color="inherit"
                aria-label="Menu"
              >
                <MenuIcon />
              </IconButton>
              <Typography
                variant="h6"
                color="inherit"
                className={classes.flex}
              >
                My Title
              </Typography>
              <Button color="inherit">Login</Button>
            </Toolbar>
          </AppBar>
        </Fade>
      );
    }
  }
);

const AppBarWithButtons = withStyles(styles)(
  ({ classes, title, buttonText }) => (
    <div className={classes.root}>
      <ScrolledAppBar />
      <div className={classes.toolbarMargin} />
      <ul>
        {new Array(500).fill(null).map((v, i) => (
          <li key={i}>{i}</li>
        ))}
      </ul>
    </div>
  )
);

export default AppBarWithButtons;

当你首次加载屏幕时,工具栏和内容会像往常一样显示:

图片

当你向下滚动时,AppBar 组件消失,为查看更多内容腾出空间。以下是当你滚动到屏幕底部时的屏幕外观:

图片

一旦你开始向上滚动,AppBar 组件就会立即重新出现。

工作原理...

让我们看看 ScrolledAppBar 组件的 state 方法 和 onScroll() 方法:

state = {
  scrolling: false,
  scrollTop: 0
};

onScroll = e => {
  this.setState(state => ({
    scrollTop: e.target.documentElement.scrollTop,
   scrolling:
      e.target.documentElement.scrollTop > state.scrollTop
  }));
};

componentDidMount() {
  window.addEventListener('scroll', this.onScroll);
}

componentWillUnmount() {
  window.removeEventListener('scroll', this.onScroll);
}

当组件挂载时,onScroll() 方法被添加为监听 window 对象上的 scroll 事件的监听器。scrolling 状态是一个布尔值,当为 true 时隐藏 AppBar 组件。scrollTop 状态是前一个滚动事件的位置。onScroll() 方法通过检查新的滚动位置是否大于最后一个滚动位置来确定用户是否在滚动。

接下来,让我们看看用于在滚动时隐藏 AppBar 组件的 Fade 组件,如下所示:

<Fade in={!this.state.scrolling}>
  <AppBar>
    <Toolbar>
      <IconButton
        className={classes.menuButton}
        color="inherit"
        aria-label="Menu"
      >
        <MenuIcon />
      </IconButton>
      <Typography
        variant="title"
        color="inherit"
        className={classes.flex}
      >
        My Title
      </Typography>
      <Button color="inherit">Login</Button>
    </Toolbar>
  </AppBar>
</Fade>

in 属性告诉 Fade 组件在值为 true 时淡入其子组件,in。在这个例子中,当 scrolling 状态为 false 时,条件为 true。

还有更多...

当用户滚动时,你不必淡入淡出 AppBar 组件,可以使用不同的效果。例如,以下代码块演示了如果你想使用 Grow 效果会是什么样子:

<Grow in={!this.state.scrolling}>
  <AppBar>
    <Toolbar>
      <IconButton
        className={classes.menuButton}
        color="inherit"
        aria-label="Menu"
      >
        <MenuIcon />
      </IconButton>
      <Typography
        variant="title"
        color="inherit"
        className={classes.flex}
      >
        My Title
      </Typography>
      <Button color="inherit">Login</Button>
    </Toolbar>
  </AppBar>
</Grow>

参见

工具栏抽象

如果你需要在多个地方渲染工具栏,工具栏代码可能会变得冗长。为了解决这个问题,你可以创建自己的 Toolbar 组件,该组件封装了工具栏的内容模式,使得在多个地方渲染 AppBar 组件更容易。

如何实现...

假设你的应用在多个屏幕上渲染 AppBar 组件。每个 AppBar 组件也会将 Menutitle 渲染到左侧,以及 Button 渲染到右侧。以下是如何实现你自己的 AppBar 组件,以便在多个屏幕上更容易使用:

import React, { Fragment, Component } from 'react';

import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  },
  toolbarMargin: theme.mixins.toolbar
});

const MyToolbar = withStyles(styles)(
  class extends Component {
    static defaultProps = {
      MenuItems: ({ closeMenu }) => (
        <Fragment>
          <MenuItem onClick={closeMenu}>Profile</MenuItem>
          <MenuItem onClick={closeMenu}>My account</MenuItem>
          <MenuItem onClick={closeMenu}>Logout</MenuItem>
        </Fragment>
      ),
      RightButton: () => <Button color="inherit">Login</Button>
    };

    state = { anchor: null };

    closeMenu = () => this.setState({ anchor: null });

    render() {
      const { classes, title, MenuItems, RightButton } = this.props;

      return (
        <Fragment>
          <AppBar>
            <Toolbar>
              <IconButton
                className={classes.menuButton}
                color="inherit"
                aria-label="Menu"
                onClick={e =>
                  this.setState({ anchor: e.currentTarget })
                }
              >
                <MenuIcon />
              </IconButton>
              <Menu
                anchorEl={this.state.anchor}
                open={Boolean(this.state.anchor)}
                onClose={this.closeMenu}
              >
                <MenuItems closeMenu={this.closeMenu} />
              </Menu>
              <Typography
                variant="title"
                color="inherit"
                className={classes.flex}
              >
                {title}
              </Typography>
              <RightButton />
            </Toolbar>
          </AppBar>
          <div className={classes.toolbarMargin} />
        </Fragment>
      );
    }
  }
);

const ToolbarAbstraction = withStyles(styles)(
  ({ classes, ...props }) => (
    <div className={classes.root}>
      <MyToolbar {...props} />
    </div>
  )
);

export default ToolbarAbstraction;

这是最终工具栏的外观:

图片

当用户点击标题旁边的菜单按钮时,菜单看起来是这样的:

图片

它是如何工作的...

让我们从查看 MyToolbar 组件的 render() 方法开始,如下所示:

render() {
  const { classes, title, MenuItems, RightButton } = this.props;

  return (
    <Fragment>
      <AppBar>
        <Toolbar>
          <IconButton
            className={classes.menuButton}
            color="inherit"
            aria-label="Menu"
            onClick={e =>
              this.setState({ anchor: e.currentTarget })
            }
          >
            <MenuIcon />
          </IconButton>
          <Menu
            anchorEl={this.state.anchor}
            open={Boolean(this.state.anchor)}
            onClose={this.closeMenu}
          >
            <MenuItems closeMenu={this.closeMenu} />
          </Menu>
          <Typography
            variant="title"
            color="inherit"
            className={classes.flex}
          >
            {title}
          </Typography>
          <RightButton />
        </Toolbar>
      </AppBar>
      <div className={classes.toolbarMargin} />
    </Fragment>
  );
}

这就是 AppBar 组件和 Material-UI 中的 Toolbar 组件被渲染的地方。使用了一个 Fragment 组件,因为返回了两个元素:AppBar 组件和设置页面内容顶部边距的 <div> 元素。在工具栏中,您有以下内容:

  • 点击时显示菜单的菜单按钮

  • 菜单本身

  • 标题

  • 右侧按钮

MyToolbar 属性中,render() 方法使用了两个组件:MenuItemsRightButton。除了 title 属性外,这些是您想要自定义的 AppBar 组件的部分。这里的做法是为这些属性定义默认值,以便 AppBar 组件可以被渲染:

static defaultProps = {
  MenuItems: ({ closeMenu }) => (
    <Fragment>
      <MenuItem onClick={closeMenu}>Profile</MenuItem>
      <MenuItem onClick={closeMenu}>My account</MenuItem>
      <MenuItem onClick={closeMenu}>Logout</MenuItem>
    </Fragment>
  ),
  RightButton: () => <Button color="inherit">Login</Button>
};

当您渲染 MyToolbar 时,可以向这些属性传递自定义值。这里使用的默认值可能是用于主页的值。

您实际上不必为这些属性提供默认值。但如果您提供了,比如对于主页,那么其他开发者查看您的代码并理解其工作方式会更容易。

更多内容...

让我们尝试设置一些自定义菜单项和右侧按钮,分别使用 MenuItemsRightButton 属性:

const ToolbarAbstraction = withStyles(styles)(
  ({ classes, ...props }) => (
    <div className={classes.root}>
      <MyToolbar
        MenuItems={({ closeMenu }) => (
          <Fragment>
            <MenuItem onClick={closeMenu}>Page 1</MenuItem>
            <MenuItem onClick={closeMenu}>Page 2</MenuItem>
            <MenuItem onClick={closeMenu}>Page 3</MenuItem>
          </Fragment>
        )}
        RightButton={() => (
          <Button color="secondary" variant="contained">
            Logout
          </Button>
        )}
        {...props}
      />
    </div>
  )
);

这是渲染后的工具栏看起来:

图片

这是带有自定义菜单选项的菜单看起来:

图片

您传递给 MenuItemsRightButton 的值是返回 React 元素的函数。这些函数实际上是您即时创建的功能组件。

参见

带有导航

Material-UI 应用程序通常由几个页面组成,这些页面通过路由器(如 react-router)相互链接。每个页面渲染一个具有特定页面信息的 App Bar。这是在 Toolbar 抽象 菜谱中创建的抽象的一个用例。

如何实现...

假设您正在构建一个有三个页面的应用程序。在每个页面上,您想要 render 一个具有页面 title 属性的 App Bar。此外,AppBar 中的菜单应包含指向三个页面的链接。以下是实现方法:

import React, { Fragment, Component } from 'react';
import {
  BrowserRouter as Router,
  Route,
  Link
} from 'react-router-dom';

import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  },
  toolbarMargin: theme.mixins.toolbar
});

const MyToolbar = withStyles(styles)(
  class extends Component {
    static defaultProps = {
      MenuItems: () => (
        <Fragment>
          <MenuItem component={Link} to="/">
            Home
          </MenuItem>
          <MenuItem component={Link} to="/page2">
            Page 2
          </MenuItem>
          <MenuItem component={Link} to="/page3">
            Page 3
          </MenuItem>
        </Fragment>
      ),
      RightButton: () => <Button color="inherit">Login</Button>
    };

    state = { anchor: null };

    closeMenu = () => this.setState({ anchor: null });

    render() {
      const { classes, title, MenuItems, RightButton } = this.props;

      return (
        <Fragment>
          <AppBar>
            <Toolbar>
              <IconButton
                className={classes.menuButton}
                color="inherit"
                aria-label="Menu"
                onClick={e =>
                  this.setState({ anchor: e.currentTarget })
                }
              >
                <MenuIcon />
              </IconButton>
              <Menu
                anchorEl={this.state.anchor}
                open={Boolean(this.state.anchor)}
                onClose={this.closeMenu}
              >
                <MenuItems />
              </Menu>
              <Typography
                variant="title"
                color="inherit"
                className={classes.flex}
              >
                {title}
              </Typography>
              <RightButton />
            </Toolbar>
          </AppBar>
          <div className={classes.toolbarMargin} />
        </Fragment>
      );
    }
  }
);

const WithNavigation = withStyles(styles)(({ classes }) => (
  <div className={classes.root}>
    <Route
      exact
      path="/"
      render={() => (
        <Fragment>
          <MyToolbar title="Home" />
          <Typography>Home</Typography>
        </Fragment>
      )}
    />
    <Route
      exact
      path="/page2"
      render={() => (
        <Fragment>
          <MyToolbar title="Page 2" />
          <Typography>Page 2</Typography>
        </Fragment>
      )}
    />
    <Route
      exact
      path="/page3"
      render={() => (
        <Fragment>
          <MyToolbar title="Page 3" />
          <Typography>Page 3</Typography>
        </Fragment>
      )}
    />
  </div>
));

export default WithNavigation;

当您首次加载应用程序时,您会看到以下内容:

图片

当 App Bar 被打开时,菜单看起来是这样的:

尝试点击第 2 页;您应该看到以下内容:

App Bar 的标题已更改,以反映页面的标题,页面的内容也发生了变化。

它是如何工作的...

让我们先看看定义您应用中页面的 Routes 组件,如下所示:

const WithNavigation = withStyles(styles)(({ classes }) => (
  <div className={classes.root}>
    <Route
      exact
      path="/"
      render={() => (
        <Fragment>
          <MyToolbar title="Home" />
          <Typography>Home</Typography>
        </Fragment>
      )}
    />
    <Route
      exact
      path="/page2"
      render={() => (
        <Fragment>
          <MyToolbar title="Page 2" />
          <Typography>Page 2</Typography>
        </Fragment>
      )}
    />
    <Route
      exact
      path="/page3"
      render={() => (
        <Fragment>
          <MyToolbar title="Page 3" />
          <Typography>Page 3</Typography>
        </Fragment>
      )}
    />
  </div>
));

每个 Route 组件(来自 react-router 包)对应您应用中的一个页面。它们有一个 path 属性,与浏览器地址栏中的路径匹配。当有匹配时,此 Routes 组件的内容会被渲染。例如,当路径是 /page3 时,会渲染 path="/page3"Route 组件的内容。

每个 Route 组件还定义了一个 render() 函数。当其 path 匹配时,会调用此函数,并渲染返回的内容。您应用中的 Routes 组件每个都会以不同的 title 属性值渲染 MyToolbar

接下来,让我们看看组成 MenuItems 默认属性值的菜单项,如下所示:

static defaultProps = {
  MenuItems: () => (
    <Fragment>
      <MenuItem component={Link} to="/">
        Home
      </MenuItem>
      <MenuItem component={Link} to="/page2">
        Page 2
      </MenuItem>
      <MenuItem component={Link} to="/page3">
        Page 3
      </MenuItem>
    </Fragment>
  ),
  RightButton: () => <Button color="inherit">Login</Button>
};

这些 MenuItems 属性中的每一个都是一个指向您应用中声明的每个 Routes 组件的链接。MenuItem 组件接受一个 component 属性,用于渲染链接。在这个例子中,您传递了来自 react-router-dom 包的 Link 组件。MenuItem 组件会将任何额外的属性传递给 Link 组件,这意味着您可以将 to 属性传递给 MenuItem 组件,这就像您将其传递给 Link 组件一样。

更多内容...

大多数时候,组成您应用屏幕的屏幕将遵循相同的模式。您不必在路由的 render 属性中有重复的代码,可以创建一个高阶函数,该函数接受屏幕独特部分的参数,并返回一个可以由 render 属性使用的新组件。

在这个例子中,每个屏幕唯一的数据只有标题和内容文本。以下是一个通用函数,它构建了一个新的函数组件,可以用于应用中的每个 Route 组件:

const screen = (title, content) => () => (
  <Fragment>
    <MyToolbar title={title} />
    <Typography>{content}</Typography>
  </Fragment>
);

要使用此函数,请在 render 属性中调用它,如下代码块所示:

export default withStyles(styles)(({ classes }) => (
  <div className={classes.root}>
    <Route exact path="/" render={screen('Home', 'Home')} />
    <Route exact path="/page2" render={screen('Page 2', 'Page 2')} />
    <Route exact path="/page3" render={screen('Page 3', 'Page 3')} />
  </div>
));

现在您已经清楚地分离了静态的 screen 结构,它在应用中的每个屏幕上都是相同的,以及作为 screen() 函数参数传递的每个屏幕的独特部分。

参见

第三章:抽屉 - 导航控制的位置

在本章中,你将学习以下食谱:

  • 抽屉类型

  • 抽屉项目状态

  • 抽屉项目导航

  • 抽屉部分

  • AppBar 交互

简介

Material-UI 使用抽屉向用户提供应用程序的主要导航。Drawer组件就像一个物理抽屉,当它未被使用时可以移出视图。

抽屉类型

在你的应用程序中,你将使用以下三种类型的Drawer组件,如下所示:

  • 临时:一个在执行操作时关闭的短暂抽屉。

  • 持久:一个可以打开并保持打开状态直到明确关闭的抽屉。

  • 永久:一个始终可见的抽屉。

如何实现...

假设你想要在应用程序中支持不同类型的抽屉。你可以使用variant属性来控制Drawer组件类型。以下是代码:

import React, { useState } from 'react';

import Drawer from '@material-ui/core/Drawer';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';

export default function DrawerTypes({ classes, variant }) {
  const [open, setOpen] = useState(false);

  return (
    <Grid container justify="space-between">
      <Grid item>
        <Drawer
          variant={variant}
          open={open}
          onClose={() => setOpen(false)}
        >
          <List>
            <ListItem
              button
              onClick={() => setOpen(false)}
            >
              <ListItemText>Home</ListItemText>
            </ListItem>
            <ListItem
              button
              onClick={() => setOpen(false)}
            >
              <ListItemText>Page 2</ListItemText>
            </ListItem>
            <ListItem
              button
              onClick={() => setOpen(false)}
            >
              <ListItemText>Page 3</ListItemText>
            </ListItem>
          </List>
        </Drawer>
      </Grid>
      <Grid item>
        <Button onClick={() => setOpen(!open)}>
          {open ? 'Hide' : 'Show'} Drawer
        </Button>
      </Grid>
    </Grid>
  );
}

variant属性默认为temporary。当你首次加载此屏幕时,你将只会看到切换抽屉显示的按钮:

图片

当你点击此按钮时,你会看到一个临时抽屉:

图片

它是如何工作的...

在你开始更改variant属性之前,让我们先浏览一下这个示例中的代码,从Drawer标记开始,如下所示:

<Drawer
  variant={variant}
  open={open}
  onClose={() => setOpen(false)}
>
  <List>
    <ListItem
      button
      onClick={() => setOpen(false)}
    >
      <ListItemText>Home</ListItemText>
    </ListItem>
    <ListItem
      button
      onClick={() => setOpen(false)}
    >
      <ListItemText>Page 2</ListItemText>
    </ListItem>
    <ListItem
      button
      onClick={() => setOpen(false)}
    >
      <ListItemText>Page 3</ListItemText>
    </ListItem>
  </List>
</Drawer>

Drawer组件接受一个open属性,当为true时显示抽屉。variant属性决定了要渲染的抽屉类型。之前显示的截图是一个临时抽屉,默认的变体值。Drawer组件的子组件是List,其中抽屉中显示的每个项目都会被渲染。

接下来,让我们看看用于切换Drawer组件显示的Button组件:

<Button onClick={() => setOpen(!open)}>
  {open ? 'Hide' : 'Show'} Drawer
</Button>

当你点击此按钮时,你的组件的open状态会切换。同样,按钮的文本也会根据open状态的值切换。

现在让我们尝试将variant属性的值更改为permanent。以下是抽屉渲染后的样子:

图片

永久抽屉,正如其名所示,始终可见,并且始终位于屏幕上的同一位置。如果你点击显示抽屉按钮,你的组件的open状态会被切换为true。你会看到按钮的文本改变,但由于Drawer组件使用的是permanent变体,所以open属性没有效果:

图片

接下来,让我们尝试使用persistent变体。持久抽屉与永久抽屉类似,在用户与应用程序交互时它们会保持在屏幕上可见,并且它们与临时抽屉类似,可以通过更改open属性来隐藏。

让我们将variant属性更改为persistent。当屏幕首次加载时,抽屉不可见,因为组件的open状态是false。尝试点击 SHOW DRAWER 按钮。抽屉被显示,看起来像永久抽屉。如果您点击 HIDE DRAWER 按钮,组件的open状态将切换到false,抽屉将被隐藏。

当您希望用户能够控制抽屉的可见性时,应使用持久抽屉。例如,使用临时抽屉时,用户可以通过点击覆盖层或按Esc键来关闭抽屉。当您希望将左侧导航作为页面布局的组成部分时,永久抽屉非常有用——它们始终可见,其他项目则围绕它们布局。

还有更多...

当您点击抽屉中的任何项目时,事件处理器会将组件的open状态设置为false。这可能不是您想要的,可能会让您的用户感到困惑。例如,如果您使用的是持久抽屉,那么您的应用可能有一个位于抽屉外的按钮来控制抽屉的可见性。如果用户点击抽屉项目,他们可能不会期望抽屉关闭。

为了解决这个问题,您的事件处理器可以考虑Drawer组件的一个变体:

<List>
  <ListItem
    button
    onClick={() => setOpen(variant !== 'temporary')}
  >
    <ListItemText>Home</ListItemText>
  </ListItem>
  <ListItem
    button
    onClick={() => setOpen(variant !== 'temporary')}
  >
    <ListItemText>Page 2</ListItemText>
  </ListItem>
  <ListItem
    button
    onClick={() => setOpen(variant !== 'temporary')}
  >
    <ListItemText>Page 3</ListItemText>
  </ListItem>
</List>

现在,当您点击这些项目中的任何一个时,只有当variant属性是temporary时,open状态才会更改为false

参见

抽屉项目状态

Drawer组件中渲染的项目很少是静态的。相反,抽屉项目是根据组件的状态渲染的,这允许您对项目的显示方式有更多的控制。

如何做到这一点...

假设您有一个使用Drawer组件渲染抽屉导航的组件。您不想直接在组件标记中写入items状态,而是希望将items状态存储在组件的状态中。例如,在响应用户的权限检查时,项目可能会被禁用或完全隐藏。

这里有一个使用组件状态中的item对象数组的示例:

import React, { useState } from 'react';

import Drawer from '@material-ui/core/Drawer';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';

import HomeIcon from '@material-ui/icons/Home';
import WebIcon from '@material-ui/icons/Web';

export default function DrawerItemState() {
  const [open, setOpen] = useState(false);
  const [content, setContent] = useState('Home');
  const [items] = useState([
    { label: 'Home', Icon: HomeIcon },
    { label: 'Page 2', Icon: WebIcon },
    { label: 'Page 3', Icon: WebIcon, disabled: true },
    { label: 'Page 4', Icon: WebIcon },
    { label: 'Page 5', Icon: WebIcon, hidden: true }
  ]);

  const onClick = content => () => {
    setOpen(false);
    setContent(content);
  };

  return (
    <Grid container justify="space-between">
      <Grid item>
        <Typography>{content}</Typography>
      </Grid>
      <Grid item>
        <Drawer open={open} onClose={() => setOpen(false)}>
          <List>
            {items
              .filter(({ hidden }) => !hidden)
              .map(({ label, disabled, Icon }, i) => (
                <ListItem
                  button
                  key={i}
                  disabled={disabled}
                  onClick={onClick(label)}
                >
                  <ListItemIcon>
                    <Icon />
                  </ListItemIcon>
                  <ListItemText>{label}</ListItemText>
                </ListItem>
              ))}
          </List>
        </Drawer>
      </Grid>

      <Grid item>
        <Button onClick={() => setOpen(!open)}>
          {open ? 'Hide' : 'Show'} Drawer
        </Button>
      </Grid>
    </Grid>
  );
}

点击 SHOW DRAWER 按钮时,抽屉看起来是这样的:

图片

如果您选择这些项目中的任何一个,抽屉将关闭,屏幕内容将更新;例如,在点击页面 2 后,您应该看到以下截图类似的内容:

图片

它是如何工作的...

让我们从查看组件的状态开始:

const [open, setOpen] = useState(false);
const [content, setContent] = useState('Home');
const [items] = useState([
  { label: 'Home', Icon: HomeIcon },
  { label: 'Page 2', Icon: WebIcon },
  { label: 'Page 3', Icon: WebIcon, disabled: true },
  { label: 'Page 4', Icon: WebIcon },
  { label: 'Page 5', Icon: WebIcon, hidden: true }
]);

open状态控制Drawer组件的可见性,content状态是屏幕上显示的文本,取决于哪个抽屉项被点击。items状态是一个用于渲染抽屉项的对象数组。每个对象都有一个label属性和一个Icon属性,分别用于渲染项目文本和图标。

为了保持 React 组件大写命名约定,Icon属性被大写。这使得在阅读代码时更容易区分 React 组件和其他数据。

disabled属性用于将项目渲染为禁用状态;例如,通过将此属性设置为true,将第 3 页标记为禁用:

图片

这可能是由于用户在此特定页面上存在权限限制或其他原因。因为这是通过组件状态而不是静态渲染来控制的,所以你可以使用任何你喜欢的机制(如 API 调用)在任何时候更新任何菜单项的disabled状态。hidden属性使用相同的原则,只是当此值为true时,项目根本不会渲染。在这个例子中,第 5 页没有渲染,因为它被标记为隐藏。

接下来,让我们看看如何根据items状态渲染List项,如下所示:

<List>
  {items
    .filter(({ hidden }) => !hidden)
    .map(({ label, disabled, Icon }, i) => (
      <ListItem
        button
        key={i}
        disabled={disabled}
        onClick={onClick(label)}
      >
        <ListItemIcon>
          <Icon />
        </ListItemIcon>
        <ListItemText>{label}</ListItemText>
      </ListItem>
    ))}
</List>

首先,items数组被过滤以移除hidden项。然后,使用map()渲染每个ListItem组件。将disabled属性传递给ListItem,当渲染时它将显示为禁用状态。Icon组件也来自列表项状态。onClick()事件处理程序隐藏抽屉并更新content标签。

当点击禁用列表项时,onClick()处理程序不会执行。

还有更多...

你可能希望将列表项的渲染分离成独立的组件。这样,你可以在其他地方使用列表项。例如,你可能希望在其他地方使用相同的渲染逻辑来渲染按钮列表。以下是一个如何将ListItems组件提取为独立组件的示例:

const ListItems = ({ items, onClick }) =>
  items
    .filter(({ hidden }) => !hidden)
    .map(({ label, disabled, Icon }, i) => (
      <ListItem
        button
        key={i}
        disabled={disabled}
        onClick={onClick(label)}
      >
        <ListItemIcon>
          <Icon />
        </ListItemIcon>
        <ListItemText>{label}</ListItemText>
      </ListItem>
    ));

ListItems组件将返回一个ListItem组件的数组。它接受一个作为数组属性的items状态来渲染。它还接受一个onClick()函数属性。这是一个高阶函数,它接受要显示的label组件作为参数,并返回一个新函数,当项目被点击时将更新内容。

这是新的 JSX 标记的示例,已更新为使用新的ListItems组件:

<Grid container justify="space-between">
  <Grid item>
    <Typography>{content}</Typography>
  </Grid>
  <Grid item>
    <Drawer open={open} onClose={() => setOpen(false)}>
      <List>
        <ListItems items={items} onClick={onClick} />
      </List>
    </Drawer>
  </Grid>

  <Grid item>
    <Button onClick={() => setOpen(!open)}>
      {open ? 'Hide' : 'Show'} Drawer
    </Button>
  </Grid>
</Grid>

在此组件中不再有列表项渲染代码。相反,ListItems作为List的子组件被渲染。你传递给它要渲染的项目和onClick()处理程序。现在你有一个通用的ListItems组件,可以在你应用中显示列表的任何地方使用。它将在任何使用位置一致地处理Icondisabled和显示逻辑。

参见

抽屉项目导航

如果你的 Material-UI 应用使用react-router等路由器在页面之间导航,你可能希望将链接作为Drawer项目。为此,你必须集成来自react-router-dom包的组件。

如何操作...

假设你的应用由三个页面组成。为了在页面之间导航,你希望在Drawer组件中为用户提供链接。以下是代码的样子:

import React, { useState } from 'react';
import { Route, Link } from 'react-router-dom';

import { withStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';

import HomeIcon from '@material-ui/icons/Home';
import WebIcon from '@material-ui/icons/Web';

const styles = theme => ({
  alignContent: {
    alignSelf: 'center'
  }
});

function DrawerItemNavigation({ classes }) {
  const [open, setOpen] = useState(false);

  return (
    <Grid container justify="space-between">
      <Grid item className={classes.alignContent}>
        <Route
          exact
          path="/"
          render={() => <Typography>Home</Typography>}
        />
        <Route
          exact
          path="/page2"
          render={() => <Typography>Page 2</Typography>}
        />
        <Route
          exact
          path="/page3"
          render={() => <Typography>Page 3</Typography>}
        />
      </Grid>
      <Grid item>
        <Drawer
          className={classes.drawerWidth}
          open={open}
          onClose={() => setOpen(false)}
        >
          <List>
            <ListItem
              component={Link}
              to="/"
              onClick={() => setOpen(false)}
            >
              <ListItemIcon>
                <HomeIcon />
              </ListItemIcon>
              <ListItemText>Home</ListItemText>
            </ListItem>
            <ListItem
              component={Link}
              to="/page2"
              onClick={() => setOpen(false)}
            >
              <ListItemIcon>
                <WebIcon />
              </ListItemIcon>
              <ListItemText>Page 2</ListItemText>
            </ListItem>
            <ListItem
              component={Link}
              to="/page3"
              onClick={() => setOpen(false)}
            >
              <ListItemIcon>
                <WebIcon />
              </ListItemIcon>
              <ListItemText>Page 3</ListItemText>
            </ListItem>
          </List>
        </Drawer>
      </Grid>
      <Grid item>
        <Button onClick={() => setOpen(!open)}>
          {open ? 'Hide' : 'Show'} Drawer
        </Button>
      </Grid>
    </Grid>
  );
}

export default withStyles(styles)(DrawerItemNavigation);

当你首次加载屏幕时,你会看到显示抽屉按钮和主页内容:

图片

这是抽屉打开时的样子:

图片

如果你点击第 2 页,它指向/page2,抽屉应该关闭,并且你应该被带到第二页。以下是它的样子:

图片

如果你点击第 3 页或主页,你应该能看到类似的内容。屏幕左侧的内容会更新。

它是如何工作的...

让我们先看看基于活动Route组件render内容的Route组件:

<Grid item className={classes.alignContent}>
  <Route
    exact
    path="/"
    render={() => <Typography>Home</Typography>}
  />
  <Route
    exact
    path="/page2"
    render={() => <Typography>Page 2</Typography>}
  />
  <Route
    exact
    path="/page3"
    render={() => <Typography>Page 3</Typography>}
  />
</Grid>

每个应用中的path都会使用一个Route组件。render()函数返回当path属性与当前 URL 匹配时应在Grid项中渲染的内容。

接下来,让我们看看Drawer组件中的一个ListItem组件,如下所示:

<ListItem
  component={Link}
  to="/"
  onClick={() => setOpen(false)}
>
  <ListItemIcon>
    <HomeIcon />
  </ListItemIcon>
  <ListItemText>Home</ListItemText>
</ListItem>

默认情况下,ListItem组件将渲染一个div元素。它接受一个button属性,当为true时,将渲染一个button元素。你都不需要这些。相反,你希望列表项是react-router将处理的链接。component属性接受一个自定义组件来使用;在这个例子中,你想要使用来自react-router-dom包的Link组件。这将渲染适当的链接,同时保持正确的样式。

你传递给ListItem组件的属性也会传递给你的自定义组件,在这个例子中,是Link组件。这意味着必需的to属性被传递给Link组件,指向/。同样,onClick处理程序也被传递给Link组件,这很重要,因为你想在点击链接时关闭临时抽屉。

更多内容...

当你的抽屉中的项是链接时,你可能想要为活动链接提供一个视觉指示。挑战在于你想使用 Material-UI 主题样式来样式化活动链接。以下是修改后的示例:

import React, { useState } from 'react';
import clsx from 'clsx';
import { Switch, Route, Link, NavLink } from 'react-router-dom';

import { withStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import Typography from '@material-ui/core/Typography';

import HomeIcon from '@material-ui/icons/Home';
import WebIcon from '@material-ui/icons/Web';

const styles = theme => ({
  alignContent: {
    alignSelf: 'center'
  },
  activeListItem: {
    color: theme.palette.primary.main
  }
});

const NavListItem = withStyles(styles)(
  ({ classes, Icon, text, active, ...other }) => (
    <ListItem component={NavLink} {...other}>
      <ListItemIcon
        classes={{
          root: clsx({ [classes.activeListItem]: active })
        }}
      >
        <Icon />
      </ListItemIcon>
      <ListItemText
        classes={{
          primary: clsx({
            [classes.activeListItem]: active
          })
        }}
      >
        {text}
      </ListItemText>
    </ListItem>
  )
);

const NavItem = props => (
  <Switch>
    <Route
      exact
      path={props.to}
      render={() => <NavListItem active={true} {...props} />}
    />
    <Route path="/" render={() => <NavListItem {...props} />} />
  </Switch>
);

function DrawerItemNavigation({ classes }) {
  const [open, setOpen] = useState(false);

  return (
    <Grid container justify="space-between">
      <Grid item className={classes.alignContent}>
        <Route
          exact
          path="/"
          render={() => <Typography>Home</Typography>}
        />
        <Route
          exact
          path="/page2"
          render={() => <Typography>Page 2</Typography>}
        />
        <Route
          exact
          path="/page3"
          render={() => <Typography>Page 3</Typography>}
        />
      </Grid>
      <Grid item>
        <Drawer
          className={classes.drawerWidth}
          open={open}
          onClose={() => setOpen(false)}
        >
          <List>
            <NavItem
              to="/"
              text="Home"
              Icon={HomeIcon}
              onClick={() => setOpen(false)}
            />
            <NavItem
              to="/page2"
              text="Page 2"
              Icon={WebIcon}
              onClick={() => setOpen(false)}
            />
            <NavItem
              to="/page3"
              text="Page 3"
              Icon={WebIcon}
              onClick={() => setOpen(false)}
            />
          </List>
        </Drawer>
      </Grid>
      <Grid item>
        <Button onClick={() => setOpen(!open)}>
          {open ? 'Hide' : 'Show'} Drawer
        </Button>
      </Grid>
    </Grid>
  );
}

export default withStyles(styles)(DrawerItemNavigation);

现在,当屏幕首次加载并打开抽屉时,它应该看起来类似于以下截图:

图片

由于主页链接处于活动状态,它使用 Material-UI 主题的基色进行样式化。如果你点击页面 2 链接然后再次打开抽屉,它应该看起来类似于以下截图:

图片

让我们看看你添加的两个新组件,从NavItem开始:

const NavItem = props => (
  <Switch>
    <Route
      exact
      path={props.to}
      render={() => <NavListItem active={true} {...props} />}
    />
    <Route path="/" render={() => <NavListItem {...props} />} />
  </Switch>
);

此组件用于根据当前 URL 确定项目是否处于活动状态。它使用来自react-router-domSwitch组件。Switch组件不仅会渲染Route组件,而且只会渲染与当前 URL 匹配的第一个路由。NavItem中的第一个Route组件是特定路径(因为它使用了exact属性)。如果这个Route组件匹配,它将渲染一个将active属性设置为 true 的NavListItem组件。因为它在Switch组件中,所以第二个Route组件将不会被渲染。

另一方面,如果第一个Route组件不匹配,第二个Route组件将始终匹配。这将渲染一个不带active属性的NavListItem组件。现在,让我们看一下NavListItem组件,如下所示:

const NavListItem = withStyles(styles)(
  ({ classes, Icon, text, active, ...other }) => (
    <ListItem component={NavLink} {...other}>
      <ListItemIcon
        classes={{
          root: clsx({ [classes.activeListItem]: active })
        }}
      >
        <Icon />
      </ListItemIcon>
      <ListItemText
        classes={{
          primary: clsx({
            [classes.activeListItem]: active
          })
        }}
      >
        {text}
      </ListItemText>
    </ListItem>
  )
);

NavListItem组件现在负责在Drawer组件中渲染ListItem组件。它接受一个text属性和一个Icon属性来分别渲染标签和图标,就像在你增强之前一样。active属性用于确定应用于ListItemIconListItemText组件的类。如果active为 true,则将应用activeListItem CSS 类到这两个组件上。这就是你能够根据 Material-UI 主题样式化活动项的方式。

clsx()函数在 Material-UI 中被广泛使用——这不是一个额外的依赖。它允许你动态地更改元素的类,而无需在标记中引入自定义逻辑。例如,clsx({ [classes.activeListItem]: active })语法只有在active为 true 时才会应用activeListItem类。另一种方法将涉及在你的组件中引入更多的逻辑。

最后,让我们看一下activeListItem类,如下所示:

const styles = theme => ({
  alignContent: {
    alignSelf: 'center'
  },
  activeListItem: {
    color: theme.palette.primary.main
  }
});

activeListItem类通过使用theme.palette.primary.main值来设置颜色 CSS 属性。这意味着如果主题发生变化,抽屉中的活动链接将被相应地样式化。

参见

抽屉部分

当你在你的Drawer中有大量项时,你可能想要将你的抽屉分成几个部分。当你有大量的抽屉项而没有部分时,你最终不得不在项本身中放入部分名称,这会导致混乱和不自然的抽屉项标签。

如何做到这一点...

假设你正在开发一个应用程序,该应用程序有用于管理 CPU、内存、存储和网络不同方面的屏幕。你可以在相关的部分中显示抽屉项,而不是有一个平面的抽屉项列表,这样可以更容易地进行导航。以下是实现这一点的代码:

import React, { useState } from 'react';

import { withStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import ListSubheader from '@material-ui/core/ListSubheader';
import Typography from '@material-ui/core/Typography';

import AddIcon from '@material-ui/icons/Add';
import RemoveIcon from '@material-ui/icons/Remove';
import ShowChartIcon from '@material-ui/icons/ShowChart';

const styles = theme => ({
  alignContent: {
    alignSelf: 'center'
  }
});

const ListItems = ({ items, onClick }) =>
  items
    .filter(({ hidden }) => !hidden)
    .map(({ label, disabled, Icon }, i) => (
      <ListItem
        button
        key={i}
        disabled={disabled}
        onClick={onClick(label)}
      >
        <ListItemIcon>
          <Icon />
        </ListItemIcon>
        <ListItemText>{label}</ListItemText>
      </ListItem>
    ));

const DrawerSections = withStyles(styles)(({ classes }) => {
  const [open, setOpen] = useState(false);
  const [content, setContent] = useState('Home');
  const [items] = useState({
    cpu: [
      { label: 'Add CPU', Icon: AddIcon },
      { label: 'Remove CPU', Icon: RemoveIcon },
      { label: 'Usage', Icon: ShowChartIcon }
    ],
    memory: [
      { label: 'Add Memory', Icon: AddIcon },
      { label: 'Usage', Icon: ShowChartIcon }
    ],
    storage: [
      { label: 'Add Storage', Icon: AddIcon },
      { label: 'Usage', Icon: ShowChartIcon }
    ],
    network: [
      { label: 'Add Network', Icon: AddIcon, disabled: true },
      { label: 'Usage', Icon: ShowChartIcon }
    ]
  });

  const onClick = content => () => {
    setOpen(false);
    setContent(content);
  };

  return (
    <Grid container justify="space-between">
      <Grid item className={classes.alignContent}>
        <Typography>{content}</Typography>
      </Grid>
      <Grid item>
        <Drawer open={open} onClose={() => setOpen(false)}>
          <List>
            <ListSubheader>CPU</ListSubheader>
            <ListItems items={items.cpu} onClick={onClick} />
            <ListSubheader>Memory</ListSubheader>
            <ListItems items={items.memory} onClick={onClick} />
            <ListSubheader>Storage</ListSubheader>
            <ListItems items={items.storage} onClick={onClick} />
            <ListSubheader>Network</ListSubheader>
            <ListItems items={items.network} onClick={onClick} />
          </List>
        </Drawer>
      </Grid>

      <Grid item>
        <Button onClick={() => setOpen(!open)}>
          {open ? 'Hide' : 'Show'} Drawer
        </Button>
      </Grid>
    </Grid>
  );
});

export default DrawerSections;

当你点击 SHOW DRAWER 按钮时,你的抽屉应该看起来像这样:

这个抽屉中有许多添加和使用项。部分使你的用户更容易扫描这些项。

它是如何工作的...

让我们先看看你的组件状态,如下所示:

const [open, setOpen] = useState(false);
const [content, setContent] = useState('Home');
const [items] = useState({
  cpu: [
    { label: 'Add CPU', Icon: AddIcon },
    { label: 'Remove CPU', Icon: RemoveIcon },
    { label: 'Usage', Icon: ShowChartIcon }
  ],
  memory: [
    { label: 'Add Memory', Icon: AddIcon },
    { label: 'Usage', Icon: ShowChartIcon }
  ],
  storage: [
    { label: 'Add Storage', Icon: AddIcon },
    { label: 'Usage', Icon: ShowChartIcon }
  ],
  network: [
    { label: 'Add Network', Icon: AddIcon, disabled: true },
    { label: 'Usage', Icon: ShowChartIcon }
  ]
});

items状态是一个平面的项数组不同,它现在是一个对象,其中数组按类别分组。这些是你想要渲染的抽屉部分。接下来,让我们看看用于渲染items状态和部分标题的List标记:

<List>
  <ListSubheader>CPU</ListSubheader>
  <ListItems items={items.cpu} onClick={onClick} />
  <ListSubheader>Memory</ListSubheader>
  <ListItems items={items.memory} onClick={onClick} />
  <ListSubheader>Storage</ListSubheader>
  <ListItems items={items.storage} onClick={onClick} />
  <ListSubheader>Network</ListSubheader>
  <ListItems items={items.network} onClick={onClick} />
</List>

当你需要在上面的列表项之上有一个标签时,使用ListSubheader组件。例如,在存储标题下面,你有ListItems组件,它从items.storage状态渲染项。

还有更多...

当你有大量的抽屉项和部分时,你仍然可以用需要解析的信息量压倒你的用户。一个解决方案是拥有可折叠的部分。为此,你可以在ListSubheader组件中添加一个Button组件,使其可点击。

下面是代码的样子:

<ListSubheader>
  <Button
    disableRipple
    classes={{ root: classes.listSubheader }}
    onClick={toggleSection('cpu')}
  >
    CPU
  </Button>
</ListSubheader>

当你点击按钮时通常会发生的水波效应在这里被禁用了,因为你希望标题文本仍然看起来像标题文本。这也需要在listSubheader类中进行一些 CSS 定制:

const styles = theme => ({
  alignContent: {
    alignSelf: 'center'
  },
  listSubheader: {
    padding: 0,
    minWidth: 0,
    color: 'inherit',
    '&:hover': {
      background: 'inherit'
    }
  }
});

当点击部分标题按钮时,它会切换部分的状态,进而切换部分项的可见性。以下是toggleSection()函数:

const toggleSection = name => () => {
  setSections({ ...sections, [name]: !sections[name] });
};

这是一个高阶函数,它返回一个新的函数作为按钮的onClick处理程序。name参数是要切换的部分状态名称。

这是添加以支持切换部分的新状态:

const [sections, setSections] = useState({
  cpu: true,
  memory: false,
  storage: false,
  network: false
});

当屏幕首次加载时,CPU 部分将是唯一有可见项的部分,因为它是最初状态为true的唯一状态。接下来,让我们看看当相应的部分状态为false时,ListItems是如何实际折叠的:

const ListItems = ({ items, visible, onClick }) => (
  <Collapse in={visible}>
    {items
      .filter(({ hidden }) => !hidden)
      .map(({ label, disabled, Icon }, i) => (
        <ListItem
          button
          key={i}
          disabled={disabled}
          onClick={onClick(label)}
        >
          <ListItemIcon>
            <Icon />
          </ListItemIcon>
          <ListItemText>{label}</ListItemText>
        </ListItem>
      ))}
  </Collapse>
);

ListItems组件现在接受一个visible属性。这个属性被Collapse组件使用,当隐藏组件时,它将使用折叠动画隐藏其子元素。最后,这是使用新的ListItems组件的方法:

<ListItems
  visible={sections.cpu}
  items={items.cpu}
  onClick={onClick}
/>

当屏幕首次加载时,并点击 SHOW DRAWER 按钮,你应该看到类似这样的东西:

现在用户需要解析的信息少多了。他们可以点击部分标题来查看列表项,并且可以再次点击来折叠部分;例如,他们可以折叠 CPU 部分并展开内存部分:

图片

参见

AppBar交互

在应用中每个页面的顶部放置一个切换Drawer组件可见性的按钮是一个常见的地方。此外,通过在抽屉中选择项目,AppBar组件的标题需要改变以反映此选择。DrawerAppBar组件通常需要相互交互。

如何实现...

假设你有一个包含一些项目的Drawer组件。你还有一个带有菜单按钮和标题的AppBar组件。菜单按钮应该切换抽屉的可见性,点击抽屉中的项目应该更新AppBar中的标题。以下是实现这一功能的代码:

import React, { useState, Fragment } from 'react';

import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import Drawer from '@material-ui/core/Drawer';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';

const styles = theme => ({
  root: {
    flexGrow: 1
  },
  flex: {
    flex: 1
  },
  menuButton: {
    marginLeft: -12,
    marginRight: 20
  },
  toolbarMargin: theme.mixins.toolbar
});

const MyToolbar = withStyles(styles)(
  ({ classes, title, onMenuClick }) => (
    <Fragment>
      <AppBar>
        <Toolbar>
          <IconButton
            className={classes.menuButton}
            color="inherit"
            aria-label="Menu"
            onClick={onMenuClick}
          >
            <MenuIcon />
          </IconButton>
          <Typography
            variant="title"
            color="inherit"
            className={classes.flex}
          >
            {title}
          </Typography>
        </Toolbar>
      </AppBar>
      <div className={classes.toolbarMargin} />
    </Fragment>
  )
);

const MyDrawer = withStyles(styles)(
  ({ classes, variant, open, onClose, setTitle }) => (
    <Drawer variant={variant} open={open} onClose={onClose}>
      <List>
        <ListItem
          button
          onClick={() => {
            setTitle('Home');
            onClose();
          }}
        >
          <ListItemText>Home</ListItemText>
        </ListItem>
        <ListItem
          button
          onClick={() => {
            setTitle('Page 2');
            onClose();
          }}
        >
          <ListItemText>Page 2</ListItemText>
        </ListItem>
        <ListItem
          button
          onClick={() => {
            setTitle('Page 3');
            onClose();
          }}
        >
          <ListItemText>Page 3</ListItemText>
        </ListItem>
      </List>
    </Drawer>
  )
);

function AppBarInteraction({ classes }) {
  const [drawer, setDrawer] = useState(false);
  const [title, setTitle] = useState('Home');

  const toggleDrawer = () => {
    setDrawer(!drawer);
  };

  return (
    <div className={classes.root}>
      <MyToolbar title={title} onMenuClick={toggleDrawer} />
      <MyDrawer
        open={drawer}
        onClose={toggleDrawer}
        setTitle={setTitle}
      />
    </div>
  );
}

export default withStyles(styles)(AppBarInteraction);

这是首次加载时的屏幕截图:

图片

当你点击标题左侧的菜单图标按钮时,你会看到抽屉:

图片

如果你点击页面 2 的项目,抽屉将关闭,AppBar的标题将改变:

图片

它是如何工作的...

此示例定义了三个组件,如下所示:

  • MyToolbar组件

  • MyDrawer组件

  • 主应用组件

让我们逐一分析这些,从MyToolbar开始:

const MyToolbar = withStyles(styles)(
  ({ classes, title, onMenuClick }) => (
    <Fragment>
      <AppBar>
        <Toolbar>
          <IconButton
            className={classes.menuButton}
            color="inherit"
            aria-label="Menu"
            onClick={onMenuClick}
          >
            <MenuIcon />
          </IconButton>
          <Typography
            variant="title"
            color="inherit"
            className={classes.flex}
          >
            {title}
          </Typography>
        </Toolbar>
      </AppBar>
      <div className={classes.toolbarMargin} />
    </Fragment>
  )
);

MyToolbar组件渲染一个接受title属性和onMenuClick()属性的AppBar组件。这两个属性都用于与MyDrawer组件交互。当抽屉项目被选中时,title属性会改变。onMenuClick()函数会在你的主应用组件中改变状态,导致抽屉显示。接下来,让我们看看MyDrawer

const MyDrawer = withStyles(styles)(
  ({ classes, variant, open, onClose, setTitle }) => (
    <Drawer variant={variant} open={open} onClose={onClose}>
      <List>
        <ListItem
          button
          onClick={() => {
            setTitle('Home');
            onClose();
          }}
        >
          <ListItemText>Home</ListItemText>
        </ListItem>
        <ListItem
          button
          onClick={() => {
            setTitle('Page 2');
            onClose();
          }}
        >
          <ListItemText>Page 2</ListItemText>
        </ListItem>
        <ListItem
          button
          onClick={() => {
            setTitle('Page 3');
            onClose();
          }}
        >
          <ListItemText>Page 3</ListItemText>
        </ListItem>
      </List>
    </Drawer>
  )
);

MyDrawer组件与MyToolbar类似,是函数式的。它接受属性而不是维护自己的状态。例如,open属性用于控制抽屉的可见性。onClose()setTitle()属性是在点击抽屉项目时被调用的函数。

最后,让我们看看包含所有状态的 app 组件:

function AppBarInteraction({ classes }) {
  const [drawer, setDrawer] = useState(false);
  const [title, setTitle] = useState('Home');

  const toggleDrawer = () => {
    setDrawer(!drawer);
  };

  return (
    <div className={classes.root}>
      <MyToolbar title={title} onMenuClick={toggleDrawer} />
      <MyDrawer
        open={drawer}
        onClose={toggleDrawer}
        setTitle={setTitle}
      />
    </div>
  );
}

title状态传递给MyDrawer组件,以及toggleDrawer()函数。MyDrawer组件接收抽屉状态以控制可见性,toggleDrawer()函数以改变可见性,以及setTitle()函数以更新MyToolbar中的标题。

更多...

如果您想要一个可以通过应用栏中的相同菜单按钮切换的持久抽屉的灵活性怎么办?让我们给传递给MyDrawerAppBarInteraction组件添加一个variant属性。这可以从temporary更改为persistent,菜单按钮仍然按预期工作。

当您点击菜单按钮时,这是一个持久抽屉的样子:

图片

抽屉覆盖了应用栏。另一个问题是,如果您点击任何抽屉项目,抽屉将关闭,这对持久抽屉来说并不理想。让我们修复这两个问题。

首先,让我们解决导致抽屉出现在应用栏上面的z-index问题。您可以创建一个看起来像这样的 CSS 类:

aboveDrawer: {
  zIndex: theme.zIndex.drawer + 1
}

您可以将此类应用于MyToolbar中的AppBar组件,如下所示:

<AppBar className={classes.aboveDrawer}>

现在当您打开抽屉时,它出现在AppBar下面,正如预期的那样:

图片

现在您只需调整边距。当抽屉使用persistent变体时,您可以将toolbarMargin类添加到<div>元素中,作为Drawer组件中的第一个元素:

<div
  className={clsx({
    [classes.toolbarMargin]: variant === 'persistent'
  })}
/>

clsx()函数的帮助下,toolbarMargin类仅在需要时添加——也就是说,当抽屉处于持久状态时。现在它看起来是这样的:

图片

最后,让我们修复当点击抽屉项目时抽屉关闭的问题。在主应用组件中,您可以添加一个看起来像以下代码块的新方法:

const onItemClick = title => () => {
  setTitle(title);
  setDrawer(variant === 'temporary' ? false : drawer);
};

onItemClick()函数负责设置应用栏中的文本,如果抽屉是临时的,它还会关闭抽屉。要使用这个新函数,您可以将MyDrawer中的setTitle属性替换为onItemClick属性。然后您可以在列表项中使用它,如下所示:

<List>
  <ListItem button onClick={onItemClick('Home')}>
    <ListItemText>Home</ListItemText>
  </ListItem>
  <ListItem button onClick={onItemClick('Page 2')}>
    <ListItemText>Page 2</ListItemText>
  </ListItem>
  <ListItem button onClick={onItemClick('Page 3')}>
    <ListItemText>Page 3</ListItemText>
  </ListItem>
</List>

现在当您在持久状态下的抽屉中点击项目时,抽屉将保持打开。唯一关闭它的方法是在应用栏标题旁边的菜单按钮上点击。

参见