React-材质-UI-秘籍-全-
React 材质 UI 秘籍(全)
原文:
zh.annas-archive.org/md5/c4e5ed8c3a8a54c4065e4c907829dab6
译者:飞龙
前言
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。
要充分利用这本书
-
确保您理解 React 的基础知识。教程是一个良好的起点:
reactjs.org/tutorial/tutorial.html
。 -
通过切换到
Material-UI-Cookbook
目录并运行npm install
来安装包。 -
通过运行
npm run storybook
来启动 Storybook。现在,您可以在阅读本书的同时浏览每个示例。一些示例在 Storybook UI 中具有属性编辑控件,但您在学习过程中可以随意调整代码!
下载示例代码文件
您可以从www.packt.com上的账户下载这本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packt.com 登录或注册。
-
选择支持选项卡。
-
点击代码下载与勘误。
-
在搜索框中输入书籍名称,并遵循屏幕上的说明。
文件下载后,请确保使用最新版本的软件解压缩或提取文件夹:
-
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-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现任何形式的我们作品的非法副本,我们非常感谢您能提供位置地址或网站名称。请通过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
组件。标签指示了xs
、sm
和md
属性使用的值。以下是结果的外观:
它是如何工作的...
你可以传递给Grid
组件的每个断点属性都对应于屏幕宽度,如下所示:
-
xs >= 0px
-
sm >= 600px
-
md >= 960px
-
lg >= 1280px
-
xl >= 1920px
之前显示的屏幕像素宽度为 725,这意味着Grid
组件使用了sm
断点。传递给此属性的值是6
。这个值可以是1
到12
之间的数字,它定义了将有多少个项目放入网格中。这可能会令人困惑,因此将这些数字视为百分比可能会有所帮助。例如,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
替换为1
至12
之间的一个值。auto
的值已经足够好,你可以开始做其他事情,而不必过多担心布局,但它对你的生产应用来说远非完美。至少通过这样设置auto
,你所有的Grid
组件和断点属性都已经就位。你只需要调整数字,直到一切看起来都很好。
参见
-
Grid
API 文档:material-ui.com/api/grid/
-
Grid
演示:material-ui.com/layout/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
演示:material-ui.com/layout/grid/
-
Grid
API 文档:material-ui.com/api/grid/
抽象容器和项目
在你的应用中,你有许多屏幕,每个屏幕都有许多Grid
组件,用于创建复杂的布局。试图阅读包含大量<Grid>
元素的源代码可能会令人望而却步。特别是当Grid
组件既用于容器又用于项目时。
如何做到这一点...
Grid
组件的container
或item
属性决定了元素的角色。你可以创建两个使用这些属性的组件,并在有大量布局组件时创建一个更容易阅读的元素名称:
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;
这就是结果布局的外观:
它是如何工作的...
让我们更仔细地看看Container
和Item
组件:
const Container = props => <Grid container {...props} />;
const Item = props => <Grid item {...props} />;
Container
组件渲染一个具有container
属性设置为 true 的Grid
组件,而Item
组件做同样的事情,只是将item
属性设置为 true。每个组件将任何额外的属性传递给Grid
组件,例如xs
和sm
断点。
当你有大量的Grid
容器和组成你的布局的项目时,能够看到<Container>
和<Item>
元素之间的区别会使你的代码更容易阅读。与此相对的是,在所有地方都有<Grid>
元素。
更多内容...
如果你发现你在布局中反复使用相同的断点,你可以在你的高阶Item
组件中包含它们。让我们重写示例,以便除了Item
属性外,还包括xs
、sm
和md
属性:
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 XML(JSX)标记中多余语法的优秀工具。
任何需要覆盖你在Item
组件中设置的任何断点属性的时候,你只需要将属性传递给Item
。例如,如果你有一个特定的案例需要md
为6
,你只需写<Item md={6}>
。这之所以有效,是因为在Item
组件中,{...props}
是在默认值之后传递的,这意味着它们覆盖了具有相同名称的任何属性。
参见
-
Grid
演示:material-ui.com/layout/grid/
-
Grid
API 文档:material-ui.com/api/grid/
固定列布局
当你使用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
演示:material-ui.com/layout/grid/
-
Grid
API 文档:material-ui.com/api/grid/
改变列方向
当使用固定列数进行布局时,内容从左到右流动。第一个网格项位于第一列,第二个项位于第二列,依此类推。有时你可能需要更好地控制哪些网格项进入哪些列。
如何实现...
假设你有一个四列布局,但你希望第一和第二项位于第一列,第三和第四项位于第二列,依此类推。这涉及到使用嵌套的 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
容器。这个 container
的 direction
属性值为 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,且不显示最后一列的结果:
参见
-
Grid
示例:material-ui.com/layout/grid/
-
Grid
API 文档:material-ui.com/api/grid/
-
Hidden
API 文档:material-ui.com/api/hidden/
第二章: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
组件隐藏:
参见
-
CSS 定位指南:
developer.mozilla.org/en-US/docs/Learn/CSS/CSS_layout/Positioning
-
AppBar
演示:material-ui.com/demos/app-bar/
-
AppBar
API 文档:material-ui.com/api/app-bar/
-
Toolbar
API 文档:material-ui.com/api/toolbar/
滚动时隐藏
如果你屏幕上有大量需要用户垂直滚动的内 容,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>
参见
-
Fade
API 文档:material-ui.com/api/fade/
-
Grow
API 文档:material-ui.com/api/grow/
-
Slide
API 文档:material-ui.com/api/slide/
工具栏抽象
如果你需要在多个地方渲染工具栏,工具栏代码可能会变得冗长。为了解决这个问题,你可以创建自己的 Toolbar
组件,该组件封装了工具栏的内容模式,使得在多个地方渲染 AppBar
组件更容易。
如何实现...
假设你的应用在多个屏幕上渲染 AppBar
组件。每个 AppBar
组件也会将 Menu
和 title
渲染到左侧,以及 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()
方法使用了两个组件:MenuItems
和 RightButton
。除了 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
时,可以向这些属性传递自定义值。这里使用的默认值可能是用于主页的值。
您实际上不必为这些属性提供默认值。但如果您提供了,比如对于主页,那么其他开发者查看您的代码并理解其工作方式会更容易。
更多内容...
让我们尝试设置一些自定义菜单项和右侧按钮,分别使用 MenuItems
和 RightButton
属性:
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>
)
);
这是渲染后的工具栏看起来:
这是带有自定义菜单选项的菜单看起来:
您传递给 MenuItems
和 RightButton
的值是返回 React 元素的函数。这些函数实际上是您即时创建的功能组件。
参见
-
AppBar
演示:material-ui.com/demos/app-bar/
-
AppBar
API 文档:material-ui.com/api/app-bar/
-
Toolbar
API 文档:material-ui.com/api/toolbar/
带有导航
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()
函数参数传递的每个屏幕的独特部分。
参见
-
React Router 文档:
reacttraining.com/react-router/
-
AppBar
示例:material-ui.com/demos/app-bar/
-
AppBar
API 文档:material-ui.com/api/app-bar/
第三章:抽屉 - 导航控制的位置
在本章中,你将学习以下食谱:
-
抽屉类型
-
抽屉项目状态
-
抽屉项目导航
-
抽屉部分
-
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
示例:material-ui.com/demos/drawers/
-
Drawer
API 文档:material-ui.com/api/drawer/
抽屉项目状态
在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
组件,可以在你应用中显示列表的任何地方使用。它将在任何使用位置一致地处理Icon
、disabled
和显示逻辑。
参见
-
Drawer
示例:material-ui.com/demos/drawers/
-
Drawer
API 文档:material-ui.com/api/drawer/
抽屉项目导航
如果你的 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-dom
的Switch
组件。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
属性用于确定应用于ListItemIcon
和ListItemText
组件的类。如果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 属性。这意味着如果主题发生变化,抽屉中的活动链接将被相应地样式化。
参见
-
React Router 文档:
reacttraining.com/react-router/
-
Drawer
演示:material-ui.com/demos/drawers/
-
Drawer
API 文档:material-ui.com/api/drawer/
抽屉部分
当你在你的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 部分并展开内存部分:
参见
-
Drawer
演示:material-ui.com/demos/drawers/
-
Drawer
API 文档:material-ui.com/api/drawer/
AppBar
交互
在应用中每个页面的顶部放置一个切换Drawer
组件可见性的按钮是一个常见的地方。此外,通过在抽屉中选择项目,AppBar
组件的标题需要改变以反映此选择。Drawer
和AppBar
组件通常需要相互交互。
如何实现...
假设你有一个包含一些项目的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
中的标题。
更多...
如果您想要一个可以通过应用栏中的相同菜单按钮切换的持久抽屉的灵活性怎么办?让我们给传递给MyDrawer
的AppBarInteraction
组件添加一个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>
现在当您在持久状态下的抽屉中点击项目时,抽屉将保持打开。唯一关闭它的方法是在应用栏标题旁边的菜单按钮上点击。
参见
-
Drawer
演示:material-ui.com/demos/drawers/
-
AppBar
演示:material-ui.com/demos/app-bar/
-
Drawer
API 文档:material-ui.com/api/drawer/
-
AppBar
API 文档:material-ui.com/api/app-bar/
第四章:标签 - 将内容分组到标签部分
在本章中,你将学习以下内容:
-
AppBar
集成 -
标签对齐
-
根据状态渲染标签
-
抽象标签内容
-
使用路由进行标签导航
简介
Tabs
Material-UI 组件用于在屏幕上组织内容。标签以水平方式组织,并且应该让用户感觉自然。当你的屏幕上有大量内容可以分成不同的类别部分时,你可以随时使用标签。
AppBar
集成
AppBar
组件可以与 Tabs
组件一起使用。你可以这样做,以便标签按钮在 App Bar 中渲染。这为你的标签按钮提供了一个容器——默认情况下,它们周围没有任何东西。
如何实现...
假设你有一个包含三个 Tab
按钮的 Tabs
组件。你不必将标签渲染得看起来像是漂浮在屏幕上,而是可以将它们包裹在一个 AppBar
组件中,以赋予它们一个封装的外观和感觉。以下是代码示例:
import React, { useState } from 'react';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
tabContent: {
padding: theme.spacing.unit * 2
}
});
function AppBarIntegration({ classes }) {
const [value, setValue] = useState(0);
const onChange = (e, value) => {
setValue(value);
};
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs value={value} onChange={onChange}>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
</AppBar>
{value === 0 && (
<Typography component="div" className={classes.tabContent}>
Item One
</Typography>
)}
{value === 1 && (
<Typography component="div" className={classes.tabContent}>
Item Two
</Typography>
)}
{value === 2 && (
<Typography component="div" className={classes.tabContent}>
Item Three
</Typography>
)}
</div>
);
}
export default withStyles(styles)(AppBarIntegration);
当屏幕首次加载时,你会看到以下内容:
当你点击其中一个标签按钮时,选中的标签会改变,以及标签下方的相关内容也会更新。例如,点击“项目三”标签会得到以下结果:
工作原理...
Tabs
和 Tab
组件在 AppBar
组件内部渲染。通常,AppBar
有一个 Toolbar
组件作为其子组件,但 Tab
也可以工作:
<AppBar position="static">
<Tabs value={value} onChange={onChange}>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
</AppBar>
你的组件有一个 value
状态,用于跟踪选中的标签。onChange()
处理器用于更新这个状态;它被设置为当前选中标签的索引。然后,你可以使用 value
状态来确定在 AppBar
组件下方渲染哪个内容:
{value === 0 && (
<Typography
component="div"
className={classes.tabContent}
>
Item One
</Typography>
)}
{value === 1 && (
<Typography
component="div"
className={classes.tabContent}
>
Item Two
</Typography>
)}
{value === 2 && (
<Typography
component="div"
className={classes.tabContent}
>
Item Three
</Typography>
)}
如果第一个标签被选中,那么值是 0
,并且会渲染“项目一”文本。对于其他两个标签,遵循相同的逻辑。
更多内容...
如果你想要标签,但不想在文本下方渲染指示器,你可以将其设置为与 AppBar
组件相同的颜色。这可以通过 indicatorColor
属性完成,如下所示:
<Tabs
value={value}
onChange={this.onChange}
indicatorColor="primary"
>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
通过将 indicatorColor
值设置为 primary
,指示器现在应该与 AppBar
组件颜色相同:
相关内容
-
Tabs
API 文档:material-ui.com/api/tabs/
-
Tabs
示例:material-ui.com/demos/tabs/
标签对齐
Tabs
组件有两个属性可以帮助你对齐标签按钮。centered
属性使标签居中,而 fullWidth
属性使标签展开。
如何实现...
假设你使用了以下代码创建了三个基本标签:
import React, { useState } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
}
});
function TabAlignment({ classes }) {
const [value, setValue] = useState(0);
const onChange = (e, value) => {
setValue(value);
};
return (
<div className={classes.root}>
<Tabs value={value} onChange={onChange}>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
</div>
);
}
export default withStyles(styles)(TabAlignment);
当屏幕首次加载时,你应该看到以下内容:
默认情况下,制表符对齐到左侧。您可以通过设置centered
属性来使制表符居中,如下所示:
<Tabs value={value} onChange={onChange} centered>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
下面是居中制表符的外观:
当您的制表符居中时,所有空余空间都位于制表符的左右两侧。另一种选择是将variant
属性设置为fullWidth
:
<Tabs value={value} onChange={onChange} variant="fullWidth">
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
下面是全宽制表符的外观:
制表符居中,但它们均匀分布以覆盖屏幕宽度。
它是如何工作的...
centered
属性只是指定Tabs
组件上的justifyContent
样式的一种方便方式。每当有属性可以以特定方式样式化 Material-UI 组件时,您应该使用它而不是应用自己的样式。库的将来版本可能包括依赖于您将错过的属性的修复。
使用属性来设置组件样式的另一个原因是,Material-UI 可能会根据其他属性的设置方式表现出不同的行为。例如,在使用Tabs
组件时,当scrollable
属性设置为true
时,您不能设置centered
属性;Material-UI 会检查这一点并处理它。
variant
属性的fullWidth
值实际上传递给了Tab
组件,这将根据此值更改它使用的样式。结果是制表符在容器元素内的均匀分布。
您可以同时设置centered
和variant
属性。然而,如果variant
的值为fullWidth
,则centered
不是必需的。尽管如此,同时使用两者是无害的。
还有更多...
制表符的居中布局在小屏幕上表现良好,而全宽布局在大屏幕上看起来不错。您可以使用 Material-UI 的实用工具来了解断点更改。然后,您可以使用这些信息来更改制表符的对齐方式。
下面是此示例的修改版本:
import React, { useState } from 'react';
import compose from 'recompose/compose';
import { withStyles } from '@material-ui/core/styles';
import withWidth from '@material-ui/core/withWidth';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
}
});
function TabAlignment({ classes, width }) {
const [value, setValue] = useState(0);
const onChange = (e, value) => {
setValue(value);
};
return (
<div className={classes.root}>
<Tabs
value={value}
onChange={onChange}
variant={['xs', 'sm'].includes(width) ? null : 'fullWidth'}
centered
>
<Tab label="Item One" />
<Tab label="Item Two" />
<Tab label="Item Three" />
</Tabs>
</div>
);
}
export default compose(
withWidth(),
withStyles(styles)
)(TabAlignment);
现在当您调整屏幕大小时,网格的对齐属性会根据断点更改而改变。让我们从底部开始分析这些更改,首先是variant
属性值:
variant={['xs', 'sm'].includes(width) ? null : 'fullWidth'}
如果width
属性不是xs
或sm
断点,则值将是fullWidth
。换句话说,如果屏幕更大,则值将是fullWidth
。
接下来,您需要以某种方式将宽度属性传递给您的组件。您可以使用 Material-UI 的withWidth()
实用工具。它的工作方式类似于withStyles()
,它会返回一个新的组件,并为其分配新的属性。withWidth()
返回的组件将在断点更改时更新其width
属性。例如,如果用户将屏幕大小从sm
调整为md
,这将触发宽度更改,fullWidth
将从false
变为true
。
要使用 withWidth()
组件——以及 withStyles()
组件——你可以使用 recompose
中的 compose()
函数。当你应用多个高阶函数来装饰你的组件时,这个函数会使你的代码更易于阅读:
export default compose(
withWidth(),
withStyles(styles)
)(TabAlignment);
如果你真的不想使用 recompose
,你可以调用 withWidth(withStyles(styles))(TabAlignment)
,但作为一个一般规则,我喜欢在涉及多个高阶函数时使用它。
参见
-
Tabs
示例:material-ui.com/demos/tabs/
-
Tabs
API 文档:material-ui.com/api/tabs/
-
React 组件组合工具:
github.com/acdlite/recompose/
根据状态渲染标签页
在你的 React 应用程序中,标签页可能是由数据驱动的。如果是这样,你可以在组件的状态中设置标签数据,以便它们最初渲染并在有任何更改时更新。
如何做到这一点...
假设你有一些数据,这些数据决定了你的应用程序中要渲染的标签页。你可以在组件的状态中设置这些数据,并使用它来渲染 Tab
组件,以及当标签选择被做出时的标签内容。以下是代码:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
tabContent: {
padding: theme.spacing(2)
}
}));
export default function RenderingTabsBasedOnState() {
const classes = useStyles();
const [tabs, setTabs] = useState([
{
active: true,
label: 'Item One',
content: 'Item One Content'
},
{
active: false,
label: 'Item Two',
content: 'Item Two Content'
},
{
active: false,
label: 'Item Three',
content: 'Item Three Content'
}
]);
const onChange = (e, value) => {
setTabs(
tabs
.map(tab => ({ ...tab, active: false }))
.map((tab, index) => ({
...tab,
active: index === value
}))
);
};
const active = tabs.findIndex(tab => tab.active);
const content = tabs[active].content;
return (
<div className={classes.root}>
<Tabs value={active} onChange={onChange}>
{tabs
.map(tab => (
<Tab
key={tab.label}
label={tab.label}
/>
))}
</Tabs>
<Typography component="div" className={classes.tabContent}>
{content}
</Typography>
</div>
);
}
当你首次加载屏幕时,你会看到以下内容:
如果你点击“项目二”标签,你会看到以下内容:
它是如何工作的...
让我们从查看驱动渲染的 tabs
的组件状态开始:
const [tabs, setTabs] = useState([
{
active: true,
label: 'Item One',
content: 'Item One Content'
},
{
active: false,
label: 'Item Two',
content: 'Item Two Content'
},
{
active: false,
label: 'Item Three',
content: 'Item Three Content'
}
]);
tabs
状态是一个数组,数组中的每个对象代表一个要渲染的标签。active
布尔属性确定哪个标签是“活动”的。label
属性是实际渲染为标签按钮的内容,当点击标签时,内容在标签下方渲染。
接下来,让我们看看用于渲染 tabs
和内容的标记:
<Tabs value={active} onChange={onChange}>
{tabs.map(tab => <Tab label={tab.label} />)}
</Tabs>
<Typography component="div" className={classes.tabContent}>
{content}
</Typography>
而不是手动渲染 Tab
组件,你现在正在遍历 tabs
状态来渲染每个标签。对于选定的内容,你现在只需要渲染一个引用 content
的 Typography
组件。
让我们看看两个 active
和 content
值,如下所示:
const active = tabs.findIndex(tab => tab.active);
const content = tabs[active].content;
active
常量是活动标签的索引。这个值传递给 Tabs
组件的 value
属性。它也被 content
值——活动标签的内容——使用。这两个常量简化了你的组件需要渲染的标记。
还有更多...
现在你用 state
控制你的标签页后,你可以控制更多渲染的标签的方面。例如,你可以在每个标签中添加 disabled
和 hidden
状态。你还可以在标签状态中放置一个 icon
属性来渲染。以下是 tabs
状态的新版本:
const [tabs, setTabs] = useState([
{
active: true,
label: 'Home',
content: 'Home Content',
icon: <HomeIcon />
},
{
active: false,
label: 'Settings',
content: 'Settings Content',
icon: <SettingsIcon />
},
{
active: false,
disabled: true,
label: 'Search',
content: 'Search Content',
icon: <SearchIcon />
},
{
active: false,
hidden: true,
label: 'Add',
content: 'AddContent',
icon: <AddIcon />
}
]);
现在,您有了渲染disabled
标签的能力,这些标签不能被点击——例如,SEARCH 标签的情况。您还可以通过将hidden
设置为true
来完全隐藏标签——例如,添加标签的情况。每个标签现在都有一个图标。让我们看看加载屏幕时的样子:
每个标签的图标都按预期渲染,即使是标记为disabled
的 SEARCH 标签。没有添加标签,因为它被标记为hidden
。让我们看看为了适应这些新的状态值而对Tabs
标记所做的更改:
<Tabs value={active} onChange={onChange}>
{tabs
.filter(tab => !tab.hidden)
.map(tab => (
<Tab
key={tab.label}
disabled={tab.disabled}
icon={tab.icon}
label={tab.label}
/>
))}
</Tabs>
Tab
的disabled
和icon
属性直接从组件状态中的标签传递。添加了filter()
调用以删除标记为隐藏的标签。
参见
-
Tabs
API 文档:material-ui.com/api/tabs/
-
Tabs
演示:material-ui.com/demos/tabs/
抽象标签内容
如果您的应用程序在多个位置使用标签,您可以创建抽象来简化涉及渲染标签和标签内容的标记。为什么不将所有内容都包含在内,使其更容易阅读,而不是在标签组件外部定义标签内容?
如何做到...
假设您的应用程序在应用程序的多个位置使用标签,并且您想简化创建标签和标签内容的标记。在您使用标签的地方,您只想能够渲染内容,而无需担心处理活动标签的状态。以下是一些代码,创建了两个新的组件,这些组件简化了渲染标签内容所需的JavaScript XML(JSX):
import React, { Fragment, Children, useState } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
tabContent: {
padding: theme.spacing(2)
}
});
function TabContainer({ children }) {
const [value, setValue] = useState(0);
const onChange = (e, value) => {
setValue(value);
};
return (
<Fragment>
<Tabs value={value} onChange={onChange}>
{Children.map(children, child => (
<Tab label={child.props.label} />
))}
</Tabs>
{Children.map(children, (child, index) =>
index === value ? child : null
)}
</Fragment>
);
}
const TabContent = withStyles(styles)(({ classes, children }) => (
<Typography component="div" className={classes.tabContent}>
{children}
</Typography>
));
const AbstractingTabContent = withStyles(styles)(({ classes }) => (
<div className={classes.root}>
<TabContainer>
<TabContent label="Item One">Item One Content</TabContent>
<TabContent label="Item Two">Item Two Content</TabContent>
<TabContent label="Item Three">Item Three Content</TabContent>
</TabContainer>
</div>
));
export default AbstractingTabContent;
当您加载屏幕时,您将看到三个标签被渲染,默认选中第一个标签。第一个标签的内容也是可见的。以下截图显示了它的样子:
它是如何工作的...
让我们先看看在这个以下示例中用于渲染标签的标记:
<TabContainer>
<TabContent label="Item One">Item One Content</TabContent>
<TabContent label="Item Two">Item Two Content</TabContent>
<TabContent label="Item Three">Item Three Content</TabContent>
</TabContainer>
与直接使用Tab
和Tabs
组件相比,这种标记更加简洁。这种方法还处理了渲染选中标签的内容。这种方法将所有内容都包含在内。
接下来,让我们看看TabContainer
组件:
function TabContainer({ children }) {
const [value, setValue] = useState(0);
const onChange = (e, value) => {
setValue(value);
};
return (
<Fragment>
<Tabs value={value} onChange={onChange}>
{Children.map(children, child => (
<Tab label={child.props.label} />
))}
</Tabs>
{Children.map(children, (child, index) =>
index === value ? child : null
)}
</Fragment>
);
}
TabContainer
组件处理选中标签的状态,并在选择不同的标签时更改状态。该组件渲染一个Fragment
组件,以便它可以在Tabs
组件之后放置选中的标签内容。它使用Children.map()
来渲染单个Tab
组件。标签的标签来自子组件的label
属性。在这个例子中,有三个子组件(TabContent
)。下一次调用Children.map()
将渲染选中标签的内容。这是基于value
状态——如果子组件索引匹配,则是active
内容。否则,它被映射到null
,不进行渲染。
最后,让我们来看看TabContent
组件:
const TabContent = withStyles(styles)(({ classes, children }) => (
<Typography component="div" className={classes.tabContent}>
{children}
</Typography>
));
TabContent
负责对Typography
组件进行样式设置,并在其中渲染子文本。尽管将label
属性传递给了TabContent
,但它实际上并没有使用它;相反,它在渲染标签时由TabContainer
使用。
还有更多...
你可以向TabsContainer
组件添加一个value
属性,这样你就可以设置要激活的任何标签。例如,你可能希望在屏幕首次加载时,第二个标签是active
而不是第一个标签。为此,你必须为value
添加一个默认属性值,如果value
状态尚未设置,则调用setValue()
,并从初始状态中删除value
。
function TabContainer({ children, value: valueProp }) {
const [value, setValue] = useState();
const onChange = (e, value) => {
setValue(value);
};
if (value === undefined) {
setValue(valueProp);
}
return (
<Fragment>
<Tabs value={value} onChange={onChange}>
{Children.map(children, child => (
<Tab label={child.props.label} />
))}
</Tabs>
{Children.map(children, (child, index) =>
index === value ? child : null
)}
</Fragment>
);
}
TabContainer.defaultProps = {
value: 0
};
默认属性是必要的,因为value
状态默认是未定义的。如果value
状态是未定义的,则调用setValue()
方法。如果是,则可以通过传递value
属性值来设置它。
现在,你可以将这个属性传递给你的组件以更改初始激活的标签页:
<TabContainer value={1}>
<TabContent label="Item One">Item One Content</TabContent>
<TabContent label="Item Two">Item Two Content</TabContent>
<TabContent label="Item Three">Item Three Content</TabContent>
</TabContainer>
值属性设置为1
。它是一个基于零的索引,这意味着第二个标签将默认激活:
当用户开始点击其他标签时,value
状态按预期更新——只有初始激活的标签受此更改影响。
参见
-
Tabs
API 文档:material-ui.com/api/tabs/
-
Tabs
演示:material-ui.com/demos/tab/
-
与 React 子组件一起工作:
reactjs.org/docs/react-api.html#reactchildren
基于路由的标签导航
你可以在路由解决方案(如react-router
)中根据路由来设置你的标签内容。为此,你必须将你的标签按钮转换为链接,并且需要在Tabs
组件下方有Route
组件来渲染当前 URL。
如何做到这一点...
假设你的应用有三个 URL,你想要使用标签作为导航机制在路由之间导航。第一步是将Tab
按钮转换为链接。第二步是让Route
组件根据点击的标签渲染相应的标签内容。以下是代码:
import React, { useState } from 'react';
import { Route, Link } from 'react-router-dom';
import { withStyles } from '@material-ui/core/styles';
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
root: {
flexGrow: 1,
backgroundColor: theme.palette.background.paper
},
tabContent: {
padding: theme.spacing(2)
}
});
function TabNavigationWithRoutes({ classes }) {
const [value, setValue] = useState(0);
const onChange = (e, value) => {
setValue(value);
};
return (
<div className={classes.root}>
<AppBar position="static">
<Tabs value={value} onChange={onChange}>
<Tab label="Item One" component={Link} to="/" />
<Tab label="Item Two" component={Link} to="/page2" />
<Tab label="Item Three" component={Link} to="/page3" />
</Tabs>
</AppBar>
<Route
exact
path="/"
render={() => (
<Typography component="div" className={classes.tabContent}>
Item One
</Typography>
)}
/>
<Route
exact
path="/page2"
render={() => (
<Typography component="div" className={classes.tabContent}>
Item Two
</Typography>
)}
/>
<Route
exact
path="/page3"
render={() => (
<Typography component="div" className={classes.tabContent}>
Item Three
</Typography>
)}
/>
</div>
);
}
export default withStyles(styles)(TabNavigationWithRoutes);
当你加载屏幕时,第一个标签应该被选中,并且第一个标签内容应该被渲染:
如果你点击“项目二”标签页,你将被带到/page2
URL。这会导致活动的Route
组件更改标签内容,并且更改的标签状态会改变选中的标签:
它是如何工作的...
你的组件的状态部分与使用Tabs
组件的任何其他组件相同。onChange
事件改变value
状态,该状态作为属性传递给Tabs
以标记选中的标签。
让我们更仔细地看看Tab
组件:
<Tabs value={value} onChange={onChange}>
<Tab label="Item One" component={Link} to="/" />
<Tab label="Item Two" component={Link} to="/page2" />
<Tab label="Item Three" component={Link} to="/page3" />
</Tabs>
与更标准的实现相比,这个实现的一个主要区别是,你使用Link
作为组件属性值。来自react-router-dom
的Link
组件被用来将标签按钮变成路由器将处理的链接。to
属性实际上传递给了Link
,这样它就知道链接应该将用户带到哪里。
在Tabs
组件下方是渲染标签内容的路由,基于用户点击的标签。让我们看看这些 Routes 中的一个:
<Route
exact
path="/"
render={() => (
<Typography
component="div"
className={classes.tabContent}
>
Item One
</Typography>
)}
/>
在标签下方渲染的内容基于当前 URL,而不是组件的value
状态。value
状态仅用于控制选中标签的状态。
还有更多...
由于活动标签依赖于活动路由,你可以完全移除任何与标签相关的状态。首先,你创建一个TabContainer
组件来渲染Tabs
组件:
const TabContainer = ({ value }) => (
<AppBar position="static">
<Tabs value={value}>
<Tab label="Item One" component={Link} to="/" />
<Tab label="Item Two" component={Link} to="/page2" />
<Tab label="Item Three" component={Link} to="/page3" />
</Tabs>
</AppBar>
);
不同于向Tabs
组件提供onChange()
处理程序,value
属性是从TabContainer
传递的。现在,你可以在每个Route
组件中render
此组件,传递适当的value
属性:
const TabNavigationWithRoutes = withStyles(styles)(({ classes }) => (
<div className={classes.root}>
<Route
exact
path="/"
render={() => (
<Fragment>
<TabContainer value={0} />
<Typography component="div" className={classes.tabContent}>
Item One
</Typography>
</Fragment>
)}
/>
<Route
exact
path="/page2"
render={() => (
<Fragment>
<TabContainer value={1} />
<Typography component="div" className={classes.tabContent}>
Item Two
</Typography>
</Fragment>
)}
/>
<Route
exact
path="/page3"
render={() => (
<Fragment>
<TabContainer value={2} />
<Typography component="div" className={classes.tabContent}>
Item Three
</Typography>
</Fragment>
)}
/>
</div>
));
export default TabNavigationWithRoutes;
不要再混淆组件状态与当前Route
以及它们之间的交互了。所有这些都由路由处理。
参见
-
Tabs
API 文档:material-ui.com/api/tabs/
-
Tabs
示例:material-ui.com/demos/tabs/
-
React Router 文档:
reacttraining.com/react-router/
第五章:展开面板 - 将内容分组到面板部分
在本章中,你将学习以下内容:
-
状态化展开面板
-
格式化面板标题
-
可滚动面板内容
-
懒加载面板内容
简介
展开面板是内容的容器。通常,你的 Material-UI 应用程序中的屏幕被分成几个部分,以便用户可以心理上组织他们正在查看的信息。ExpansionPanel
组件是创建这些部分的一种方式。你甚至可以将展开面板与其他组织组件(如标签页)结合使用,为用户提供一致的组织布局。
状态化展开面板
你可以使用组件状态来控制展开面板的各个方面。例如,每个面板可以表示为数组中的一个对象,其中每个对象都有面板标题和面板内容属性。你还可以控制其他方面,如可见性和禁用面板。
如何实现...
假设你的组件有一个用于渲染展开面板的状态。面板本身是数组中的对象。以下是实现这一功能的代码:
import React, { useState, Fragment } from 'react';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
export default function StatefulExpansionPanels() {
const [panels] = useState([
{
title: 'First Panel Title',
content: 'First panel content...'
},
{
title: 'Second Panel Title',
content: 'Second panel content...'
},
{
title: 'Third Panel Title',
content: 'Third panel content...'
},
{
title: 'Fourth Panel Title',
content: 'Fourth panel content...'
}
]);
return (
<Fragment>
{panels
.filter(panel => !panel.hidden)
.map((panel, index) => (
<ExpansionPanel
key={index}
disabled={panel.disabled}
>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{panel.title}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>{panel.content}</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
))}
</Fragment>
);
}
当你加载屏幕时,你会看到以下内容:
当前两个面板展开时,它们看起来是这样的:
第三面板无法展开,因为它已被禁用。
它是如何工作的...
状态定义了展开面板的所有内容。这包括面板 标题
、当面板展开时显示的 内容
、disabled
属性以及面板是否 隐藏
:
const [panels] = useState([
{
title: 'First Panel Title',
content: 'First panel content...'
},
{
title: 'Second Panel Title',
content: 'Second panel content...'
},
{
title: 'Third Panel Title',
content: 'Third panel content...'
},
{
title: 'Fourth Panel Title',
content: 'Fourth panel content...'
}
]);
disabled
属性将面板标记为禁用。这意味着用户可以看到面板 标题
,但不能展开它。它也被视觉上标记为不可展开。hidden
属性确保面板根本不会被渲染。这在你不希望用户知道它的情况下非常有用。
接下来,让我们看看根据组件状态渲染每个面板的代码:
{panels
.filter(panel => !panel.hidden)
.map((panel, index) => (
}>
{panel.title}
{panel.content}
))}
filter()
调用从数组中移除了设置 hidden
属性为 true 的面板。
使用 hidden
属性隐藏面板的替代方法是完全从数组中移除它们。这完全取决于个人喜好——切换属性值与从数组中添加和移除值。
每个面板都使用 map()
函数映射到 ExpansionPanel
组件。展开面板使用 ExpansionPanelSummary
组件作为标题,内容则放入 ExpansionPanelDetails
组件中。
还有更多...
你也可以使用状态来控制面板是否展开。例如,你可以使用 ExpansionPanel
组件创建一个 手风琴 小部件——始终有一个面板是打开的,打开另一个面板会关闭任何打开的内容。
第一步是添加一个 expanded
状态来确定在任何给定时间哪个面板是打开的:
const [expanded, setExpanded] = useState(0);
const [panels] = useState([
{
title: 'First Panel Title',
content: 'First panel content...'
},
{
title: 'Second Panel Title',
content: 'Second panel content...'
},
{
title: 'Third Panel Title',
content: 'Third panel content...'
},
{
title: 'Fourth Panel Title',
content: 'Fourth panel content...'
}
]);
expanded
状态默认为 0
,这意味着第一个面板默认展开。随着展开的面板发生变化,expanded
状态也会改变以反映展开面板的索引。接下来,你将为 ExpansionPanel
组件添加一个 onChange
处理器:
const onChange = expanded => () => {
setExpanded(expanded);
};
这是一个高阶函数——它接受你想要展开的面板的索引,并返回一个在给定面板被点击时设置展开状态的函数。最后,你可以将新的 expanded
状态和 onChange
处理器添加到 ExpansionPanel
组件中:
<ExpansionPanel
key={index}
expanded={index === expanded}
disabled={panel.disabled}
onChange={onChange(index)}
>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{panel.title}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>{panel.content}</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
expanded
属性基于当前面板的索引,等于组件的展开状态。如果它们相等,则面板已展开。onChange
处理器也被分配给 ExpansionPanel
,当面板被点击时,它会更改展开状态。
参见
-
ExpansionPanel
演示:material-ui.com/demos/expansion-panels/
-
ExpansionPanel
API 文档:material-ui.com/api/expansion-panel/
-
ExpansionPanelSummary
API 文档:material-ui.com/api/expansion-panel-summary/
-
ExpansionPanelDetails
API 文档:material-ui.com/api/expansion-panel-details/
格式化面板标题
ExpansionPanel
组件中的标题可以格式化。通常,Typography
组件用于渲染面板标题内的文本。这意味着你可以使用 Typography
的属性来自定义你的面板标题的外观。
如何做到这一点...
假设你想要你的 ExpansionPanel
标题中的文本相对于每个面板内容部分的文本更加突出。你可以在 ExpansionPanelSummary
组件中更改 Typography
组件的 variant
属性。以下是实现此功能的代码:
import React, { Fragment } from 'react';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
const FormattingPanelHeaders = () => (
<Fragment>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1">Devices</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>Devices content...</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1">Networks</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>Networks content...</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography variant="subtitle1">Storage</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>Storage content...</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
</Fragment>
);
export default FormattingPanelHeaders;
这是屏幕加载时面板的外观:
这是面板展开时的外观:
它是如何工作的...
要使标题文本相对于 ExpansionPanelDetails
组件中的文本更加突出,你只需更改用于标题的 Typography
组件的 variant
属性。在这种情况下,你正在使用 subtitle1
变体,但这里还有许多其他变体可供选择。
还有更多...
除了格式化标题文本外,你还可以添加其他组件,例如图标。让我们修改示例以包含每个面板标题的图标。首先,你需要导入所需的图标:
import DevicesIcon from 'material-ui/icons/Devices';
import NetworkWifiIcon from 'material-ui/icons/NetworkWifi';
import StorageIcon from '@material-ui/icons/Storage';
然后,你将在面板标题中添加一个新的图标样式,该样式在图标和文本之间添加空间:
const styles = theme => ({
icon: {
marginRight: theme.spacing(1)
}
});
最后,这里提供了包含您已导入的图标在适当的面板标题中的标记:
<Fragment>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<DevicesIcon className={classes.icon} />
<Typography variant="subtitle1">Devices</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>Devices content...</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<NetworkWifiIcon className={classes.icon} />
<Typography variant="subtitle1">Networks</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>Networks content...</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<StorageIcon className={classes.icon} />
<Typography variant="subtitle1">Storage</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>Storage content...</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
</Fragment>
图标在 ExpansionPanelSummary
组件中位于 Typography
组件之前。现在面板看起来是这样的:
当它们展开时,它们看起来是这样的:
通过结合图标和排版,你可以让你的展开面板的标题更加突出,使你的内容更容易导航。
参见
-
ExpansionPanel
示例:material-ui.com/demos/expansion-panels/
-
ExpansionPanel
API 文档:material-ui.com/api/expansion-panel/
-
ExpansionPanelSummary
API 文档:material-ui.com/api/expansion-panel-summary/
-
ExpansionPanelDetails
API 文档:material-ui.com/api/expansion-panel-details/
可滚动面板内容
当展开时,ExpansionPanel
组件的高度会改变,以便所有内容都能在屏幕上显示。在你面板中有大量内容的情况下,这并不理想,因为面板标题对用户不可见。你不必滚动整个页面,可以使得面板内的内容可滚动。
如何做到这一点...
假设你有三个面板,每个面板都有几段文字。而不是让每个面板调整其高度以适应内容,你可以使面板具有固定高度并可滚动。以下是代码:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
const styles = theme => ({
panelDetails: {
flexDirection: 'column',
height: 150,
overflow: 'auto'
}
});
const IpsumContent = () => (
<Fragment>
<Typography paragraph>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer
ultricies nibh ut ipsum placerat, eget egestas leo imperdiet.
Etiam consectetur mollis ultrices. Fusce eu eros a dui maximus
rutrum. Aenean at dolor eu nunc ultricies placerat. Sed finibus
porta sapien eget euismod. Donec eget tortor non turpis
hendrerit euismod. Phasellus at commodo augue. Maecenas
scelerisque augue at mattis pharetra. Aenean fermentum sed neque
id feugiat.
</Typography>
<Typography paragraph>
Aliquam erat volutpat. Donec sit amet venenatis leo. Nullam
tincidunt diam in nisi pretium, sit amet tincidunt nisi aliquet.
Proin quis justo consectetur, congue nisi nec, pharetra erat. Ut
volutpat pulvinar neque vitae vestibulum. Phasellus nisl risus,
dapibus at sapien in, aliquam tempus tellus. Integer accumsan
tortor id dolor lacinia, et pulvinar est porttitor. Mauris a est
vitae arcu iaculis dictum. Sed posuere suscipit ultricies.
Vivamus a lacus in dui vehicula tincidunt.
</Typography>
<Typography paragraph>
In ut velit laoreet, blandit nisi id, tempus mi. Mauris interdum
in turpis vel tempor. Vivamus tincidunt turpis vitae porta
dignissim. Quisque condimentum augue arcu, quis tincidunt erat
luctus sit amet. Sed quis ligula malesuada, sollicitudin nisl
nec, molestie tellus. Donec commodo consequat gravida. Mauris in
rhoncus tellus, eget posuere risus. Pellentesque eget lectus
lorem. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Integer condimentum, sapien varius vulputate lobortis, urna elit
vestibulum ligula, sit amet interdum lectus augue ac eros.
Vestibulum lorem ante, tincidunt eget faucibus id, placerat non
est. Vivamus pretium consectetur nunc at imperdiet. Nullam eu
elit dui. In imperdiet magna ac dui aliquam gravida. Aenean
ipsum ex, fermentum eu pretium quis, posuere et velit.
</Typography>
</Fragment>
);
const ScrollablePanelContent = withStyles(styles)(({ classes }) => (
<Fragment>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>First</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.panelDetails}>
<IpsumContent />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Second</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.panelDetails}>
<IpsumContent />
</ExpansionPanelDetails>
</ExpansionPanel>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Third</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.panelDetails}>
<IpsumContent />
</ExpansionPanelDetails>
</ExpansionPanel>
</Fragment>
));
export default ScrollablePanelContent;
为了简洁起见,Typography
组件中的段落内容已被截断——你可以在本书的 GitHub 仓库中查看完整文本。
当第一个面板展开时,它看起来是这样的:
如果你将鼠标指针移至展开面板的内容上,你现在可以滚动内容到段落的底部,在面板内。以下是内容已滚动到底部时的样子:
它是如何工作的...
IpsumContent
组件只是一个方便的组件,它包含段落内容,这样你就不必在每一个面板中重复它。让我们先看看这个例子中使用的样式:
const styles = theme => ({
panelDetails: {
flexDirection: 'column',
height: 150,
overflow: 'auto'
}
});
面板内容使用弹性盒模型样式来布局其内容。默认情况下,它根据行方向流动,所以如果你想内容从上到下流动,你必须将 flexDirection
样式设置为 column
。接下来,你可以为你的面板内容设置一个固定高度——在这个例子中,它是 150px
。最后,将 overflow
样式设置为 auto
将启用面板内容的垂直滚动。
然后,你可以将 panelDetails
类应用到每个 ExpansionPanelContent
组件上:
<ExpansionPanelDetails className={classes.panelDetails}>
<IpsumContent />
</ExpansionPanelDetails>
参见
-
ExpansionPanel
示例:material-ui.com/demos/expansion-panels/
-
ExpansionPanel
API 文档:material-ui.com/api/expansion-panel/
-
ExpansionPanelSummary
API 文档:material-ui.com/api/expansion-panel-summary/
-
ExpansionPanelDetails
API 文档:material-ui.com/api/expansion-panel-details/
懒加载面板内容
如果你正在渲染默认全部折叠的展开面板,你不需要预先填充ExpansionPanelDetails
组件。相反,你可以等待用户展开面板——然后你可以进行任何必要的 API 调用以渲染内容。
如何做到这一点...
假设你有一个根据索引值获取内容的 API 函数。例如,如果第一个面板被展开,index
值将是0
。你需要能够在面板展开时调用这个函数,提供相应的index
值。下面是代码的样子:
import React, { useState, Fragment } from 'react';
import ExpansionPanel from '@material-ui/core/ExpansionPanel';
import ExpansionPanelSummary from '@material-ui/core/ExpansionPanelSummary';
import ExpansionPanelDetails from '@material-ui/core/ExpansionPanelDetails';
import Typography from '@material-ui/core/Typography';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
const fetchPanelContent = index =>
new Promise(resolve =>
setTimeout(
() =>
resolve(
[
'First panel content...',
'Second panel content...',
'Third panel content...',
'Fourth panel content...'
][index]
),
1000
)
);
export default function LazyLoadingPanelContent() {
const [panels, setPanels] = useState([
{ title: 'First Panel Title' },
{ title: 'Second Panel Title' },
{ title: 'Third Panel Title' },
{ title: 'Fourth Panel Title' }
]);
const onChange = index => e => {
if (!panels[index].content) {
fetchPanelContent(index).then(content => {
const newPanels = [...panels];
newPanels[index] = { ...newPanels[index], content };
setPanels(newPanels);
});
}
};
return (
<Fragment>
{panels.map((panel, index) => (
<ExpansionPanel key={index} onChange={onChange(index)}>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{panel.title}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails>
<Typography>{panel.content}</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
))}
</Fragment>
);
}
当屏幕首次加载时,四个面板看起来是这样的:
尝试展开第一个面板。它立即展开,但大约一秒钟内没有任何内容。然后内容出现:
它是如何工作的...
让我们从fetchPanelContent()
API 函数开始:
const fetchPanelContent = index =>
new Promise(resolve =>
setTimeout(
() =>
resolve(
[
'First panel content...',
'Second panel content...',
'Third panel content...',
'Fourth panel content...'
][index]
),
1000
)
);
由于这只是一个模拟,它直接返回一个 promise。它使用setTimeout()
来模拟延迟,类似于使用真实 API 时的体验。promise 解析为从数组中查找的字符串值,基于index
参数。
接下来,让我们看看当ExpansionPanel
展开时被调用的onChange
处理函数:
const onChange = index => (e) => {
if (!panels[index].content) {
fetchPanelContent(index).then(content => {
const newPanels = [...panels];
newPanels[index] = { ...newPanels[index], content };
setPanels(newPanels);
});
}
};
首先,这个函数检查展开的面板在其状态中是否有任何content
。如果没有,你知道你必须通过调用fetchPanelContent()
来获取它。当返回的 promise 解析时,你可以调用setPanels()
来更新面板数组并在适当的索引处设置内容。
你组件的其余部分只是根据面板数组渲染ExpansionPanel
组件,使用content
状态作为面板内容。当内容更新时,它会在渲染的内容中反映出来。
还有更多...
你可以通过这个示例进行一些改进。首先,你可以在内容加载时在面板内显示进度指示器,这样用户就会知道有事情在进行中。第二个改进可以在面板展开和折叠时进行——这应该避免。
让我们从进度指示器开始。为此,你需要一个实用组件以及为ExpansionPanelDetails
组件设置的样式:
const MaybeProgress = ({ loading }) =>
loading ? <LinearProgress /> : null;
const useStyles = makeStyles(theme => ({
panelDetails: { flexDirection: 'column' }
}));
MaybeProgress
组件接受一个 loading
属性,当该属性为 true 时,将生成一个 LinearProgress
组件。否则,不会渲染任何内容。flexDirection
样式设置为 column
;否则,LinearProgress
组件将不会显示。现在让我们修改 LazyLoadingPanelContent
生成的标记,使其使用这两个新增功能:
return (
<Fragment>
{panels.map((panel, index) => (
<ExpansionPanel key={index} onChange={onChange(index)}>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />}>
<Typography>{panel.title}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.panelDetails}>
<MaybeProgress loading={!panel.content} />
<Typography>{panel.content}</Typography>
</ExpansionPanelDetails>
</ExpansionPanel>
))}
</Fragment>
);
panelDetails
类现在被 ExpansionPanelDetails
组件使用。此组件的第一个子元素现在是 MaybeProgress
。加载属性为 true,直到 API 调用填充给定面板的内容状态。这意味着进度指示器将在内容加载前可见。
在内容加载之前,展开的第一个面板看起来是这样的:
一旦加载完成,内容将替换进度指示器进行渲染。最后,确保在面板折叠时不会发起加载内容的 API 调用。这需要对 onChange()
处理器进行调整:
const onChange = index => (e, expanded) => {
if (!panels[index].content && expanded) {
fetchPanelContent(index).then(content => {
const newPanels = [...panels];
newPanels[index] = { ...newPanels[index], content };
setPanels(newPanels);
});
}
};
传递给此函数的第二个参数 expanded
告诉你面板是否正在展开。如果此值为 false,则表示面板已折叠,不应发起 API 调用。此条件已添加以查找已为该面板加载的内容。
参见
-
ExpansionPanel
示例:material-ui.com/demos/expansion-panels/
-
ExpansionPanel
API 文档:material-ui.com/api/expansion-panel/
-
ExpansionPanelSummary
API 文档:material-ui.com/api/expansion-panel-summary/
-
ExpansionPanelDetails
API 文档:material-ui.com/api/expansion-panel-details/
第六章:列表 - 显示简单集合数据
在本章中,你将涵盖以下食谱:
-
使用状态来渲染列表项
-
列表图标
-
列表头像和文本
-
列表部分
-
嵌套列表
-
列表控制
-
滚动列表
简介
Material-UI 中的List
组件用于渲染数据集合。列表就像表格,但更简单。如果你需要显示用户数组,例如,你可以将它们渲染成列表,只显示最相关的数据,而不是在表格格式中显示多个属性。Material-UI 列表是通用的,并提供了很多灵活性。
使用状态来渲染列表项
用于渲染List
组件的数据源通常来自你的组件状态。一个集合——通常是一个对象数组——被映射到ListItem
组件。当这个数组中的对象发生变化时,Material-UI 列表项会在屏幕上相应地改变。
如何做到这一点...
假设你有一个包含三个对象的数组,你需要将其显示在你的屏幕上的列表中。你可以将这个数组添加到你的组件状态中,然后将每个数组项映射到一个ListItem
组件。以下是代码:
import React, { useState } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
export default function UsingStatetoRenderListItems() {
const [items, setItems] = useState([
{ name: 'First Item', timestamp: new Date() },
{ name: 'Second Item', timestamp: new Date() },
{ name: 'Third Item', timestamp: new Date() }
]);
return (
<List>
{items.map((item, index) => (
<ListItem key={index} button dense>
<ListItemText
primary={item.name}
secondary={item.timestamp.toLocaleString()}
/>
</ListItem>
))}
</List>
);
}
当你首次加载屏幕时,你会看到以下内容:
它是如何工作的...
让我们从查看items
状态开始:
const [items, setItems] = useState([
{ name: 'First Item', timestamp: new Date() },
{ name: 'Second Item', timestamp: new Date() },
{ name: 'Third Item', timestamp: new Date() }
]);
name
属性是每个列表项的primary
文本,而timestamp
属性是每个列表项的secondary
文本。接下来,让我们看看将这个状态转换为渲染列表项的List
标记:
<List>
{items.map((item, index) => (
<ListItem key={index} button dense>
<ListItemText
primary={item.name}
secondary={item.timestamp.toLocaleString()}
/>
</ListItem>
))}
</List>
ListItem
组件有两个布尔属性传递给它——button
和dense
。button
属性使列表项表现得像一个按钮。例如,如果你将鼠标指针移到列表中的某个项上,你会看到应用于它的悬停样式。dense
属性从列表项中移除了额外的填充。如果没有这个属性,列表在屏幕上会占用更多的空间。
ListItemText
组件使用primary
和secondary
属性分别渲染name
和timestamp
属性。primary
文本旨在相对于显示在项中的secondary
信息脱颖而出——在这个例子中,是timestamp
。
还有更多...
这个例子原本可以使用属性而不是状态,因为列表中的项目从未改变。让我们修改它,以便用户可以从列表中选择项目。下面是新的List
标记的样式:
<List>
{items.map((item, index) => (
<ListItem
key={index}
button
dense
selected={item.selected}
onClick={onClick(index)}
>
<ListItemText
primary={item.name}
secondary={item.timestamp.toLocaleString()}
primaryTypographyProps={{
color: item.selected ? 'primary' : undefined
}}
/>
</ListItem>
))}
</List>
传递给ListItem
组件的selected
属性将在true
时应用选中样式到该项。这个值来自item.selected
状态,默认情况下每个项都是false
(没有选中)。接下来,ListItem
组件有一个onClick
处理程序。
ListItemText
组件也会根据项目的选择状态应用样式。在幕后,项目文本是通过Typography
组件渲染的。你可以使用primaryTypographyProps
属性将属性传递给Typography
组件。在这种情况下,当项目被选中时,你正在将文本的color
更改为primary
。
让我们看看以下是如何处理onClick()
处理器的:
const onClick = index => () => {
const item = items[index];
const newItems = [...items];
newItems[index] = { ...item, selected: !item.selected };
setItems(newItems);
};
这是一个高阶函数,它根据index
参数返回一个事件处理函数。它切换给定索引处的项目选择状态。
onClick
属性不是ListItem
属性,而是button
属性。由于你已经将button
属性设置为true
,ListItem
使用button
属性并将其传递给你的onClick
属性。
当选择第一个项目时,列表看起来是这样的:
背景颜色的变化是由ListItem
的selected
属性引起的。文本颜色的变化是由ListItemText
的primaryTypographyProps
属性引起的。
参见
-
List
演示:material-ui.com/demos/lists/
-
Typography
API 文档:material-ui.com/api/typography/
列表图标
ListItem
组件对图标有第一级支持。通过在每个列表项中渲染图标,你可以清楚地让用户知道列表中显示的对象类型。
如何实现...
假设你有一个用户对象的数组,你想要在List
中渲染这些对象。你可以为列表中的每个项目渲染一个用户图标,以便清楚地表明列表中的每个项目是什么。下面是这个功能的代码实现:
import React, { useState } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
export default function ListIcons() {
const [items, setItems] = useState([
{ name: 'First User' },
{ name: 'Second User' },
{ name: 'Third User' }
]);
return (
<List>
{items.map((item, index) => (
<ListItem key={index} button>
<ListItemIcon>
<AccountCircleIcon />
</ListItemIcon>
<ListItemText primary={item.name} />
</ListItem>
))}
</List>
);
}
当你加载屏幕时,列表应该看起来是这样的:
它是如何工作的...
ListItemIcon
组件可以用作ListItem
组件的子组件。在上一个示例中,它位于文本之前,因此最终位于项目文本的左侧:
<ListItem button key={index}>
<ListItemIcon>
<AccountCircleIcon />
</ListItemIcon>
<ListItemText primary={item.name} />
</ListItem>
你也可以将图标放置在文本之后:
<ListItem button key={index}>
<ListItemText primary={item.name} />
<ListItemIcon>
<AccountCircleIcon />
</ListItemIcon>
</ListItem>
这就是它的样子:
更多内容...
你可以通过将selected
属性设置为true
来标记ListItem
组件为选中状态。你还可以更改图标,以更好地提供项目已选中的视觉指示。以下是更新后的代码:
import React, { useState } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import CheckCircleOutlineIcon from '@material-ui/icons/CheckCircleOutline';
const MaybeSelectedIcon = ({ selected, Icon }) =>
selected ? <CheckCircleOutlineIcon /> : <Icon />;
export default function ListIcons() {
const [items, setItems] = useState([
{ name: 'First User' },
{ name: 'Second User' },
{ name: 'Third User' }
]);
const onClick = index => () => {
const item = items[index];
const newItems = [...items];
newItems[index] = { ...item, selected: !item.selected };
setItems(newItems);
};
return (
<List>
{items.map((item, index) => (
<ListItem
key={index}
button
selected={item.selected}
onClick={onClick(index)}
>
<ListItemText primary={item.name} />
<ListItemIcon>
<MaybeSelectedIcon
selected={item.selected}
Icon={AccountCircleIcon}
/>
</ListItemIcon>
</ListItem>
))}
</List>
);
}
这是选择第一个用户时列表的样子:
选择项目的图标变为圆形勾选标记。让我们分析一下引入的更改,从MaybeSelectedIcon
组件开始:
const MaybeSelectedIcon = ({ selected, Icon }) =>
selected ? <CheckCircleOutlineIcon /> : <Icon />;
此组件将渲染CheckCircleOutlineIcon
或作为属性传入的Icon
组件。这取决于selected
属性。接下来,让我们看看这个组件如何在ListItemIcon
内部使用:
<ListItemIcon>
<MaybeSelectedIcon
selected={item.selected}
Icon={AccountCircleIcon}
/>
</ListItemIcon>
当点击列表项时,该项目的selected
状态会被切换。然后,selected
状态会被传递给MaybeSelectedIcon
。AccountCircleIcon
组件是在列表项未被选中时渲染的图标,因为它被传递给了Icon
属性。
另请参阅
-
List
演示:material-ui.com/demos/lists/
-
ListItemIcon
API 文档:material-ui.com/api/list-item-icon/
列表头像和文本
如果你的列表项有primary
和secondary
文本,单独使用图标可能不如围绕图标的头像在视觉上吸引人。它更好地填充了列表项内的空间。
如何实现...
假设你的应用可以显示四种消息类别。要访问某个类别,用户点击列表中的一个项。为了帮助用户理解类别,你会使用图标。为了让图标在列表项的primary
和secondary
文本中脱颖而出,你会用Avatar
组件包裹它。以下是代码:
import React, { useState } from 'react';
import clsx from 'clsx';
import Avatar from '@material-ui/core/Avatar';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import MarkunreadIcon from '@material-ui/icons/Markunread';
import PriorityHighIcon from '@material-ui/icons/PriorityHigh';
import LowPriorityIcon from '@material-ui/icons/LowPriority';
import DeleteIcon from '@material-ui/icons/Delete';
export default function ListAvatarsAndText({ classes }) {
const [items] = useState([
{
name: 'Unread',
updated: '2 minutes ago',
Icon: MarkunreadIcon,
notifications: 1
},
{
name: 'High Priority',
updated: '30 minutes ago',
Icon: PriorityHighIcon
},
{
name: 'Low Priority',
updated: '3 hours ago',
Icon: LowPriorityIcon
},
{ name: 'Junk', updated: '6 days ago', Icon: DeleteIcon }
]);
return (
<List>
{items.map(({ Icon, ...item }, index) => (
<ListItem button>
<ListItemIcon>
<Avatar>
<Icon />
</Avatar>
</ListItemIcon>
<ListItemText
primary={item.name}
secondary={item.updated}
/>
</ListItem>
))}
</List>
);
}
这就是列表渲染后的样子:
围绕图标的圆圈是Avatar
组件,它帮助图标脱颖而出。以下是这个没有头像的列表的样子:
内容和图标都是相同的,但由于列表项文本的高度,图标周围有大量的多余空间。Avatar
组件帮助填充这个空间,同时吸引人们对图标的注意。
它是如何工作的...
Avatar
组件用于圆形形状的图标。圆圈的颜色来自主题调色板——使用的灰色阴影取决于主题是亮色还是暗色。图标本身作为子元素传递:
<ListItemIcon>
<Avatar>
<Icon />
</Avatar>
</ListItemIcon>
还有更多...
如果你使用Avatar
在你的列表项中的图标,你可以改变Avatar
的颜色,并且可以应用徽章来指示未确认的操作。让我们修改示例,使得items
状态中的每个项都可以有一个notifications
属性;即表示该类别未读消息数量的数字。如果这个数字大于 0,你可以改变Avatar
的颜色,并在徽章中显示通知数量。以下是结果的样子:
列表中的第一个项有一个使用主要theme
颜色和显示通知数量的徽章的Avatar
。其余的项没有通知,所以Avatar
颜色使用默认值,徽章不显示。
让我们看看这是如何实现的,从样式开始:
const styles = theme => ({
activeAvatar: {
backgroundColor: theme.palette.primary[theme.palette.type]
}
});
当notifications
状态为一个大于 0 的数字时,activeAvatar
样式会被应用到Avatar
组件上。它会根据主题类型(浅色或深色)查找主要的theme
颜色。接下来,让我们看看items
数组中第一个项目的状态:
{
name: 'Unread',
updated: '2 minutes ago',
Icon: MarkunreadIcon,
notifications: 1
}
因为notifications
的值是1
,所以头像的颜色会改变,徽章会显示。最后,让我们看看如何使用Badge
和Avatar
组件在组件标记中组合所有这些内容:
<Badge
color={item.notifications ? 'secondary' : undefined}
badgeContent={
item.notifications ? item.notifications : null
}
>
<Avatar
className={clsx({
[classes.activeAvatar]: item.notifications
})}
>
<Icon />
</Avatar>
</Badge>
Badge
的color
属性基于项目notifications
状态是否大于 0。如果是,则使用主要颜色。如果不是,则将undefined
传递给Badge
。在这种情况下,这是必要的,这样当没有通知时,就不会显示空的徽章圆圈。
将undefined
作为属性值相当于根本未设置该属性。
接下来,根据项目的notifications
状态设置badgeContent
属性。如果它不大于 0,那么你不想设置任何值。最后,设置Avatar
组件的颜色时,如果项目的notifications
状态大于 0,则使用clsx()
应用activeAvatar
类。
参见
-
Badge
演示:material-ui.com/demos/badges/
-
Avatar
演示:material-ui.com/demos/avatars/
-
List
演示:material-ui.com/demos/lists/
列表部分
当你的列表中包含的不仅仅是几个项目时,你可能想要考虑将项目组织到部分中。为此,你将你的列表分成几个更小的列表,这些列表彼此堆叠,中间用分隔符隔开。
如何做...
假设你有几个可以分成三个部分的项目。你可以使用三个List
组件将你的项目分组到各自的区域,并使用Divider
组件来向用户视觉上指示部分边界。以下是代码的样子:
import React, { Fragment } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Divider from '@material-ui/core/Divider';
const ListSections = () => (
<Fragment>
<List>
<ListItem>
<ListItemText primary="First" />
</ListItem>
<ListItem>
<ListItemText primary="Second" />
</ListItem>
</List>
<Divider />
<List>
<ListItem>
<ListItemText primary="Third" />
</ListItem>
<ListItem>
<ListItemText primary="Fourth" />
</ListItem>
</List>
<Divider />
<List>
<ListItem>
<ListItemText primary="Fifth" />
</ListItem>
<ListItem>
<ListItemText primary="Sixth" />
</ListItem>
</List>
</Fragment>
));
export default ListSections;
这就是渲染后的列表看起来像什么:
它是如何工作的...
每个部分都是一个独立的List
组件,有自己的ListItem
组件。Divider
组件用于分隔列表。例如,第一个部分看起来是这样的:
<List>
<ListItem>
<ListItemText primary="First" />
</ListItem>
<ListItem>
<ListItemText primary="Second" />
</ListItem>
</List>
更多内容...
你可以使用Typography
来标记你的部分,而不是使用Divider
组件分隔你的列表部分。这可以帮助你的用户理解每个部分中的项目:
<Fragment>
<Typography variant="title">First Section</Typography>
<List>
<ListItem>
<ListItemText primary="First" />
</ListItem>
<ListItem>
<ListItemText primary="Second" />
</ListItem>
</List>
<Typography variant="title">Second Section</Typography>
<List>
<ListItem>
<ListItemText primary="Third" />
</ListItem>
<ListItem>
<ListItemText primary="Fourth" />
</ListItem>
</List>
<Typography variant="title">Third Section</Typography>
<List>
<ListItem>
<ListItemText primary="Fifth" />
</ListItem>
<ListItem>
<ListItemText primary="Sixth" />
</ListItem>
</List>
</Fragment>
现在列表看起来是这样的:
参见
-
List
演示:material-ui.com/demos/lists/
-
Typography
API 文档:material-ui.com/api/typography/
嵌套列表
列表可以嵌套。当你需要渲染大量项目时,这很有用。你不必一次性显示所有内容,而只需显示这些项目类别。然后用户可以点击这些类别来显示项目。
如何实现...
假设你有两个项目类别。当用户点击一个类别时,该类别中的项目应该显示出来。以下是通过使用 List
组件来实现此功能的代码:
import React, { useState, Fragment } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import Collapse from '@material-ui/core/Collapse';
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import InboxIcon from '@material-ui/icons/Inbox';
import MailIcon from '@material-ui/icons/Mail';
import ContactsIcon from '@material-ui/icons/Contacts';
import ContactMailIcon from '@material-ui/icons/ContactMail';
const ExpandIcon = ({ expanded }) =>
expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />;
export default function NestedLists() {
const [items, setItems] = useState([
{
name: 'Messages',
Icon: InboxIcon,
expanded: false,
children: [
{ name: 'First Message', Icon: MailIcon },
{ name: 'Second Message', Icon: MailIcon }
]
},
{
name: 'Contacts',
Icon: ContactsIcon,
expanded: false,
children: [
{ name: 'First Contact', Icon: ContactMailIcon },
{ name: 'Second Contact', Icon: ContactMailIcon }
]
}
]);
const onClick = index => () => {
const newItems = [...items];
const item = items[index];
newItems[index] = { ...item, expanded: !item.expanded };
setItems(newItems);
};
return (
<List>
{items.map(({ Icon, ...item }, index) => (
<Fragment key={index}>
<ListItem button onClick={onClick(index)}>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
<ExpandIcon expanded={item.expanded} />
</ListItem>
<Collapse in={item.expanded}>
{item.children.map(child => (
<ListItem key={child.name} button dense>
<ListItemIcon>
<child.Icon />
</ListItemIcon>
<ListItemText primary={child.name} />
</ListItem>
))}
</Collapse>
</Fragment>
))}
</List>
);
}
当你首次加载屏幕时,你会看到以下内容:
如果你点击每个这些类别,你会看到以下内容:
它是如何工作的...
当你点击一个类别时,向下箭头图标会变成向上箭头。在类别下方,显示属于该类别的列表项。让我们分析一下代码中发生的事情,从组件状态开始:
const [items, setItems] = useState([
{
name: 'Messages',
Icon: InboxIcon,
expanded: false,
children: [
{ name: 'First Message', Icon: MailIcon },
{ name: 'Second Message', Icon: MailIcon }
]
},
{
name: 'Contacts',
Icon: ContactsIcon,
expanded: false,
children: [
{ name: 'First Contact', Icon: ContactMailIcon },
{ name: 'Second Contact', Icon: ContactMailIcon }
]
}
]);
项目数组中的每个对象代表一个列表类别。在这种情况下,类别是 Messages
和 Contacts
。Icon
属性是用于渲染类别的图标组件。expanded
属性确定展开箭头图标的状态,以及类别中的项目是否应该显示。
children
数组包含属于该类别的项目。它们具有 name
和 Icon
属性,就像类别项目一样,因为它们都是使用 ListItem
组件渲染的。
接下来,让我们看看用于渲染每个类别及其子项的标记:
<Fragment key={index}>
<ListItem button onClick={onClick(index)}>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
<ExpandIcon expanded={item.expanded} />
</ListItem>
<Collapse in={item.expanded}>
{item.children.map(child => (
<ListItem key={child.name} button dense>
<ListItemIcon>
<child.Icon />
</ListItemIcon>
<ListItemText primary={child.name} />
</ListItem>
))}
</Collapse>
</Fragment>
类别 ListItem
组件有一个 onClick
处理器,用于切换类别的 expanded
状态。接下来,使用 Collapse
组件根据 expanded
的值来控制类别子项的可见性。
还有更多...
你可以通过区分子项的外观来改善嵌套列表的外观。目前,类别项目和子项之间的唯一区别是类别项目有展开和折叠箭头。
通常,列表项会缩进以表示它们是另一个项目层次结构的一部分。让我们创建一个样式,允许你缩进子项:
const useStyles = makeStyles(theme => ({
subItem: { paddingLeft: theme.spacing(3) }
}));
paddingLeft
样式属性会将列表项中的所有内容向右移动。现在,让我们将这个类应用到 subItem
上,同时使项目比类别项目更小:
<ListItem
key={child.name}
className={classes.subItem}
button
dense
>
<ListItemIcon>
<child.Icon />
</ListItemIcon>
<ListItemText primary={child.name} />
</ListItem>
通过将 dense
和 className
属性添加到 ListItem
中,用户应该能够更容易地区分类别及其子项:
相关内容
-
List
示例:material-ui.com/demos/lists/
-
Collapse
API 文档:material-ui.com/api/collapse/
列表控件
列表项可以是可点击的,导致状态改变,或者跟随链接,或者完全是其他操作。这是项目的主要操作。你可以在列表上拥有次要操作,称为控件。这些是你可能会执行的一些常见操作,具体取决于项目的类型。
如何做到这一点...
假设你有一个设备列表。当你点击列表项时,它可能会带你到设备的详细信息页面。每个设备都有可以开启或关闭的蓝牙连接。这是在项目中渲染的很好的候选次要操作。以下是实现此功能的代码:
import React, { useState } from 'react';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
import IconButton from '@material-ui/core/IconButton';
import BluetoothIcon from '@material-ui/icons/Bluetooth';
import BluetoothDisabledIcon from '@material-ui/icons/BluetoothDisabled';
import DevicesIcon from '@material-ui/icons/Devices';
const MaybeBluetoothIcon = ({ bluetooth }) =>
bluetooth ? <BluetoothIcon /> : <BluetoothDisabledIcon />;
export default function ListControls() {
const [items, setItems] = useState([
{
name: 'Device 1',
bluetooth: true,
Icon: DevicesIcon
},
{
name: 'Device 2',
bluetooth: true,
Icon: DevicesIcon
},
{
name: 'Device 3',
bluetooth: true,
Icon: DevicesIcon
}
]);
const onBluetoothClick = index => () => {
const newItems = [...items];
const item = items[index];
newItems[index] = { ...item, bluetooth: !item.bluetooth };
setItems(newItems);
};
return (
<List>
{items.map(({ Icon, ...item }, index) => (
<ListItem key={index} button>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
<ListItemSecondaryAction>
<IconButton
onClick={onBluetoothClick(index, 'bluetooth')}
>
<MaybeBluetoothIcon bluetooth={item.bluetooth} />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
))}
</List>
);
}
这是屏幕首次加载时的样子:
你可以通过点击图标按钮来切换其中一个项目的蓝牙状态。以下是切换第一个项目蓝牙状态后的样子:
蓝牙图标已更改以表示 disabled
状态。再次点击图标将启用蓝牙。
它是如何工作的...
让我们看看用于渲染每个列表项的标记:
<ListItem key={index} button>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
<ListItemSecondaryAction>
<IconButton
onClick={onBluetoothClick(index, 'bluetooth')}
>
<MaybeBluetoothIcon bluetooth={item.bluetooth} />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
ListItemSecondaryAction
组件被用作列表项中任何控件的外壳。在这个例子中,IconButton
被用作控件。它使用 MaybeBluetoothIcon
组件显示根据项目状态的不同图标。onBluetoothClick()
函数用于返回项目的处理函数。让我们看看这个函数:
const onBluetoothClick = index => () => {
const newItems = [...items];
const item = items[index];
newItems[index] = { ...item, bluetooth: !item.bluetooth };
setItems(newItems);
};
在 items
数组中查找设备项。然后,切换蓝牙状态,并返回新的 items
数组以设置为新状态。这导致列表项控件中的图标更新。
还有更多...
你可以在列表项中拥有多个控件。例如,假设除了切换设备的蓝牙状态外,用户还可能执行切换设备的 power
状态的常见操作。当设备关闭电源时,列表项和蓝牙控件应该显示。
避免在列表项中拥有太多的控件作为次要操作。这样做会减少用户轻松访问一两个常见操作的便利性。
让我们从为你的组件状态中的每个项目添加一个新的 power
状态开始:
const [items, setItems] = useState([
{
name: 'Device 1',
bluetooth: true,
power: true,
Icon: DevicesIcon
},
{
name: 'Device 2',
bluetooth: true,
power: true,
Icon: DevicesIcon
},
{
name: 'Device 3',
bluetooth: true,
power: true,
Icon: DevicesIcon
}
]);
接下来,让我们创建一个可以处理更新蓝牙和项目 power
状态的切换点击处理程序:
const onToggleClick = (index, prop) => () => {
const newItems = [...items];
const item = items[index];
newItems[index] = { ...item, [prop]: !item[prop] };
setItems(newItems);
};
这与 onBluetoothClick()
处理程序非常相似。现在,它接受一个额外的 prop
参数。这用于告诉函数更新哪个属性 - bluetooth
或 power
。最后,让我们看看更新的 ListItem
标记:
<ListItem key={index} disabled={!item.power} button>
<ListItemIcon>
<Icon />
</ListItemIcon>
<ListItemText primary={item.name} />
<ListItemSecondaryAction>
<IconButton
onClick={onToggleClick(index, 'bluetooth')}
disabled={!item.power}
>
<MaybeBluetoothIcon bluetooth={item.bluetooth} />
</IconButton>
<IconButton onClick={onToggleClick(index, 'power')}>
<PowerSettingsNewIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
变更可以总结如下:
-
ListItem
的disabled
属性取决于项目的power
状态。 -
另有一个用于切换项目
power
状态的IconButton
控件。 -
onToggleClick()
函数被两个控件用来切换项目的状态。
这是首次加载时的屏幕外观:
当你点击电源图标时,列表项和蓝牙按钮将变为禁用状态。以下是第一个项目关闭电源时的样子:
参考也
-
ListItemSecondaryAction
API 文档:material-ui.com/api/list-item-secondary-action/
-
IconButton
API 文档:material-ui.com/api/icon-button/
滚动列表
当你的列表中包含有限数量的项目时,你可以安全地遍历项目数据并渲染ListItem
组件。当你有超过 1,000 个项目的列表时,这就会成为一个问题。你可以快速渲染这些项目,但这么多项目在文档对象模型(DOM)中会消耗大量的浏览器资源,并可能导致用户面临不可预测的性能挑战。解决方案是使用react-virtualized
虚拟化你的 Material-UI 列表。
如何做到这一点...
假设你有一个包含 1,000 个项目的列表。你希望在具有固定高度的列表中渲染这些项目。为了为用户提供可预测的性能特性,你只想渲染用户在滚动列表时实际可见的项目。以下是代码:
import React, { useState } from 'react';
import { List as VirtualList, AutoSizer } from 'react-virtualized';
import { makeStyles } from '@material-ui/styles';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import Paper from '@material-ui/core/Paper';
const useStyles = makeStyles(theme => ({
list: {
height: 300
},
paper: {
margin: theme.spacing(3)
}
}));
function* genItems() {
for (let i = 1; i <= 1000; i++) {
yield `Item ${i}`;
}
}
export default function ScrollingLists() {
const classes = useStyles();
const [items] = useState([...genItems()]);
const rowRenderer = ({ index, isScrolling, key, style }) => {
const item = items[index];
return (
<ListItem button key={key} style={style}>
<ListItemText primary={isScrolling ? '...' : item} />
</ListItem>
);
};
return (
<Paper className={classes.paper}>
<List className={classes.list}>
<AutoSizer disableHeight>
{({ width }) => (
<VirtualList
width={width}
height={300}
rowHeight={50}
rowCount={items.length}
rowRenderer={rowRenderer}
/>
)}
</AutoSizer>
</List>
</Paper>
);
}
当你首次加载屏幕时,你将看到以下内容:
当你滚动浏览列表时,你将看到以下内容:
最后,这是列表底部的样子:
它是如何工作的...
首先,让我们看看items
状态是如何生成的。首先,有一个genItems()
生成器函数:
function* genItems() {
for (let i = 1; i <= 1000; i++) {
yield `Item ${i}`;
}
}
然后,使用扩展运算符将生成的items
转换为组件状态中的数组:
const [items] = useState([...genItems()]);
接下来,让我们看看rowRenderer()
函数:
const rowRenderer = ({ index, isScrolling, key, style }) => {
const item = items[index];
return (
<ListItem button key={key} style={style}>
<ListItemText primary={isScrolling ? '...' : item} />
</ListItem>
);
};
此函数返回在给定索引处应渲染的ListItem
组件。而不是手动将此组件映射到items
上,react-virtualized
中的List
组件会根据用户如何滚动列表来为你安排调用它的时间。
传递给此函数的key
和style
值是react-virtualized
正确工作所必需的。例如,style
值用于控制项目在滚动时的可见性。isScrolling
值用于在列表被积极滚动时渲染不同的数据。例如,想象一下,在列表项中除了文本标签之外,你还有一个图标,以及其他所有基于状态的控制。在滚动过程中尝试渲染这些内容是昂贵且浪费的。相反,你可以渲染一些资源消耗较少的内容,例如占位符字符串:'...'
。
最后,让我们检查用于渲染此列表的标记:
<List className={classes.list}>
<AutoSizer disableHeight>
{({ width }) => (
<VirtualList
width={width}
height={300}
rowHeight={50}
rowCount={items.length}
rowRenderer={rowRenderer}
/>
)}
</AutoSizer>
</List>
列表
组件是其他所有内容的容器。接下来,react-virtualized
中的AutoSizer
组件确定列表的宽度,这是作为VirtualList
属性所需的。
使用别名VirtualList
从react-virtualized
导入List
。这样做是为了避免与material-ui
中的List
命名冲突。如果你更喜欢,你也可以将List
从material-ui
导入为一个别名。
react-virtualized
中的List
组件还接受列表的高度、每行的高度和行数,以确定要渲染哪些行。有了这个,你永远不必担心由于列表组件包含太多项目而导致应用程序的性能问题。
参见
-
React Virtualized 文档:
bvaughn.github.io/react-virtualized/
第七章:表格 - 显示复杂集合数据
在本章中,你将学习以下主题:
-
状态化表格
-
可排序的列
-
过滤行
-
选择行
-
行操作
简介
如果你的应用程序需要显示表格数据,你可以使用 Material-UI 的Table
组件及其所有支持组件。与你在其他 React 库中可能看到或使用过的网格组件不同,Material-UI 组件是无偏见的。这意味着你必须编写自己的代码来控制表格数据。好处是,Table
组件不会妨碍你,让你能够以自己的方式实现。
状态化表格
使用Table
组件时,很少会遇到静态标记来定义表格的行数据。相反,组件状态将映射到构成你的表格数据的行。例如,你可能有一个从 API 获取数据并希望在表格中显示的组件。
如何实现...
假设你有一个从 API 端点获取数据的组件。当数据加载时,你希望在 Material-UI 的Table
组件中显示表格数据。以下是代码的样子:
import React, { useState, useEffect } from 'react';
import { makeStyles } from '@material-ui/styles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
const fetchData = () =>
new Promise(resolve => {
const items = [
{
id: 1,
name: 'First Item',
created: new Date(),
high: 2935,
low: 1924,
average: 2429.5
},
{
id: 2,
name: 'Second Item',
created: new Date(),
high: 439,
low: 231,
average: 335
},
{
id: 3,
name: 'Third Item',
created: new Date(),
high: 8239,
low: 5629,
average: 6934
},
{
id: 4,
name: 'Fourth Item',
created: new Date(),
high: 3203,
low: 3127,
average: 3165
},
{
id: 5,
name: 'Fifth Item',
created: new Date(),
high: 981,
low: 879,
average: 930
}
];
setTimeout(() => resolve(items), 1000);
});
const usePaperStyles = makeStyles(theme => ({
root: { margin: theme.spacing(2) }
}));
export default function StatefulTables() {
const classes = usePaperStyles();
const [items, setItems] = useState([]);
useEffect(() => {
fetchData().then(items => {
setItems(items);
});
}, []);
return (
<Paper className={classes.root}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Created</TableCell>
<TableCell align="right">High</TableCell>
<TableCell align="right">Low</TableCell>
<TableCell align="right">Average</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map(item => {
return (
<TableRow key={item.id}>
<TableCell component="th" scope="row">
{item.name}
</TableCell>
<TableCell>{item.created.toLocaleString()}</TableCell>
<TableCell align="right">{item.high}</TableCell>
<TableCell align="right">{item.low}</TableCell>
<TableCell align="right">{item.average}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Paper>
);
}
当你加载屏幕时,你将在一秒后看到一个填充了数据的表格:
它是如何工作的...
让我们从查看fetchData()
函数开始,该函数解析最终设置为组件状态的数据:
const fetchData = () =>
new Promise(resolve => {
const items = [
{
id: 1,
name: 'First Item',
created: new Date(),
high: 2935,
low: 1924,
average: 2429.5
},
{
id: 2,
name: 'Second Item',
created: new Date(),
high: 439,
low: 231,
average: 335
},
...
];
setTimeout(() => resolve(items), 1000);
});
此函数返回一个Promise
,在一秒后解析为一个对象数组。其想法是模拟一个使用fetch()
调用真实 API 的函数。
为了简洁起见,数组中显示的对象被截断了。
接下来,让我们看看初始组件状态以及你的组件挂载时会发生什么:
const [items, setItems] = useState([]);
useEffect(() => {
fetchData().then(items => {
setItems(items);
});
}, []);
items
状态表示要在Table
组件内渲染的表格行。当你的组件挂载时,会调用fetchData()
,当Promise
解析时,items
状态被设置。最后,让我们看看负责渲染表格行的标记:
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Created</TableCell>
<TableCell align="right">High</TableCell>
<TableCell align="right">Low</TableCell>
<TableCell align="right">Average</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items.map(item => {
return (
<TableRow key={item.id}>
<TableCell component="th" scope="row">
{item.name}
</TableCell>
<TableCell>{item.created.toLocaleString()}</TableCell>
<TableCell align="right">{item.high}</TableCell>
<TableCell align="right">{item.low}</TableCell>
<TableCell align="right">{item.average}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
Table
组件通常有两个子组件——一个TableHead
组件和一个TableBody
组件。在TableHead
内部,你会找到一个包含多个TableCell
组件的TableRow
组件。这些是表格列标题。在TableBody
内部,你会看到items
状态被映射到TableRow
和TableCell
组件。当items
状态改变时,行也会改变。你已经在实际操作中看到了这一点,因为items
状态默认为空数组。在 API 数据解析后,items
状态改变,行在屏幕上可见。
还有更多...
这个示例的一个次优方面是用户在等待表格数据加载时的体验。提前显示列标题是可以的,因为你事先知道它们是什么,用户也可能知道。需要的是某种指示器,表明实际的行数据确实正在加载。
解决此问题的一种方法是在列标题下方添加一个环形进度指示器。这应该有助于用户理解他们不仅正在等待数据加载,而且具体是等待表格行数据,多亏了进度指示器的位置。
首先,让我们介绍一个新的组件来显示CircularProgress
组件和一些新的样式:
const usePaperStyles = makeStyles(theme => ({
root: { margin: theme.spacing(2), textAlign: 'center' }
}));
const useProgressStyles = makeStyles(theme => ({
progress: { margin: theme.spacing(2) }
}));
function MaybeLoading({ loading }) {
const classes = useProgressStyles();
return loading ? (
<CircularProgress className={classes.progress} />
) : null;
}
新增了一种应用于CircularProgress
组件的progress
样式。这为进度指示器添加了margin
。textAlign
属性已被添加到root
样式,以便进度指示器在Paper
组件内水平居中。如果loading
属性为true
,则MaybeLoading
组件会渲染CircularProgress
组件。
这意味着你现在必须跟踪 API 调用的loading
状态。以下是新的状态,默认为true
:
const [loading, setLoading] = useState(true);
当 API 调用返回时,您可以设置loading
状态为false
:
useEffect(() => {
fetchData().then(items => {
setItems(items);
setLoading(false);
});
}, []);
最后,你需要在Table
组件之后渲染MaybeLoading
组件:
<Paper className={classes.root}>
<Table>
...
</Table>
<MaybeLoading loading={loading} />
</Paper>
当您的用户等待表格数据加载时,他们会看到以下内容:
参见
Table
API 文档:material-ui.com/api/table/
可排序列
Material-UI 表格具有帮助您实现可排序列的工具。如果您在应用程序中渲染Table
组件,您的用户可能会期望能够按列排序表格数据。
如何实现...
当用户点击列标题时,应该有一个视觉指示表明表格行现在按此列排序,行顺序应改变。再次点击时,列应以相反的顺序出现。以下是代码:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableSortLabel from '@material-ui/core/TableSortLabel';
import Paper from '@material-ui/core/Paper';
const comparator = (prop, desc = true) => (a, b) => {
const order = desc ? -1 : 1;
if (a[prop] < b[prop]) {
return -1 * order;
}
if (a[prop] > b[prop]) {
return 1 * order;
}
return 0 * order;
};
const useStyles = makeStyles(theme => ({
root: { margin: theme.spacing(2), textAlign: 'center' }
}));
export default function SortableColumns() {
const classes = useStyles();
const [columns, setColumns] = useState([
{ name: 'Name', active: false },
{ name: 'Created', active: false },
{ name: 'High', active: false, numeric: true },
{ name: 'Low', active: false, numeric: true },
{ name: 'Average', active: false, numeric: true }
]);
const [rows, setRows] = useState([
{
id: 1,
name: 'First Item',
created: new Date(),
high: 2935,
low: 1924,
average: 2429.5
},
{
id: 2,
name: 'Second Item',
created: new Date(),
high: 439,
low: 231,
average: 335
},
{
id: 3,
name: 'Third Item',
created: new Date(),
high: 8239,
low: 5629,
average: 6934
},
{
id: 4,
name: 'Fourth Item',
created: new Date(),
high: 3203,
low: 3127,
average: 3165
},
{
id: 5,
name: 'Fifth Item',
created: new Date(),
high: 981,
low: 879,
average: 930
}
]);
const onSortClick = index => () => {
setColumns(
columns.map((column, i) => ({
...column,
active: index === i,
order:
(index === i &&
(column.order === 'desc' ? 'asc' : 'desc')) ||
undefined
}))
);
setRows(
rows
.slice()
.sort(
comparator(
columns[index].name.toLowerCase(),
columns[index].order === 'desc'
)
)
);
};
return (
<Paper className={classes.root}>
<Table>
<TableHead>
<TableRow>
{columns.map((column, index) => (
<TableCell
key={column.name}
align={column.numeric ? 'right' : 'inherit'}
>
<TableSortLabel
active={column.active}
direction={column.order}
onClick={onSortClick(index)}
>
{column.name}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
<TableRow key={row.id}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell>{row.created.toLocaleString()}</TableCell>
<TableCell align="right">{row.high}</TableCell>
<TableCell align="right">{row.low}</TableCell>
<TableCell align="right">{row.average}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
);
}
如果你点击名称列标题,你会看到以下内容:
列会改变以指示排序顺序。如果您再次点击名称列,排序顺序将反转:
它是如何工作的...
让我们分解用于渲染此表格的代码,从用于渲染列标题的标记开始:
<TableHead>
<TableRow>
{columns.map((column, index) => (
<TableCell
key={column.name}
align={column.numeric ? 'right' : 'inherit'}
>
<TableSortLabel
active={column.active}
direction={column.order}
onClick={onSortClick(index)}
>
{column.name}
</TableSortLabel>
</TableCell>
))}
</TableRow>
</TableHead>
表格中的每一列都在columns
状态中定义。此数组映射到TableCell
组件。在每个TableCell
内部,有一个TableSortLabel
组件。当它是排序的激活列时,此组件会使列标题文本加粗,并在文本右侧添加排序箭头。TableSortLabel
接受active
、direction
和onClick
属性。active
属性基于列的active
状态,当列被点击时改变。direction
属性确定对于给定列,行是按升序还是降序排序。onClick
属性接受一个事件处理器,当列被点击时,它会进行必要的状态更改。以下是onSortClick()
处理器:
const onSortClick = index => () => {
setColumns(
columns.map((column, i) => ({
...column,
active: index === i,
order:
(index === i &&
(column.order === 'desc' ? 'asc' : 'desc')) ||
undefined
}))
);
setRows(
rows
.slice()
.sort(
comparator(
columns[index].name.toLowerCase(),
columns[index].order === 'desc'
)
)
);
};
这个函数接受一个index
参数——列索引——并返回一个新的列函数。返回的函数有两个目的:
-
为了更新列状态,以便正确标记为激活的列,并具有正确的排序方向
-
为了更新行状态,使表格行按正确顺序排列
一旦这些状态变化已经完成,active
列和表格行将反映这些变化。接下来要查看的最后一段代码是comparator()
函数。这是一个高阶函数,它接受一个列名,并返回一个新的函数,该函数可以被传递给Array.sort()
以按给定列对对象数组进行排序:
const comparator = (prop, desc = true) => (a, b) => {
const order = desc ? -1 : 1;
if (a[prop] < b[prop]) {
return -1 * order;
}
if (a[prop] > b[prop]) {
return 1 * order;
}
return 0 * order;
};
这个函数足够通用,可以用于你应用中的任何表格。在这种情况下,列名和顺序是从组件状态传递给comparator()
的。随着组件状态的变化,comparator()
中的排序行为也会发生变化。
还有更多...
如果你的数据在从 API 到达时已经按特定列排序,你会怎么办?如果是这种情况,你可能会想在用户开始与表格交互之前,指出哪些列的行是按什么方向排序的。
要这样做,你只需要更改默认的列状态。例如,假设平均值列默认按降序排序。以下是你的初始column
状态的外观:
const [columns, setColumns] = useState([
{ name: 'Name', active: false },
{ name: 'Created', active: false },
{ name: 'High', active: false, numeric: true },
{ name: 'Low', active: false, numeric: true },
{ name: 'Average', active: true, numeric: true }
]);
平均值列默认为激活状态。由于默认为升序,因此不需要指定顺序。以下是屏幕首次加载时的表格外观:
相关内容
Table
演示:material-ui.com/demos/tables/
过滤行
在有表格的地方,信息量过多是潜在的问题。这就是为什么在表格中添加搜索功能是个好主意。它允许用户在输入时从表格中删除不相关的行。
如何实现...
假设你有一个包含许多行的表格,这意味着用户将很难滚动查看整个表格。为了让他们更容易操作,你决定在你的表格中添加一个搜索功能,通过检查搜索文本是否存在于名称列中来过滤行。以下是代码:
import React, { useState, useEffect, Fragment } from 'react';
import { makeStyles } from '@material-ui/styles';
import { withStyles } from '@material-ui/core/styles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import CircularProgress from '@material-ui/core/CircularProgress';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import InputAdornment from '@material-ui/core/InputAdornment';
import FormControl from '@material-ui/core/FormControl';
import TextField from '@material-ui/core/TextField';
import SearchIcon from '@material-ui/icons/Search';
const fetchData = () =>
new Promise(resolve => {
const items = [
{
id: 1,
name: 'First Item',
created: new Date(),
high: 2935,
low: 1924,
average: 2429.5
},
{
id: 2,
name: 'Second Item',
created: new Date(),
high: 439,
low: 231,
average: 335
},
{
id: 3,
name: 'Third Item',
created: new Date(),
high: 8239,
low: 5629,
average: 6934
},
{
id: 4,
name: 'Fourth Item',
created: new Date(),
high: 3203,
low: 3127,
average: 3165
},
{
id: 5,
name: 'Fifth Item',
created: new Date(),
high: 981,
low: 879,
average: 930
}
];
setTimeout(() => resolve(items), 1000);
});
const styles = theme => ({
root: { margin: theme.spacing(2), textAlign: 'center' },
progress: { margin: theme.spacing(2) },
search: { marginLeft: theme.spacing(2) }
});
const useStyles = makeStyles(styles);
const MaybeLoading = withStyles(styles)(({ classes, loading }) =>
loading ? <CircularProgress className={classes.progress} /> : null
);
export default function FilteringRows() {
const classes = useStyles();
const [search, setSearch] = useState('');
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(items => {
setItems(items);
setLoading(false);
});
}, []);
const onSearchChange = e => {
setSearch(e.target.value);
};
return (
<Fragment>
<TextField
value={search}
onChange={onSearchChange}
className={classes.search}
id="input-search"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Paper className={classes.root}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Created</TableCell>
<TableCell align="right">High</TableCell>
<TableCell align="right">Low</TableCell>
<TableCell align="right">Average</TableCell>
</TableRow>
</TableHead>
<TableBody>
{items
.filter(item => !search || item.name.includes(search))
.map(item => {
return (
<TableRow key={item.id}>
<TableCell component="th" scope="row">
{item.name}
</TableCell>
<TableCell>
{item.created.toLocaleString()}
</TableCell>
<TableCell align="right">{item.high}</TableCell>
<TableCell align="right">{item.low}</TableCell>
<TableCell align="right">
{item.average}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<MaybeLoading loading={loading} />
</Paper>
</Fragment>
);
}
当屏幕首次加载时,表格和搜索输入字段看起来如下:
搜索输入位于表格上方。尝试输入一个过滤器字符串,例如 第四——你应该看到以下内容:
如果你从搜索输入中删除过滤器文本,表格数据中的所有行将再次渲染。
它是如何工作的...
让我们先看看FilteringRows
组件的状态:
const [search, setSearch] = useState('');
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
搜索字符串是实际过滤器,它会改变在Table
元素中渲染的行。接下来,让我们看看渲染搜索输入的TextField
组件:
<TextField
value={search}
onChange={onSearchChange}
className={classes.search}
id="input-search"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
onSearchChange()
函数负责在用户输入时维护搜索状态。你应该在过滤表格附近渲染搜索输入组件。在这个例子中,搜索输入的位置感觉像是属于表格的。
最后,让我们看看表格行是如何过滤和渲染的:
<TableBody>
{items
.filter(item => !search || item.name.includes(search))
.map(item => {
return (
<TableRow key={item.id}>
<TableCell component="th" scope="row">
{item.name}
</TableCell>
<TableCell>
{item.created.toLocaleString()}
</TableCell>
<TableCell align="right">{item.high}</TableCell>
<TableCell align="right">{item.low}</TableCell>
<TableCell align="right">
{item.average}
</TableCell>
</TableRow>
);
})}
</TableBody>
不同于直接在项目状态上调用 map()
,使用 filter()
来生成一个与搜索标准匹配的项目数组。随着 search
状态的变化,filter()
调用会重复进行。检查项目是否匹配用户输入的条件是查看项目的 name
属性是否包含搜索字符串。但首先,你必须确保用户实际上正在进行过滤。例如,如果搜索字符串为空,则应返回每个项目。项目是如何被搜索的取决于你的应用程序——如果你想的话,你可以搜索每个项目的每个属性。
参见
Table
示例:material-ui.com/demos/tables/
选择行
用户经常需要与表格中的特定行进行交互。例如,他们可能会选择一行,然后执行使用所选行数据的操作。或者,用户选择多行,这会产生与他们的选择相关的新数据。使用 Material-UI 表格,你可以使用单个 TableRow
属性来标记选中的行。
如何做到这一点...
在这个例子中,假设用户需要能够在你的表格中选择多行。随着行的选择,屏幕上的另一个部分会更新,以显示反映所选行的数据。让我们首先看看显示所选表格行数据的 Card
组件:
<Card className={classes.card}>
<CardHeader title={`(${selections()}) rows selected`} />
<CardContent>
<Grid container direction="column">
<Grid item>
<Grid container justify="space-between">
<Grid item>
<Typography>Low</Typography>
</Grid>
<Grid item>
<Typography>{selectedLow()}</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container justify="space-between">
<Grid item>
<Typography>High</Typography>
</Grid>
<Grid item>
<Typography>{selectedHigh()}</Typography>
</Grid>
</Grid>
</Grid>
<Grid item>
<Grid container justify="space-between">
<Grid item>
<Typography>Average</Typography>
</Grid>
<Grid item>
<Typography>{selectedAverage()}</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
</CardContent>
</Card>
现在让我们看看其余的组件:
import React, { useState, Fragment } from 'react';
import { makeStyles } from '@material-ui/styles';
import Typography from '@material-ui/core/Typography';
import Grid from '@material-ui/core/Grid';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import CardHeader from '@material-ui/core/CardHeader';
const useStyles = makeStyles(theme => ({
root: { margin: theme.spacing.unit * 2, textAlign: 'center' },
card: { margin: theme.spacing.unit * 2, maxWidth: 300 }
}));
export default function SelectingRows() {
const classes = useStyles();
const [columns, setColumns] = useState([
{ name: 'Name', active: false },
{ name: 'Created', active: false },
{ name: 'High', active: false, numeric: true },
{ name: 'Low', active: false, numeric: true },
{ name: 'Average', active: true, numeric: true }
]);
const [rows, setRows] = useState([
{
id: 1,
name: 'First Item',
created: new Date(),
high: 2935,
low: 1924,
average: 2429.5
},
{
id: 2,
name: 'Second Item',
created: new Date(),
high: 439,
low: 231,
average: 335
},
{
id: 3,
name: 'Third Item',
created: new Date(),
high: 8239,
low: 5629,
average: 6934
},
{
id: 4,
name: 'Fourth Item',
created: new Date(),
high: 3203,
low: 3127,
average: 3165
},
{
id: 5,
name: 'Fifth Item',
created: new Date(),
high: 981,
low: 879,
average: 930
}
]);
const onRowClick = id => () => {
const newRows = [...rows];
const index = rows.findIndex(row => row.id === id);
const row = rows[index];
newRows[index] = { ...row, selected: !row.selected };
setRows(newRows);
};
const selections = () => rows.filter(row => row.selected).length;
const selectedLow = () =>
rows
.filter(row => row.selected)
.reduce((total, row) => total + row.low, 0);
const selectedHigh = () =>
rows
.filter(row => row.selected)
.reduce((total, row) => total + row.high, 0);
const selectedAverage = () => (selectedLow() + selectedHigh()) / 2;
return (
<Fragment>
<Card className={classes.card}>
...
</Card>
<Paper className={classes.root}>
<Table>
<TableHead>
<TableRow>
{columns.map(column => (
<TableCell
key={column.name}
align={column.numeric ? 'right' : 'inherit'}
>
{column.name}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
<TableRow
key={row.id}
onClick={onRowClick(row.id)}
selected={row.selected}
>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell>{row.created.toLocaleString()}</TableCell>
<TableCell align="right">{row.high}</TableCell>
<TableCell align="right">{row.low}</TableCell>
<TableCell align="right">{row.average}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Paper>
</Fragment>
);
}
这是屏幕首次加载时的样子:
现在,你可以尝试进行一些行选择。如果你选择了第二行和第四行,你会看到以下内容:
当你点击表格行时,它会从视觉上发生变化,以便用户可以看到它已被选中。此外,请注意,Card
组件的内容也会改变,以反映选中的行。它还会告诉你已选中多少行。
它是如何工作的...
Card
组件依赖于几个辅助函数:
-
selectedLow
-
selectedHigh
-
selectedAverage
这些函数的返回值会随着表格行选择的变化而变化。让我们更仔细地看看这些值是如何计算的:
const selectedLow = () =>
rows
.filter(row => row.selected)
.reduce((total, row) => total + row.low, 0);
const selectedHigh = () =>
rows
.filter(row => row.selected)
.reduce((total, row) => total + row.high, 0);
const selectedAverage = () => (selectedLow() + selectedHigh()) / 2;
selectedLow()
和selectedHigh()
函数以相同的方式工作——它们只是分别操作low
和high
字段。filter()
调用用于确保你只处理选中的行。reduce()
调用将选中的行给定字段的值相加,并将结果作为属性值返回。selectedAverage()
函数使用selectedLow()
和selectedHigh()
函数来计算行选择的新的平均值。
接下来,让我们看看当选择行时被调用的处理程序:
const onRowClick = id => () => {
const newRows = [...rows];
const index = rows.findIndex(row => row.id === id);
const row = rows[index];
newRows[index] = { ...row, selected: !row.selected };
setRows(newRows);
};
onRowClick()
函数根据id
参数在rows
状态中找到选中的行。然后,它切换行的选中状态。结果,你刚才看到的计算属性被更新,行的外观也是如此:
<TableRow
key={row.id}
onClick={onRowClick(row.id)}
selected={row.selected}
>
TableRow
组件有一个selected
属性,它会改变行的样式以标记它为已选择。
参见
行操作
表格行通常代表你可以执行操作的实体。例如,你可能有一个包含服务器的表格,其中每一行代表一个可以开启或关闭的服务器。而不是让用户点击一个将他们从表格带到另一个页面去执行操作的链接,你可以在每一行表格中直接包含常见的操作。
如何做到...
假设你有一个表格,其中包含可以开启或关闭的服务器行,这取决于它们当前的状态。你希望将这两个操作作为每一行表格的一部分包含进来,这样用户就可以更容易地控制他们的服务器,而无需花费大量时间导航。按钮还需要根据行的状态改变它们的颜色和禁用状态。
这是完成这个功能的代码:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import Paper from '@material-ui/core/Paper';
import IconButton from '@material-ui/core/IconButton';
import PlayArrowIcon from '@material-ui/icons/PlayArrow';
import StopIcon from '@material-ui/icons/Stop';
const useStyles = makeStyles(theme => ({
root: { margin: theme.spacing(2), textAlign: 'center' },
button: {}
}));
const StartButton = ({ row, onClick }) => (
<IconButton
onClick={onClick}
color={row.status === 'off' ? 'primary' : 'default'}
disabled={row.status === 'running'}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
);
const StopButton = ({ row, onClick }) => (
<IconButton
onClick={onClick}
color={row.status === 'running' ? 'primary' : 'default'}
disabled={row.status === 'off'}
>
<StopIcon fontSize="small" />
</IconButton>
);
export default function RowActions() {
const classes = useStyles();
const [rows, setRows] = useState([
{
id: 1,
name: 'First Item',
status: 'running'
},
{
id: 2,
name: 'Second Item',
status: 'off'
},
{
id: 3,
name: 'Third Item',
status: 'off'
},
{
id: 4,
name: 'Fourth Item',
status: 'running'
},
{
id: 5,
name: 'Fifth Item',
status: 'off'
}
]);
const toggleStatus = id => () => {
const newRows = [...rows];
const index = rows.findIndex(row => row.id === id);
const row = rows[index];
newRows[index] = {
...row,
status: row.status === 'running' ? 'off' : 'running'
};
setRows(newRows);
};
return (
<Paper className={classes.root}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Status</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => {
return (
<TableRow key={row.id}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell>{row.status}</TableCell>
<TableCell>
<StartButton
row={row}
onClick={toggleStatus(row.id)}
/>
<StopButton
row={row}
onClick={toggleStatus(row.id)}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</Paper>
);
}
这是屏幕首次加载时的样子:
根据行数据的状态
,操作按钮将显示不同。例如,在第一行中,启动按钮被禁用,因为状态
是running
。第二行有一个禁用的停止按钮,因为状态
是off
。让我们尝试点击第一行的停止按钮和第二行的启动按钮。完成这些操作后,UI 将如何变化:
它是如何工作的...
让我们从查看用作行操作的两种组件开始:
const StartButton = ({ row, onClick }) => (
<IconButton
onClick={onClick}
color={row.status === 'off' ? 'primary' : 'default'}
disabled={row.status === 'running'}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
);
const StopButton = ({ row, onClick }) => (
<IconButton
onClick={onClick}
color={row.status === 'running' ? 'primary' : 'default'}
disabled={row.status === 'off'}
>
<StopIcon fontSize="small" />
</IconButton>
);
StartButton
和StopButton
组件非常相似。这两个组件在表格的每一行中都被渲染。有一个onClick
属性,这是一个函数,当点击时它会改变行数据的当前状态。图标的颜色
会根据行的状态
改变。同样,disabled
属性也会根据行的状态
改变。
接下来,让我们看看当点击操作按钮时被调用的toggleStatus()
处理程序,它会改变行的状态状态:
const toggleStatus = id => () => {
const newRows = [...rows];
const index = rows.findIndex(row => row.id === id);
const row = rows[index];
newRows[index] = {
...row,
status: row.status === 'running' ? 'off' : 'running'
};
setRows(newRows);
};
StartButton
和 StopButton
组件都使用相同的处理函数——它在 status
值之间切换 running
和 off
。最后,让我们看看 TableCell
组件,这些 row
动作在这里被渲染:
<TableCell>
<StartButton
row={row}
onClick={toggleStatus(row.id)}
/>
<StopButton
row={row}
onClick={toggleStatus(row.id)}
/>
</TableCell>
行数据作为 row
属性传递。toggleStatus()
函数接受一个 row id
参数,并返回一个作用于该行的新处理函数。
参见
Table
示例:material-ui.com/demos/tables/
第八章:卡片 - 显示详细信息
在本章中,您将学习以下关于卡片的内容:
-
主要内容
-
卡片标题
-
执行操作
-
展示媒体
-
可展开卡片
简介
卡片是用于在给定主题上显示特定信息的 Material Design 概念。例如,主题可以是 API 端点返回的对象。或者,主题可以是复杂对象的一部分——在这种情况下,您可以使用多个卡片以帮助用户理解他们正在查看的信息的方式来组织信息。
主要内容
Card
组件的主要内容是放置与主题相关的信息的地方。CardContent
组件是Card
的子组件,您可以使用它来渲染其他 Material UI 组件,例如Typography
。
如何做...
假设您正在为某种类型的实体(如博客文章)创建一个详情屏幕。您决定使用Card
组件来渲染一些实体详情,因为实体是考虑的主题。以下是渲染包含特定主题信息的Card
组件的代码:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
card: {
maxWidth: 400
},
content: {
marginTop: theme.spacing(1)
}
});
const MainContent = withStyles(styles)(({ classes }) => (
<Card className={classes.card}>
<CardContent>
<Typography variant="h4">Subject Title</Typography>
<Typography variant="subtitle1">
A little more about subject
</Typography>
<Typography className={classes.content}>
Even more information on the subject, contained within the
card. You can fit a lot of information here, but don't try to
overdo it.
</Typography>
</CardContent>
</Card>
));
export default MainContent;
当您首次加载屏幕时,您将看到以下内容:
卡片内容分为三个部分:
-
主题标题:告诉用户他们正在查看什么
-
副标题:为用户提供更多上下文
-
内容:主题的主要内容
它是如何工作的...
此示例使用CardContent
组件作为Card
中的主要组织单元。其余部分由您自行决定。例如,本示例中的卡片使用三个Typography
组件来渲染三种不同样式的文本作为卡片内容。
第一个Typography
组件使用h4
变体,作为卡片的标题。第二个Typography
组件作为卡片的副标题,使用subtitle1
变体。最后,是卡片的主要内容,使用Typography
默认字体。此文本设置了marginTop
样式,以便它不会紧挨着副标题。
相关内容
卡片标题
CardHeader
组件用于渲染卡片的标题。这包括标题文本以及一些其他潜在元素。您可能想要使用CardHeader
组件的原因是让它处理标题的布局样式,并保持您的Card
语义内的标记。
如何做...
假设您正在为您的应用程序用户构建一个card
组件。作为卡片标题,您想显示用户的姓名。您可以使用CardHeader
组件,而不是使用Typography
组件通过文本变体来渲染标题,将其放置在CardContent
组件旁边。以下是代码的显示方式:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import Typography from '@material-ui/core/Typography';
import Avatar from '@material-ui/core/Avatar';
import PersonIcon from '@material-ui/icons/Person';
const styles = theme => ({
card: {
maxWidth: 400
}
});
const CardHeader = withStyles(styles)(({ classes }) => (
<Card className={classes.card}>
<CardHeader
title="Ron Swanson"
subheader="Legend"
avatar={
<Avatar>
<PersonIcon />
</Avatar>
}
/>
<CardContent>
<Typography variant="caption">Joined 2009</Typography>
<Typography>
Some filler text about the user. There doesn't have to be a
lot - just enough so that the text spans at least two lines.
</Typography>
</CardContent>
</Card>
));
export default CardHeader;
下面是屏幕的显示效果:
它是如何工作的...
让我们看看渲染此卡片所使用的标记:
<Card className={classes.card}>
<CardHeader title="Ron Swanson" />
<CardContent>
<Typography variant="caption">Joined 2009</Typography>
<Typography>
Some filler text about the user. There doesn't have to be a
lot - just enough so that the text spans at least two lines.
</Typography>
</CardContent>
CardHeader
组件是 CardContent
的兄弟组件。这使得 Card
标记语义化,而不是需要在 CardContent
内声明卡片头部。CardHeader
组件接受一个 title
字符串属性,这是卡片标题的渲染方式。
还有更多...
你可以向 CardHeader
组件添加不仅仅是字符串。你还可以传递一个副标题字符串和一个头像,以帮助用户识别卡片中的主题。让我们修改这个示例以添加这两者。首先,这里是你需要添加的新组件导入:
import Avatar from '@material-ui/core/Avatar';
import PersonIcon from '@material-ui/icons/Person';
接下来,这是更新后的 CardHeader
标记:
<CardHeader
title="Ron Swanson"
subheader="Legend"
avatar={
<Avatar>
<PersonIcon />
</Avatar>
}
/>
下面是结果的样子:
CardHeader
组件处理三个头部组件的对齐——头像、标题和副标题。
相关内容
Card
示例:material-ui.com/demos/cards/
执行操作
卡片用于显示关于主题的特定操作。通常,用户会对主题执行操作,例如向联系人发送消息或删除联系人。CardActions
组件可以被 Card
组件用来显示用户可以对主题执行的操作。
如何做到...
假设你正在使用 Card
组件来显示一个联系人。除了显示联系人的信息外,你还希望用户能够在卡片内对联系人执行操作。例如,你可以提供两个操作——一个用于给联系人发消息,另一个用于给联系人打电话。以下是执行此操作的代码:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Typography from '@material-ui/core/Typography';
import Avatar from '@material-ui/core/Avatar';
import IconButton from '@material-ui/core/IconButton';
import PersonIcon from '@material-ui/icons/Person';
import ContactMailIcon from '@material-ui/icons/ContactMail';
import ContactPhoneIcon from '@material-ui/icons/ContactPhone';
const styles = theme => ({
card: {
maxWidth: 400
}
});
const PerformingActions = withStyles(styles)(({ classes }) => (
<Card className={classes.card}>
<CardHeader
title="Ron Swanson"
subheader="Legend"
avatar={
<Avatar>
<PersonIcon />
</Avatar>
}
/>
<CardContent>
<Typography variant="caption">Joined 2009</Typography>
<Typography>
Some filler text about the user. There doesn't have to be a
lot - just enough so that the text spans at least two lines.
</Typography>
</CardContent>
<CardActions disableActionSpacing>
<IconButton>
<ContactMailIcon />
</IconButton>
<IconButton>
<ContactPhoneIcon />
</IconButton>
</CardActions>
</Card>
));
export default PerformingActions;
当屏幕首次加载时,卡片看起来是这样的:
用户可以对主题执行的两种操作以图标按钮的形式渲染在卡片底部。
它是如何工作的...
CardActions
组件负责在其内部对按钮项进行水平对齐,并确保它们放置在卡片底部。disableActionSpacing
属性移除了 CardActions
添加的额外边距。通常,你会在使用 IconButton
组件作为 actions
时使用此属性。
让我们更仔细地看看标记:
<CardActions disableActionSpacing>
<IconButton>
<ContactMailIcon />
</IconButton>
<IconButton>
<ContactPhoneIcon />
</IconButton>
</CardActions>
与 Card
的其他子组件一样,CardActions
组件使整体卡片结构语义化,因为它与相关的卡片功能是兄弟关系。放置在 CardActions
内的项可以是任何你想要的内容,但常见的做法是使用图标按钮。
还有更多...
你可以改变 CardActions
组件中项的对齐方式。由于它使用 flexbox 作为其显示方式,你可以使用任何 justify-content
的值。下面是一个更新版本,将操作按钮对齐到卡片的右侧:
const styles = theme => ({
card: {
maxWidth: 400
},
actions: {
justifyContent: 'flex-end'
}
});
const PerformingActions = withStyles(styles)(({ classes }) => (
<Card className={classes.card}>
<CardHeader
title="Ron Swanson"
subheader="Legend"
avatar={
<Avatar>
<PersonIcon />
</Avatar>
}
/>
<CardContent>
<Typography variant="caption">Joined 2009</Typography>
<Typography>
Some filler text about the user. There doesn't have to be a
lot - just enough so that the text spans at least two lines.
</Typography>
</CardContent>
<CardActions disableActionSpacing className={classes.actions}>
<IconButton>
<ContactMailIcon />
</IconButton>
<IconButton>
<ContactPhoneIcon />
</IconButton>
</CardActions>
</Card>
));
export default PerformingActions;
justify-content
属性是 actions
样式的一部分,然后应用于 CardActions
组件。下面是结果的样子:
这是另一个版本,显示 center
作为 justify-content
的值:
相关内容
Card
示例:material-ui.com/demos/cards/
展示媒体
卡片具有内置的显示媒体的能力。这包括图像和视频,它们成为卡片的核心内容。
如何实现...
假设你有一个 Card
组件显示的主题图片。你可以使用 CardMedia
组件来渲染图片。你应该使用这个组件而不是 <img>
这样的东西,因为它会为你处理许多样式问题。以下是代码:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import CardMedia from '@material-ui/core/CardMedia';
import CardActions from '@material-ui/core/CardActions';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
card: {
maxWidth: 322
},
media: {
width: 322,
height: 322
}
});
const PresentingMedia = withStyles(styles)(({ classes }) => (
<Card className={classes.card}>
<CardHeader title="Grapefruit" subheader="Red" />
<CardMedia
className={classes.media}
image="grapefruit-slice-332-332.jpg"
title="Grapefruit"
/>
<CardContent>
<Typography>Mmmm. Grapefruit.</Typography>
</CardContent>
</Card>
));
export default PresentingMedia;
这是渲染后的卡片的样子:
它是如何工作的...
CardMedia
组件就像构成卡片的其它组件一样——只是另一个部分。在这个例子中,CardMedia
放在 CardHeader
下方和 CardContent
上方。但不必这样。你可以重新排列这些组件的顺序。
更多内容...
你可以根据你的应用逻辑重新排列你的卡片项目。例如,你的带有媒体的卡片可能没有任何内容,你可能想在卡片的底部显示标题文本,在媒体下方,并且文本居中。以下是修改后的代码:
const styles = theme => ({
card: {
maxWidth: 322
},
media: {
width: 322,
height: 322
},
header: {
textAlign: 'center'
}
});
const PresentingMedia = withStyles(styles)(({ classes }) => (
<Card className={classes.card}>
<CardMedia
className={classes.media}
image="https://interactive-grapefruit-slice-332-332.jpg"
title="Grapefruit"
/>
<CardHeader
className={classes.header}
title="Grapefruit"
subheader="Red"
/>
</Card>
));
export default PresentingMedia;
这是最终卡片的样式:
相关内容
img
HTML 标签参考:developer.mozilla.org/en-US/docs/Web/HTML/Element/img
可展开卡片
有时,你可能无法将所有想要的内容都放入卡片中。为了适应,你可以使你的卡片可展开,这意味着用户可以点击 expand
按钮来显示附加内容。
如果你试图在 Card
中放入太多内容,使卡片可展开只是掩盖了问题。相反,考虑一种不同的方法来显示关于所讨论主题的信息。例如,也许,而不是卡片,主题值得拥有自己的页面。
如何实现...
让我们看看卡片内关于一个主题的附加内容:
-
占用过多的垂直空间
-
并不重要,不需要默认显示
你可以通过将内容放入卡片的可展开区域来处理这两个挑战。这样,垂直空间就不是问题,用户如果认为内容相关,可以查看内容。以下是一个基于本章早期示例构建的示例,其中默认隐藏卡片的部分内容:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardContent from '@material-ui/core/CardContent';
import CardActions from '@material-ui/core/CardActions';
import Typography from '@material-ui/core/Typography';
import Avatar from '@material-ui/core/Avatar';
import IconButton from '@material-ui/core/IconButton';
import Collapse from '@material-ui/core/Collapse';
import PersonIcon from '@material-ui/icons/Person';
import ContactMailIcon from '@material-ui/icons/ContactMail';
import ContactPhoneIcon from '@material-ui/icons/ContactPhone';
import ExpandLessIcon from '@material-ui/icons/ExpandLess';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
const useStyles = makeStyles(theme => ({
card: {
maxWidth: 400
},
expand: {
marginLeft: 'auto'
}
}));
const ExpandIcon = ({ expanded }) =>
expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />;
export default function ExpandableCards() {
const classes = useStyles();
const [expanded, setExpanded] = useState(false);
const toggleExpanded = () => {
setExpanded(!expanded);
};
return (
<Card className={classes.card}>
<CardHeader
title="Ron Swanson"
subheader="Legend"
avatar={
<Avatar>
<PersonIcon />
</Avatar>
}
/>
<CardContent>
<Typography variant="caption">Joined 2009</Typography>
<Typography>
Some filler text about the user. There doesn't have to be a
lot - just enough so that the text spans at least two lines.
</Typography>
</CardContent>
<CardActions disableActionSpacing>
<IconButton>
<ContactMailIcon />
</IconButton>
<IconButton>
<ContactPhoneIcon />
</IconButton>
<IconButton
className={classes.expand}
onClick={toggleExpanded}
>
<ExpandIcon expanded={expanded} />
</IconButton>
</CardActions>
<Collapse in={expanded}>
<CardContent>
<Typography>
Even more filler text about the user. It doesn't fit in
the main content area of the card, so this is what the
user will see when they click the expand button.
</Typography>
</CardContent>
</Collapse>
</Card>
);
}
当你首次加载屏幕时,卡片看起来是这样的:
在卡片操作按钮的右侧,现在有一个带有向下箭头的 expand
按钮。如果你点击 expand
按钮,以下是卡片展开时的样子:
展开图标现在已更改为折叠图标——点击它将使卡片折叠回原始状态。
它是如何工作的...
让我们分析一下这个示例中添加的可展开卡片区域。首先,是 expand
样式:
expand: {
marginLeft: 'auto'
}
这用于将展开/折叠图标按钮对齐到其他操作按钮的左侧。接下来,让我们看看 ExpandIcon
组件:
const ExpandIcon = ({ expanded }) =>
expanded ? <ExpandLessIcon /> : <ExpandMoreIcon />;
这个实用组件用于根据组件的展开状态渲染正确的图标组件。接下来,让我们看看 toggleExpanded()
函数:
const toggleExpanded = () => {
setExpanded(!expanded);
};
当调用此处理程序时,将切换展开状态。然后,将此状态传递给 ExpandIcon
组件,该组件将渲染适当的图标。接下来,让我们更仔细地看看这张卡的标记:
<CardActions disableActionSpacing>
<IconButton>
<ContactMailIcon />
</IconButton>
<IconButton>
<ContactPhoneIcon />
</IconButton>
<IconButton
className={classes.expand}
onClick={toggleExpanded}
>
<ExpandIcon expanded={expanded} />
</IconButton>
</CardActions>
展开折叠按钮是这里显示的最后一个 IconButton
组件。它使用展开样式,toggleExpanded()
点击处理程序和展开状态。最后,让我们看看当按钮被点击时可以展开和折叠的卡片内容:
<Collapse in={expanded}>
<CardContent>
<Typography>
Even more filler text about the user. It doesn't fit
in the main content area of the card, so this is what
the user will see when they click the expand button.
</Typography>
</CardContent>
</Collapse>
Collapse
组件用于根据展开状态显示或隐藏额外的卡片内容。请注意,这里使用的是 CardContent
组件,以确保一旦显示额外内容,其样式与卡片内容的其余部分保持一致。
参见
-
Card
示例:material-ui.com/demos/cards/
-
Card
API 文档:material-ui.com/api/card/
-
CardHeader
API 文档:material-ui.com/api/card-header/
-
CardContent
API 文档:material-ui.com/api/card-content/
-
CardActions
API 文档:material-ui.com/api/card-actions/
-
IconButton
API 文档:material-ui.com/api/icon-button/
-
Collapse
API 文档:material-ui.com/api/collapse/
第九章:Snackbars - 临时消息
在本章中,你将了解以下内容:
-
Snackbar 内容
-
使用状态控制可见性
-
Snackbars 过渡
-
Snackbars 的位置
-
错误边界和错误 Snackbars
-
带有操作的 Snackbars
-
Snackbars 排队
简介
Material-UI 附带一个用于向用户显示消息的Snackbar
组件。这些消息简短、短暂,不会干扰主要应用程序组件。
Snackbar 内容
文本是你在为用户显示的Snackbar
消息内容中最常见的形式。因此,Snackbar
组件使得设置消息内容和显示 snackbar 变得简单直接。
如何做到...
Snackbar
组件的message
属性接受一个字符串值或任何其他有效的React
元素。以下代码展示了如何设置Snackbar
组件的内容并显示它:
import React from 'react';
import Snackbar from '@material-ui/core/Snackbar';
const MySnackbarContent = () => <Snackbar open={true} message="Test" />;
export default MySnackbarContent;
当页面首次加载时,你会看到一个看起来像这样的 snackbar:
它是如何工作的...
默认情况下,snackbar 并不复杂,但它将你的文本内容渲染为message
属性中指定的内容。open
属性设置为 true,因为任何其他值都会隐藏 snackbar。
更多内容...
Snackbar
组件使用SnackbackContent
组件来渲染实际显示的内容。反过来,SnackbarContent
使用Paper
,而Paper
使用Typography
。在这个所有间接层中导航可能有点棘手,但幸运的是,你不必这样做。相反,你可以通过ContentProps
属性将属性一路传递到Typography
组件。
假设你想要使用h6
排版变体。以下是你可以这样做的方法:
import React from 'react';
import Snackbar from '@material-ui/core/Snackbar';
const MySnackbarContent () => (
<Snackbar
open={true}
message="Test"
ContentProps={{ variant: 'h6' }}
/>
);
export default MySnackbarContent;
你想要传递给Paper
组件的任何属性都可以通过ContentProps
设置。在这里,你正在传递variant
属性——这会导致以下视觉变化:
最终结果是更大的文本和更宽的边距。这个例子不是为了这个特定的排版变化,而是为了说明你可以以与Typography
组件相同的方式自定义Snackbar
文本。
你可以在 snackbar 内容中放入你想要的任何数量的组件或尽可能少的组件。例如,你可以将子组件传递给Snackbar
而不是在message
属性中。然而,我建议尽可能保持你的 snackbar 内容简单。你不想在已经设计用来处理简单文本的组件中陷入设计陷阱。
相关内容
-
Snackbar
演示:material-ui.com/demos/snackbars/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
使用状态控制可见性
Snackbar 是在响应某些事件时显示的。例如,如果你的应用程序中创建了一个新的资源,那么使用 Snackbar
组件将此信息传达给用户是一个不错的选择。如果你需要控制 Snackbar 的状态,那么你需要添加一个控制 Snackbar 可见性的状态。
如何做到这一点...
open
属性用于控制 Snackbar 的可见性。为了控制这个属性的值,你需要传递一个状态值给它。然后,当这个状态改变时,Snackbar 的可见性也会随之改变。以下是一些代码示例,说明了如何通过状态控制 Snackbar 的基本概念:
import React, { Fragment, useState } from 'react';
import Button from '@material-ui/core/Button';
import Snackbar from '@material-ui/core/Snackbar';
export default function ControllingVisibilityWithState() {
const [open, setOpen] = useState(false);
const showSnackbar = () => {
setOpen(true);
};
return (
<Fragment>
<Button variant="contained" onClick={showSnackbar}>
Show Snackbar
</Button>
<Snackbar open={open} message="Visible Snackbar!" />
</Fragment>
);
}
当你首次加载屏幕时,你将看到的只是一个“显示 Snackbar”按钮:
点击此按钮将显示 Snackbar:
它是如何工作的...
组件有一个 open
状态,该状态决定了 Snackbar 的可见性。open
的值传递给了 Snackbar
的 open
属性。当用户点击“显示 Snackbar”按钮时,showSnackbar()
函数将 open
状态设置为 true。结果,true 值被传递给了 Snackbar
的 open
属性。
还有更多...
一旦显示了一个 Snackbar,你将需要某种方式来关闭它。同样,open
状态可以隐藏 Snackbar。但如何将 open
状态改回 false 呢?Snackbar 消息的典型模式是它们只短暂出现,之后会自动隐藏。
通过向 Snackbar
传递两个额外的属性,你可以增强这个例子,使得 Snackbar 在一段时间后自动隐藏。以下是更新后的代码:
import React, { Fragment, useState } from 'react';
import Button from '@material-ui/core/Button';
import Snackbar from '@material-ui/core/Snackbar';
export default function ControllingVisibilityWithState() {
const [open, setOpen] = useState(false);
const showSnackbar = () => {
setOpen(true);
};
const hideSnackbar = () => {
setOpen(false);
};
return (
<Fragment>
<Button variant="contained" onClick={showSnackbar}>
Show Snackbar
</Button>
<Snackbar
open={open}
onClose={hideSnackbar}
autoHideDuration={5000}
message="Visible Snackbar!"
/>
</Fragment>
);
}
组件中添加了一个新函数——hideSnackbar()
。这个函数被传递给了 Snackbar
的 onClose
属性。autoHideDuration
组件是你希望 Snackbar 保持可见的毫秒数。在这个例子中,五秒后,Snackbar
组件将调用传递给其 onClose
属性的函数。这会将 open
状态设置为 false,然后这个值被传递给了 Snackbar
的 open
属性。
参见
-
Snackbar
演示:material-ui.com/demos/snackbars/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
-
Button
API 文档:material-ui.com/api/button/
Snackbar 过渡
你可以控制 Snackbar
组件在显示和隐藏时使用的过渡效果。Snackbar
组件直接通过属性支持过渡自定义,因此你不需要花费太多时间去思考如何实现你的 Snackbar
过渡效果。
如何做到这一点...
假设你想要使整个应用程序中 snackbars 所使用的过渡效果更容易更改。你可以在 Snackbar
组件周围创建一个薄的包装组件,负责设置适当的属性。以下是代码的样子:
import React, { Fragment, useState } from 'react';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import Snackbar from '@material-ui/core/Snackbar';
import Slide from '@material-ui/core/Slide';
import Grow from '@material-ui/core/Grow';
import Fade from '@material-ui/core/Fade';
const MySnackbar = ({ transition, direction, ...rest }) => (
<Snackbar
TransitionComponent={
{ slide: Slide, grow: Grow, fade: Fade }[transition]
}
TransitionProps={{ direction }}
{...rest}
/>
);
export default function SnackbarTransitions() {
const [first, setFirst] = useState(false);
const [second, setSecond] = useState(false);
const [third, setThird] = useState(false);
const [fourth, setFourth] = useState(false);
return (
<Fragment>
<Grid container spacing={8}>
<Grid item>
<Button variant="contained" onClick={() => setFirst(true)}>
Slide Down
</Button>
</Grid>
<Grid item>
<Button variant="contained" onClick={() => setSecond(true)}>
Slide Up
</Button>
</Grid>
<Grid item>
<Button variant="contained" onClick={() => setThird(true)}>
Grow
</Button>
</Grid>
<Grid item>
<Button variant="contained" onClick={() => setFourth(true)}>
Fade
</Button>
</Grid>
</Grid>
<MySnackbar
open={first}
onClose={() => setFirst(false)}
autoHideDuration={5000}
message="Slide Down"
transition="slide"
direction="down"
/>
<MySnackbar
open={second}
onClose={() => setSecond(false)}
autoHideDuration={5000}
message="Slide Up"
transition="slide"
direction="up"
/>
<MySnackbar
open={third}
onClose={() => setThird(false)}
autoHideDuration={5000}
message="Grow"
transition="grow"
/>
<MySnackbar
open={fourth}
onClose={() => setFourth(false)}
autoHideDuration={5000}
message="Fade"
transition="fade"
/>
</Fragment>
);
}
此代码渲染了四个按钮和四个 snackbars。当你首次加载屏幕时,你只会看到按钮:
点击这些按钮中的每一个将在屏幕底部显示相应的 Snackbar
组件。如果你注意观察每个 snackbars 显示时使用的过渡效果,你会注意到根据你按的按钮的不同而有所差异。例如,点击 Fade 按钮,将使用 fade
过渡,结果如下 snackbar:
它是如何工作的...
让我们从查看这个例子中创建的 MySnackbar
组件开始:
const MySnackbar = ({ transition, direction, ...rest }) => (
<Snackbar
TransitionComponent={
{ slide: Slide, grow: Grow, fade: Fade }[transition]
}
TransitionProps={{ direction }}
{...rest}
/>
);
这里有两个有趣的属性。第一个是 transition
字符串。它用于查找要使用的过渡组件。例如,字符串 slide
将使用 Slide
组件。生成的组件由 TransitionComponent
属性使用。Snackbar
组件将内部使用此组件来应用您 snackbars 所需要的过渡。direction
属性与 Slide
过渡一起使用,这就是为什么这个属性被传递给 TransitionProps
。这些属性值直接传递给传递给 TransitionComponent
的组件。
使用 TransitionProps
的替代方法是创建一个高阶组件,它包装自己的属性自定义值。但是,由于 Snackbar
已经设置好以帮助您传递属性,如果您想避免创建另一个组件,那么就没有必要再创建一个。
接下来,让我们看看组件状态及其改变它的函数:
const [first, setFirst] = useState(false);
const [second, setSecond] = useState(false);
const [third, setThird] = useState(false);
const [fourth, setFourth] = useState(false);
first
、second
、third
和 fourth
状态对应于它们自己的 Snackbar
组件。这些状态值控制每个函数的可见性,它们对应的设置函数显示或隐藏 snackbars。
最后,让我们看看两个正在渲染的 MySnackbar
组件:
<MySnackbar
open={first}
onClose={() => setFirst(false)}
autoHideDuration={5000}
message="Slide Down"
transition="slide"
direction="down"
/>
<MySnackbar
open={second}
onClose={() => setSecond(false)}
autoHideDuration={5000}
message="Slide Up"
transition="slide"
direction="up"
/>
这两个实例都使用 slide
过渡。然而,每个的 direction
属性是不同的。MySnackbar
抽象使你指定过渡和过渡参数变得稍微简单一些。
参见
-
Snackbar
演示:material-ui.com/demos/snackbars/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
-
Slide
API 文档:material-ui.com/api/slide/
-
Grow
API 文档:material-ui.com/api/grow/
-
Fade
API 文档:material-ui.com/api/fade/
snackbars 定位
Material-UI 的 Snackbar
组件有一个 anchorOrigin
属性,允许你在显示时更改 snackbars 的位置。你可能对 snackbars 的默认定位很满意,但有时你需要这种程度的定制来保持与其他应用程序部分的统一。
如何实现...
虽然你无法随意在屏幕上定位 snackbars,但有一些选项允许你更改 snackbars 的位置。以下是一些代码,允许你玩转 anchorOrigin
属性值:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Snackbar from '@material-ui/core/Snackbar';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(3)
}
}));
export default function PositioningSnackbars() {
const classes = useStyles();
const [vertical, setVertical] = useState('bottom');
const [horizontal, setHorizontal] = useState('left');
const onVerticalChange = event => {
setVertical(event.target.value);
};
const onHorizontalChange = event => {
setHorizontal(event.target.value);
};
return (
<Fragment>
<FormControl
component="fieldset"
className={classes.formControl}
>
<FormLabel component="legend">Vertical</FormLabel>
<RadioGroup
name="vertical"
className={classes.group}
value={vertical}
onChange={onVerticalChange}
>
<FormControlLabel
value="top"
control={<Radio />}
label="Top"
/>
<FormControlLabel
value="bottom"
control={<Radio />}
label="Bottom"
/>
</RadioGroup>
</FormControl>
<FormControl
component="fieldset"
className={classes.formControl}
>
<FormLabel component="legend">Horizontal</FormLabel>
<RadioGroup
name="horizontal"
className={classes.group}
value={horizontal}
onChange={onHorizontalChange}
>
<FormControlLabel
value="left"
control={<Radio />}
label="Left"
/>
<FormControlLabel
value="center"
control={<Radio />}
label="Center"
/>
<FormControlLabel
value="right"
control={<Radio />}
label="Right"
/>
</RadioGroup>
</FormControl>
<Snackbar
anchorOrigin={{
vertical,
horizontal
}}
open={true}
message="Positioned Snackbar"
/>
</Fragment>
);
}
当屏幕首次加载时,你会看到用于更改 snackbar 位置的控件,以及默认位置的 Snackbar
组件:
如果你更改了任何位置控制值,snackbar 将移动到新的位置。例如,如果你将垂直锚点更改为顶部,并将水平锚点更改为右侧,以下是你会看到的内容:
它是如何工作的...
在本例中的两个单选按钮组仅用于说明可用的不同位置值组合。在实际应用中,当你显示 snackbars 时,你不会有可配置的状态来改变 snackbars 的位置。相反,你应该将传递给 anchorOrigin
属性的值视为在启动时设置的一次性配置值。
依赖于状态值并不好,就像本例中那样:
<Snackbar
anchorOrigin={{
vertical,
horizontal
}}
open={true}
message="Positioned Snackbar"
/>
相反,你应该静态地设置 anchorOrigin
值:
<Snackbar
anchorOrigin={{
vertical: 'top'
horizontal: 'right'
}}
open={true}
message="Positioned Snackbar"
/>
还有更多...
一旦你知道你想要将 snackbars 定位在哪里,你就可以创建一个具有定义好的 anchorOrigin
值的自己的 Snackbar
组件。以下是一个示例:
const MySnackbar = props => (
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...props}
/>
);
在你的应用程序中任何使用 MySnackbar
的地方,snackbar 都将在屏幕的右上角显示。否则,MySnackbar
就像是一个普通的 Snackbar
组件。
相关内容
-
Snackbar
示例:material-ui.com/demos/snackbars/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
错误边界和错误 snackbars
React 中的错误边界使你能够在组件尝试渲染时捕获错误。你可以在错误边界中使用 Snackbar
组件来显示捕获的错误。此外,你可以对 snackbars 进行样式化,使错误与普通消息在视觉上有所区别。
如何实现...
假设你在应用程序的最高级别有一个错误边界,并且你想使用 Snackbar
组件向用户显示错误信息。以下是一个示例,展示了你可以如何做到这一点:
import React, { Fragment, Component } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Snackbar from '@material-ui/core/Snackbar';
import Button from '@material-ui/core/Button';
const styles = theme => ({
error: {
backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText
}
});
const ErrorBoundary = withStyles(styles)(
class extends Component {
state = { error: null };
onClose = () => {
this.setState({ error: null });
};
componentDidCatch(error) {
this.setState({ error });
}
render() {
const { classes } = this.props;
return (
<Fragment>
{this.state.error === null && this.props.children}
<Snackbar
open={Boolean(this.state.error)}
message={
this.state.error !== null && this.state.error.toString()
}
ContentProps={{ classes: { root: classes.error } }}
/>
</Fragment>
);
}
}
);
const MyButton = () => {
throw new Error('Random error');
};
export default () => (
<ErrorBoundary>
<MyButton />
</ErrorBoundary>
);
当你加载此屏幕时,MyButton
组件在渲染时抛出错误。以下是你会看到的内容:
它明确地抛出一个错误,这样你就可以看到错误边界机制在起作用。在实际应用中,错误可能是由渲染过程中调用的任何函数触发的。
它是如何工作的...
让我们首先更仔细地看看ErrorBoundary
组件。它有一个初始为 null 的error
状态。componentDidCatch()
生命周期方法在发生错误时改变这个状态:
componentDidCatch(error) {
this.setState({ error });
}
接下来,让我们更仔细地看看render()
方法:
render() {
const { classes } = this.props;
return (
<Fragment>
{this.state.error === null && this.props.children}
<Snackbar
open={Boolean(this.state.error)}
message={
this.state.error !== null && this.state.error.toString()
}
ContentProps={{ classes: { root: classes.error } }}
/>
</Fragment>
);
}
它使用error
状态来确定是否应该渲染子组件。当error
状态非空时,渲染子组件没有意义,因为你将陷入错误被抛出和处理的无限循环。error
状态还用作open
属性,以确定 snackbar 是否应该显示,以及作为消息文本。
ContentProps
属性用于样式化 snackbar,使其看起来像错误。error
类使用theme
值来改变背景和文字颜色:
const styles = theme => ({
error: {
backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText
}
});
还有更多...
这个示例中使用的错误边界覆盖了整个应用程序。从一方面来说,你可以一次性在整个应用程序中应用错误处理,这是好的。但这也是不好的,因为整个用户界面都消失了,因为错误边界不知道哪个组件失败了。
因为错误边界是组件,你可以在组件树的任何级别放置尽可能多的它们。这样,你可以在屏幕上保持 UI 中未失败的部分可见的同时显示 Material-UI error
snackbars。
让我们更改示例中使用的错误边界的范围。首先,你可以更改MyButton
实现,使其仅在布尔属性为true
时抛出错误:
const MyButton = ({ label, throwError }) => {
if (throwError) {
throw new Error('Random error');
}
return <Button>{label}</Button>;
};
现在你可以渲染一个带有指定标签的按钮。如果throwError
为true
,则由于错误,没有任何内容渲染。接下来,让我们更改示例的标记,以包含多个按钮和多个error
边界:
export default () => (
<Fragment>
<ErrorBoundary>
<MyButton label="First Button" />
</ErrorBoundary>
<ErrorBoundary>
<MyButton label="Second Button" throwError />
</ErrorBoundary>
</Fragment>
);
第一个按钮渲染时没有任何问题。然而,如果错误边界像之前那样是全包容性的,那么这个按钮就不会显示。第二个按钮抛出错误,因为throwError
属性为真。因为这个按钮有自己的错误边界,所以它不会阻止其他工作正常的 UI 部分渲染。现在当你运行示例时,你会看到以下内容:
参见
-
React
error
边界:reactjs.org/docs/error-boundaries.html
-
Snackbar
示例: https://material-ui.com/demos/snackbars/ -
Snackbar
API 文档:material-ui.com/api/snackbar/
带有操作的 Snackbars
Material-UI snackbars 的目的是向用户显示简短的消息。此外,你还可以在 snackbar 中嵌入用户的下一步操作。
如何操作...
假设您想在 Snackbar 中添加一个简单的按钮来关闭 Snackbar。这可以在 Snackbar 自动关闭之前关闭它很有用。或者,您可能希望用户通过手动关闭来明确确认消息。以下是向Snackbar
组件添加关闭按钮的代码:
import React, { Fragment, useState } from 'react';
import { Route, Link } from 'react-router-dom';
import Snackbar from '@material-ui/core/Snackbar';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import CloseIcon from '@material-ui/icons/Close';
export default function Snackbars() {
const [open, setOpen] = useState(false);
return (
<Fragment>
<Button onClick={() => setOpen(true)}>Do Something</Button>
<Snackbar
open={open}
onClose={() => setOpen(false)}
message="All done doing the thing"
action={[
<IconButton color="inherit" onClick={() => setOpen(false)}>
<CloseIcon />
</IconButton>
]}
/>
</Fragment>
);
}
当屏幕首次加载时,您将只看到一个按钮:
点击此按钮将显示 Snackbar:
在 Snackbar 右侧的关闭图标按钮,当点击时,会关闭 Snackbar。
它是如何工作的...
关闭按钮是通过action
属性添加到Snackbar
组件中的,该属性接受节点或节点数组。SnackbarContent
组件负责应用样式以在 Snackbar 内对齐操作。
还有更多...
当用户在您的应用程序中创建新资源时,您可能希望让他们知道资源是否成功创建。Snackbar 是完成这个任务的理想工具,因为它不会强制用户离开他们可能正在进行的事情。如果 Snackbar 中包含一个链接到新创建资源的操作按钮,那就更好了。
让我们修改这个示例,当用户点击 CREATE 按钮时,他们会看到一个包含以下内容的 Snackbar:
-
简短的消息
-
关闭操作
-
新资源的链接
让我们添加来自react-router-dom
的路由,并将链接添加到 Snackbar 中。以下是新的标记:
<Fragment>
<Route
exact
path="/"
render={() => (
<Button onClick={() => setOpen(true)}>create thing</Button>
)}
/>
<Route
exact
path="/thing"
render={() => <Typography>The Thing</Typography>}
/>
<Snackbar
open={open}
onClose={() => setOpen(false)}
message="Finished creating thing"
action={[
<Button
color="secondary"
component={Link}
to="/thing"
onClick={() => setOpen(false)}
>
The Thing
</Button>,
<IconButton color="inherit" onClick={() => setOpen(false)}>
<CloseIcon />
</IconButton>
]}
/>
</Fragment>
第一条路由是用于索引页面的,因此,当屏幕首次加载时,用户将看到由该路由渲染的按钮:
当您点击此按钮时,您将看到一个包含指向新创建资源的链接的 Snackbar:
现在您已经为用户提供了一个轻松导航到资源的途径,而不会打断他们当前正在做的事情。
参见
-
React 路由指南:
reacttraining.com/react-router/web/guides/quick-start
-
Snackbar
演示:material-ui.com/demos/snackbars/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
-
按钮
API 文档:material-ui.com/api/button/
-
图标按钮
API 文档:material-ui.com/api/icon-button/
队列 Snackbar
在较大的 Material-UI 应用程序中,您可能会发现自己在一个很短的时间内发送了多个 Snackbar 消息。为了处理这种情况,您可以创建一个队列来处理所有 Snackbar 消息,以确保只显示最新的通知,并且正确处理过渡。
如何操作...
假设你的应用程序中有几个组件需要向用户发送 snackbar 消息。在所有地方手动渲染Snackbar
组件将会很繁琐——尤其是如果你只是想显示简单的文本 snackbar。
一种替代方法是实现一个高阶组件,它通过调用一个函数并将文本作为参数传递来包装你的组件,使其能够显示消息。然后,你可以包装任何需要 snackbar 功能的组件。以下是代码的样子:
import React, { Fragment, useState } from 'react';
import Snackbar from '@material-ui/core/Snackbar';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
const withMessage = Wrapped =>
function WithMessage(props) {
const [queue, setQueue] = useState([]);
const [open, setOpen] = useState(false);
const [message, setMessage] = useState('');
const sendMessage = msg => {
const newQueue = [...queue, msg];
if (newQueue.length === 1) {
setOpen(true);
setMessage(msg);
}
};
const onClose = () => {
setOpen(false);
};
const onExit = () => {
const [msg, ...rest] = queue;
if (msg) {
setQueue(rest);
setOpen(true);
setMessage(msg);
}
};
return (
<Fragment>
<Wrapped message={sendMessage} {...props} />
<Snackbar
key={message}
open={open}
message={message}
autoHideDuration={4000}
onClose={onClose}
onExit={onExit}
/>
</Fragment>
);
};
const QueuingSnackbars = withMessage(({ message }) => {
const [counter, setCounter] = useState(0);
const onClick = () => {
const newCounter = counter + 1;
setCounter(newCounter);
message(`Message ${newCounter}`);
};
return <Button onClick={onClick}>Message</Button>;
});
export default QueuingSnackbars;
当屏幕首次加载时,你会看到一个消息按钮。点击它将显示一个类似这样的 snackbar 消息:
再次点击消息按钮将清除当前的 snackbar,通过在屏幕上视觉上将其移除,然后再将新的 snackbar 转换到屏幕上。即使你连续快速点击按钮几次,一切都会顺利工作,你总是会看到最新的消息:
它是如何工作的...
让我们先看看QueuingSnackbars
组件,它渲染了当点击时发送消息的按钮:
const QueuingSnackbars = withMessage(({ message }) => {
const [counter, setCounter] = useState(0);
const onClick = () => {
const newCounter = counter + 1;
setCounter(newCounter);
message(`Message ${newCounter}`);
};
return <Button onClick={onClick}>Message</Button>;
});
withMessage()
包装器为组件提供了一个作为属性的message()
函数。如果你查看onClick()
处理程序,你可以在其中看到message()
函数的作用。
接下来,让我们分解withMessage()
高阶组件。我们将从标记开始,逐步向下:
<Fragment>
<Wrapped message={sendMessage} {...props} />
<Snackbar
key={message}
open={open}
message={message}
autoHideDuration={4000}
onClose={onClose}
onExit={onExit}
/>
</Fragment>
Wrapped
组件是withMessage()
被调用的组件。它传递了它通常会被传递的正常属性,加上message()
函数。旁边是Snackbar
组件。这里有两个值得注意的属性:
-
key
:这个值由Snackbar
内部使用,以确定是否正在显示新消息。它应该是一个唯一的值。 -
onExit
:当关闭的 snackbar 的转换完成时被调用。
接下来,让我们看看sendMessage()
函数:
const sendMessage = msg => {
const newQueue = [...queue, msg];
if (newQueue.length === 1) {
setOpen(true);
setMessage(msg);
}
};
当一个组件想要显示 snackbar 消息时,这个函数会被调用。它将message
字符串放入队列。如果消息是队列中唯一的项,那么open
和message
状态会立即更新。
接下来,让我们看看onClose()
函数。当 snackbar 关闭时,这个函数会被调用:
const onClose = () => {
setOpen(false);
};
这个函数的唯一任务就是确保打开状态为 false。
最后,让我们看看当 snackbar 完成其退出转换时被调用的onExit()
函数:
const onExit = () => {
const [msg, ...rest] = queue;
if (msg) {
setQueue(rest);
setOpen(true);
setMessage(msg);
}
};
队列中的第一条消息被分配给message
常量。如果有消息,它将成为活动消息状态,并且下一个 snackbar 将被打开。此时,项目也将从队列中移除。
参见
-
Snackbar
演示:material-ui.com/demos/snackbars/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
-
Button
API 文档:material-ui.com/api/button/
-
IconButton
API 文档:material-ui.com/api/icon-button/
第十章:按钮 - 启动操作
在本章中,您将学习以下主题:
-
按钮变体
-
按钮强调
-
链接按钮
-
浮动操作
-
图标按钮
-
按钮大小
简介
在 Material-UI 应用程序中,按钮用于启动操作。用户点击按钮,就会发生某些事情。按钮被激活时会发生什么完全取决于您。Material-UI 按钮的复杂度从简单的文本按钮到浮动操作按钮不等。
按钮变体
Material-UI 的 Button
组件存在三种变体之一。这些如下所示:
-
文本
-
Outlined
-
Filled
如何操作...
下面是一些渲染三个 Button
组件的代码,每个组件都明确设置了它们的 variant
属性:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
const styles = theme => ({
container: {
margin: theme.spacing(1)
}
});
const ButtonVariants = withStyles(styles)(({ classes }) => (
<Grid
container
direction="column"
spacing={2}
className={classes.container}
>
<Grid item>
<Button variant="text">Text</Button>
</Grid>
<Grid item>
<Button variant="outlined">Outlined</Button>
</Grid>
<Grid item>
<Button variant="contained">Contained</Button>
</Grid>
</Grid>
));
export default ButtonVariants;
当您加载屏幕时,您将看到以下内容:
它是如何工作的...
variant
属性控制渲染的按钮类型。这三个变体可以根据您的需要用于不同的场景或上下文。例如,如果这是您需要的,TEXT 按钮的注意力较少。相反,CONTAINED 按钮试图成为用户明显的交互点。
默认变体是 text
。我发现当您明确包含变体时,Button
标记更容易阅读。这样,您或任何阅读代码的人都不必记住默认的 variant
是什么。
相关内容
-
Button
示例:material-ui.com/demos/buttons/
-
Button
API 文档:material-ui.com/api/button/
按钮强调
Button
的 color
和 disabled
属性允许您控制按钮相对于其周围环境的强调程度。例如,您可以指定按钮应使用 primary
颜色值。按钮的强调是 variant
和 color
属性累积的结果。您可以调整这两个属性,直到按钮具有适当的强调。
没有正确的强调级别。请使用适合您应用程序上下文的内容。
如何操作...
下面是一些显示您可以应用于 Button
组件的不同颜色值的代码:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
container: {
margin: theme.spacing(1)
}
});
const ButtonEmphasis = withStyles(styles)(({ classes, disabled }) => (
<Grid
container
direction="column"
spacing={16}
className={classes.container}
>
<Grid item>
<Typography variant="h6">Default</Typography>
</Grid>
<Grid item>
<Grid container spacing={16}>
<Grid item>
<Button variant="text" disabled={disabled}>
Text
</Button>
</Grid>
<Grid item>
<Button variant="outlined" disabled={disabled}>
Outlined
</Button>
</Grid>
<Grid item>
<Button variant="contained" disabled={disabled}>
Contained
</Button>
</Grid>
</Grid>
</Grid>
<Grid item>
<Typography variant="h6">Primary</Typography>
</Grid>
<Grid item>
<Grid container spacing={16}>
<Grid item>
<Button variant="text" color="primary" disabled={disabled}>
Text
</Button>
</Grid>
<Grid item>
<Button
variant="outlined"
color="primary"
disabled={disabled}
>
Outlined
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
color="primary"
disabled={disabled}
>
Contained
</Button>
</Grid>
</Grid>
</Grid>
<Grid item>
<Typography variant="h6">Secondary</Typography>
</Grid>
<Grid item>
<Grid container spacing={16}>
<Grid item>
<Button
variant="text"
color="secondary"
disabled={disabled}
>
Text
</Button>
</Grid>
<Grid item>
<Button
variant="outlined"
color="secondary"
disabled={disabled}
>
Outlined
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
color="secondary"
disabled={disabled}
>
Contained
</Button>
</Grid>
</Grid>
</Grid>
</Grid>
));
export default ButtonEmphasis;
当屏幕首次加载时,您将看到以下内容:
如果 disabled
属性为 true
,您将看到以下内容:
它是如何工作的...
此示例旨在说明 variant
和 color
属性的组合结果。或者,您可以完全禁用按钮,同时仍然控制其强调方面的 variant
(color
属性对禁用按钮没有影响)。
最强调到最不强调的 variant
值的顺序如下:
-
filled
-
概述
-
text
最强调到最不强调的 color
值的顺序如下:
-
primary
-
secondary
-
默认
通过结合这两个属性值,你可以控制按钮的强调。有时,你确实需要一个按钮非常突出,因此你可以将contained
和primary
结合使用:
如果你希望你的按钮完全不突出,你可以将text
变体与default
颜色结合使用:
更多内容...
如果你的按钮放置在另一个 Material-UI 组件中,确保正确的颜色选择可能会很困难。例如,假设你有一个AppBar
组件中的按钮,如下所示:
<AppBar color={appBarColor}>
<Toolbar>
<Grid container spacing={16}>
<Grid item>
<Button variant="text" disabled={disabled}>
Text
</Button>
</Grid>
<Grid item>
<Button variant="outlined" disabled={disabled}>
Outlined
</Button>
</Grid>
<Grid item>
<Button variant="contained" disabled={disabled}>
Contained
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
如果AppBar
颜色值是default
,你会看到以下内容:
这实际上看起来并不太糟糕,因为按钮本身正在使用默认颜色。但是,如果你将AppBar
颜色更改为primary
会发生什么呢:
contained
变体是唯一一个看起来几乎像是属于应用栏的按钮。让我们修改按钮,使它们都使用inherit
颜色属性值,如下所示:
<AppBar color={appBarColor}>
<Toolbar>
<Grid container spacing={16}>
<Grid item>
<Button
variant="text"
disabled={disabled}
color="inherit"
>
Text
</Button>
</Grid>
<Grid item>
<Button
variant="outlined"
disabled={disabled}
color="inherit"
>
Outlined
</Button>
</Grid>
<Grid item>
<Button
variant="contained"
disabled={disabled}
color="inherit"
>
Contained
</Button>
</Grid>
</Grid>
</Toolbar>
</AppBar>
现在,你的应用栏和按钮看起来是这样的:
文本和轮廓按钮现在看起来好多了。它们已经从其父组件继承了主题字体颜色。实际上,包含按钮现在看起来更糟,因为它正在使用继承
作为其字体颜色。这是因为当继承颜色时,包含按钮的背景颜色不会改变。因此,相反,你必须自己更改包含按钮的颜色。
让我们看看我们是否可以通过实现一个返回要使用颜色的函数来自动设置包含按钮的颜色,基于其父元素的色彩:
function buttonColor(parentColor) {
if (parentColor === 'primary') {
return 'secondary';
}
if (parentColor === 'secondary') {
return 'primary';
}
return 'default';
}
现在,当你设置你包含按钮的颜色
时,你可以使用这个函数
。只需确保你以参数的形式传递父元素的颜色
,如下所示:
<Button
variant="contained"
disabled={disabled}
color={buttonColor(appBarColor)}
>
Contained
</Button>
现在,如果你将应用栏颜色更改为primary
,你的按钮看起来是这样的:
如果你将应用栏颜色更改为secondary
,你的按钮看起来是这样的:
快速回顾:TEXT 和 OUTLINED 按钮可以安全地使用inherit
作为颜色。如果你正在处理包含按钮,你需要采取额外步骤来使用正确的颜色,就像你使用buttonColor()
函数所做的那样。
参见
-
Button
示例:material-ui.com/demos/buttons/
-
Button
API 文档:material-ui.com/api/button/
-
AppBar
API 文档:material-ui.com/api/app-bar/
-
Toolbar
API 文档:material-ui.com/api/toolbar/
链接按钮
Material-UI 的 Button
组件也可以用作链接,指向应用中的其他位置。最常见的例子是将按钮用作通过 react-router
声明的路由的链接。
如何实现...
假设你的应用有三个页面,你需要三个按钮将它们链接到每个页面。随着应用的扩展,你可能还需要从任意位置链接到它们。以下是实现这一点的代码:
import React from 'react';
import { Switch, Route, Link } from 'react-router-dom';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
content: {
margin: theme.spacing(2)
}
});
const LinkButtons = withStyles(styles)(({ classes }) => (
<Grid container direction="column" className={classes.container}>
<Grid item>
<Grid container>
<Grid item>
<Button component={Link} to="/">
Home
</Button>
</Grid>
<Grid item>
<Button component={Link} to="/page1">
Page 1
</Button>
</Grid>
<Grid item>
<Button component={Link} to="/page2">
Page 2
</Button>
</Grid>
</Grid>
</Grid>
<Grid item className={classes.content}>
<Switch>
<Route
exact
path="/"
render={() => <Typography>home content</Typography>}
/>
<Route
path="/page1"
render={() => <Typography>page 1 content</Typography>}
/>
<Route
path="/page2"
render={() => <Typography>page 2 content</Typography>}
/>
</Switch>
</Grid>
</Grid>
));
export default LinkButtons;
设置此示例以运行的 Storybook 代码包括一个 BrowserRouter
组件。在你的代码中,你需要将此组件包含为任何 Route
组件的父组件。
当屏幕首次加载时,你会看到以下内容:
如果你点击“页面 2”按钮,你将被带到 /page2
,内容将相应更新:
它是如何工作的...
当你使用 react-router
作为应用的路由器时,你可以使用来自 react-router-dom
的 Link
组件来渲染链接。由于你想要渲染 Material-UI 按钮以获得一致的 Material-UI 主题和用户交互行为,你不能直接渲染 Link
组件。相反,你可以将底层的 Button
组件变成一个 Link
组件,如下所示:
<Button component={Link} to="/">
Home
</Button>
通过使用 component
属性,你可以告诉 Button
组件将样式和事件处理逻辑应用到该组件而不是默认样式。然后,任何你通常传递给 Link
的附加属性都设置在 Button
组件上——并将它们转发给 Link
。例如,to
属性不是 Button
的属性,所以它被传递给 Link
,这是它工作所必需的。
还有更多...
这个示例的一个问题是,没有视觉指示按钮链接到当前 URL。例如,当应用首次加载 /
URL 时,主页按钮应该从其他按钮中突出出来。一种方法是将 color
属性更改为 primary
,如果按钮被认为是活动的。
你可以使用来自 react-router-dom
的 NavLink
组件。这个组件允许你设置仅在链接活动时应用的样式或类名。挑战在于,你只需要在活动时更改一个简单的 Button
属性。维护活动按钮的样式似乎有点多,尤其是如果你想使你的 UI 容易主题化。
相反,你可以创建一个按钮抽象,使用 react-router
工具在活动时渲染适当的 Button
属性,如下所示:
const NavButton = ({ color, ...props }) => (
<Switch>
<Route
exact
path={props.to}
render={() => (
<Button color="primary" component={Link} {...props} />
)}
/>
<Route
path="/"
render={() => <Button component={Link} {...props} />}
/>
</Switch>
);
NavButton
组件使用Switch
和Route
组件来确定活动路由。它是通过比较传递给NavButton
的to
属性与当前 URL 来做到这一点的。如果找到匹配项,则渲染带有color
属性设置为primary
的Button
组件。否则,不指定颜色(如果Switch
中的第一个Route
不匹配,则第二个Route
匹配一切)。以下是新组件在操作中的样子:
<Grid container>
<Grid item>
<NavButton to="/">Home</NavButton>
</Grid>
<Grid item>
<NavButton to="/page1">Page 1</NavButton>
</Grid>
<Grid item>
<NavButton to="/page2">Page 2</NavButton>
</Grid>
</Grid>
这是屏幕首次加载时的样子:
因为初始 URL 是/
,并且第一个NavButton
组件有一个to
属性为/
,所以主页按钮颜色被标记为primary
。
参见
-
Button
演示:material-ui.com/demos/buttons/
-
Button
API 文档:material-ui.com/api/button/
-
React Router 指南:
reacttraining.com/react-router/web/guides/quick-start
浮动操作
你的应用程序中的某些屏幕将有一个主要操作。例如,如果你在一个列出项目的屏幕上,主要操作可能是添加新项目。如果你在一个项目详情页面上,主要操作可能是编辑项目。Material-UI 提供了一个Fab
组件(浮动操作按钮),以突出显示主要屏幕操作。
如何实现...
浮动操作按钮的常见情况是向用户展示一个带有表示要执行的操作的图标、位于屏幕右下角的圆形按钮。此外,浮动操作按钮的位置是固定
的,这意味着当用户滚动页面时,主要操作始终可见。
让我们编写一些代码来定位
一个位于屏幕右下角的浮动操作按钮,以指示添加操作,如下所示:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
const styles = theme => ({
fab: {
margin: 0,
top: 'auto',
left: 'auto',
bottom: 20,
right: 20,
position: 'fixed'
}
});
const FloatingActions = withStyles(styles)(({ classes, fabColor }) => (
<Fragment>
<Fab className={classes.fab} color={fabColor}>
<AddIcon />
</Fab>
</Fragment>
));
export default FloatingActions;
当你加载屏幕时,你会在屏幕右下角看到以下内容:
该屏幕组件有一个fabColor
属性,用于设置Fab
组件的颜色。以下是primary
颜色的样子:
最后,这是以secondary
颜色为背景的浮动操作按钮的样子:
它是如何工作的...
Fab
组件与Button
组件非常相似。实际上,你过去使用Button
来渲染浮动操作按钮,使用fab
变体。按钮的圆角样式由Fab
处理。你只需要支持图标和任何其他按钮属性,例如onClick
处理程序。此外,你可以在浮动操作按钮中包含文本。如果你这样做,你应该使用extended
变体,以便正确地样式化按钮的形状(顶部和底部是平的而不是圆角)。
还有更多...
让我们为Fab
组件创建一个小型的抽象,它应用fab
样式并使用正确的变体。由于extended
变体仅在按钮中有文本时才有用,因此你不需要每次使用时都记住设置它。如果你的应用程序既有图标又有图标加文本的浮动操作按钮,这可能会特别令人困惑。
这是实现新Fab
组件的代码:
const ExtendedFab = withStyles(styles)(({ classes, ...props }) => {
const isExtended = React.Children.toArray(props.children).find(
child => typeof child === 'string'
);
return (
<Fab
className={classes.fab}
variant={isExtended && 'extended'}
{...props}
/>
);
});
className
属性设置方式与之前相同。当isExtended
为true
时,variant
属性设置为extended
。为了找出这一点,它使用React.Children.toArray()
函数将children
属性转换为普通数组。然后,find()
方法查找任何文本元素。如果找到了一个,isExtended
将为true
,并使用extended
变体。
这是如何使用新的ExtendedFab
按钮的:
export default ({ fabColor }) => (
<ExtendedFab color={fabColor}>
Add
<AddIcon />
</ExtendedFab>
);
添加
文本放置在AddIcon
组件之前。这个ExtendedFab
组件有两个子组件,其中一个是有文本的,这意味着将使用extended
变体。以下是它的样子:
参见
-
Fab
API 文档:material-ui.com/api/fab/
图标按钮
有时,你需要一个仅是图标的按钮。这就是IconButton
组件派上用场的地方。你可以传递任何图标组件作为子组件,然后你就有了一个图标按钮。
如何做到这一点...
图标按钮在你处理受限的屏幕空间或想要直观地显示某物的切换状态时特别有用。例如,如果启用/禁用状态表示实际的麦克风,用户切换麦克风的开关可能更容易。
让我们在此基础上构建,并在应用程序中使用图标按钮实现麦克风和音量的切换控制。以下是代码:
import React, { useState } from 'react';
import IconButton from '@material-ui/core/IconButton';
import Grid from '@material-ui/core/Grid';
import MicIcon from '@material-ui/icons/Mic';
import MicOffIcon from '@material-ui/icons/MicOff';
import VolumeUpIcon from '@material-ui/icons/VolumeUp';
import VolumeOffIcon from '@material-ui/icons/VolumeOff';
export default function IconButtons({ iconColor }) {
const [mic, setMic] = useState(true);
const [volume, setVolume] = useState(true);
return (
<Grid container>
<Grid item>
<IconButton color={iconColor} onClick={() => setMic(!mic)}>
{mic ? <MicIcon /> : <MicOffIcon />}
</IconButton>
</Grid>
<Grid item>
<IconButton
color={iconColor}
onClick={() => setVolume(!volume)}
>
{volume ? <VolumeUpIcon /> : <VolumeOffIcon />}
</IconButton>
</Grid>
</Grid>
);
}
当你首次加载屏幕时,你会看到以下内容:
如果你点击两个图标按钮,你会看到以下内容:
无论麦克风或音量的状态如何,用户仍然可以有一个对项目及其状态的视觉指示。
它是如何工作的...
此屏幕的组件维护两个状态:mic
和volume
。这两个都是布尔值,用于控制IconButton
组件中显示的图标:
const [mic, setMic] = useState(true);
const [volume, setVolume] = useState(true);
然后,基于这些状态,当状态
改变时,图标会相应交换,为用户提供有用的视觉反馈:
<Grid item>
<IconButton color={iconColor} onClick={() => setMic(!mic)}>
{mic ? <MicIcon /> : <MicOffIcon />}
</IconButton>
</Grid>
<Grid item>
<IconButton
color={iconColor}
onClick={() => setVolume(!volume)}
>
{volume ? <VolumeUpIcon /> : <VolumeOffIcon />}
</IconButton>
</Grid>
此外,此屏幕的组件接受一个iconColor
属性,它可以是default
、primary
或secondary
。以下是primary
颜色的样子:
参见
-
IconButton
API 文档:material-ui.com/api/icon-button/
按钮尺寸
Material-UI 按钮支持 T 恤式尺寸。与其试图为您的按钮找到完美的尺寸,您可以使用最接近您需求的预定义尺寸之一。
如何做到这一点...
如果您需要调整按钮的大小,可以使用small
、medium
(默认值)或large
。以下是如何设置Button
组件的size
的示例:
import React from 'react';
import Button from '@material-ui/core/Button';
export default function ButtonSizes({ size, color }) {
return (
<Button variant="contained" size={size} color={color}>
Add
</Button>
);
}
下面是各种尺寸的外观:
它是如何工作的...
中等尺寸和大型尺寸之间的区别最大。使用大型按钮,结合其他Button
属性,如color
和Icons
,可以使按钮真正脱颖而出。
还有更多...
使用带按钮的 T 恤尺寸的一个缺点是,当结合文本和图标图像时。图标不会像文本一样缩放,所以按钮看起来永远不太对劲,除非使用中等默认尺寸。
让我们实现一个按钮抽象,使其更容易使用可一致调整大小的文本按钮或图标按钮。以下是代码:
import React from 'react';
import Grid from '@material-ui/core/Grid';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import Fab from '@material-ui/core/Fab';
import AddIcon from '@material-ui/icons/Add';
const MyButton = ({ fab, ...props }) => {
const [child] = React.Children.toArray(props.children);
let ButtonComponent;
if (React.isValidElement(child) && fab) {
ButtonComponent = Fab;
} else if (React.isValidElement(child)) {
ButtonComponent = IconButton;
} else {
ButtonComponent = Button;
}
return <ButtonComponent {...props} />;
};
export default function ButtonSizes({ size, color }) {
return (
<Grid container spacing={16} alignItems="center">
<Grid item>
<MyButton variant="contained" size={size} color={color}>
Add
</MyButton>
</Grid>
<Grid item>
<MyButton size={size} color={color}>
<AddIcon />
</MyButton>
</Grid>
<Grid item>
<MyButton fab size={size} color={color}>
<AddIcon />
</MyButton>
</Grid>
</Grid>
);
}
当size
属性设置为small
时,屏幕上三个按钮的外观如下:
下面是large
尺寸的外观:
让我们分析MyButton
组件中正在发生的事情。它期望一个单一的child
节点,它通过将children
属性转换为数组并将第一个元素分配给child
常量来获取:
const [child] = React.Children.toArray(props.children);
理念是根据child
元素和fab
属性渲染适当的Button
元素。以下是正确组件分配给ButtonComponent
的方式:
if (React.isValidElement(child) && fab) {
ButtonComponent = Fab;
} else if (React.isValidElement(child)) {
ButtonComponent = IconButton;
} else {
ButtonComponent = Button;
}
如果child
是一个元素且fab
属性为真,则使用Fab
组件。如果child
是一个元素且fab
为假,则使用IconButton
。否则,使用Button
。这意味着您可以将有效的图标元素或文本作为子元素传递给MyButton
。使用此组件渲染的任何按钮的大小设置都将保持一致。
参见
-
Button
演示:material-ui.com/demos/buttons/
-
Button
API 文档:material-ui.com/api/button/
-
IconButton
API 文档:material-ui.com/api/icon-button/
-
Fab
API 文档:material-ui.com/api/fab/
第十一章:文本 - 收集文本输入
在本章中,你将了解以下主题:
-
使用状态控制输入
-
占位符和辅助文本
-
验证和错误显示
-
密码字段
-
多行输入
-
输入装饰
-
输入掩码
简介
Material-UI 有一个灵活的文本输入组件,可以以多种方式使用来收集用户输入。它的用法范围从收集简单的单行文本输入到带有图标的掩码输入。
使用状态控制输入
TextField
组件可以通过 React 组件的state
来控制,就像常规 HTML 文本输入元素一样。与其他类型的表单控件一样,实际值通常是起点——随着更多功能的添加,每个表单控件的状态会变得更加复杂。
如何做到这一点...
就像任何其他文本输入元素一样,你需要为TextField
组件提供一个onChange
事件处理器来更新输入的状态。没有这个处理器,用户输入时输入的值不会改变。让我们看看一个例子,其中渲染了三个文本字段,并且它们各自由自己的状态控制:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField';
import Grid from '@material-ui/core/Grid';
const useStyles = makeStyles(theme => ({
container: { margin: theme.spacing.unit * 2 }
}));
export default function ControllingInputWithState() {
const classes = useStyles();
const [first, setFirst] = useState('');
const [second, setSecond] = useState('');
const [third, setThird] = useState('');
return (
<Grid container spacing={4} className={classes.container}>
<Grid item>
<TextField
id="first"
label="First"
value={first}
onChange={e => setFirst(e.target.value)}
/>
</Grid>
<Grid item>
<TextField
id="second"
label="Second"
value={second}
onChange={e => setSecond(e.target.value)}
/>
</Grid>
<Grid item>
<TextField
id="third"
label="Third"
value={third}
onChange={e => setThird(e.target.value)}
/>
</Grid>
</Grid>
);
}
当你首次加载屏幕时,你会看到以下内容:
如果你输入每个文本字段,你将更新屏幕上组件的状态:
它是如何工作的...
使用useState()
创建的设置函数:setFirst()
、setSecond()
和setThird()
,通过改变组件在onChange
事件中使用的状态来改变TextField
组件的值。
TextField
组件是一个方便的抽象,它建立在其他 Material-UI 组件(如FormControl
和Input
)之上。你可以通过用这些组件中的任何一个替换TextField
来达到完全相同的结果。但你会得到更多的代码来维护。
还有更多...
如果,除了只在组件状态中保留TextField
值之外,你还保留了id
和label
信息呢?将永远不会改变的价值作为状态存储可能会显得有些混乱,但权衡是你可以让状态数据驱动组件渲染的内容,而不是不得不反复重复相同的TextField
组件。
首先,让我们改变组件状态的结构,如下所示:
const [inputs, setInputs] = useState([
{ id: 'first', label: 'First', value: '' },
{ id: 'second', label: 'Second', value: '' },
{ id: 'third', label: 'Third', value: '' }
]);
与使用具有字符串属性的对象来保存文本字段值不同,inputs
状态是一个对象数组。它是一个数组,这样组件就可以在保持顺序的同时遍历值。每个对象都有渲染TextField
所需的一切。让我们看看更新的标记:
<Grid container spacing={4} className={classes.container}>
{inputs.map(input => (
<Grid item key={input.id}>
<TextField
id={input.id}
label={input.label}
value={input.value}
onChange={onChange}
/>
</Grid>
))}
</Grid>
每个Grid
项现在映射到inputs
数组中的一个元素。如果你需要添加、删除或更改这些文本字段中的任何一个,你可以通过更新状态来实现。最后,让我们看看onChange()
的实现:
const onChange = ({ target: { id, value } }) => {
const newInputs = [...inputs];
const index = inputs.findIndex(input => input.id === id);
newInputs[index] = { ...inputs[index], value };
setInputs(newInputs);
};
onChange()
函数更新数组中的一个项目,即 inputs
数组。首先,它根据文本字段的 id
找到要更新的项目的 index
。然后,它使用文本字段的值更新 value
属性。
功能与之前完全相同,但采用了更少 JSX 标记的方法。
相关内容
-
TextField
示例:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
占位符和辅助文本
至少,文本字段应该有一个标签,以便用户知道要输入什么。但仅有的标签可能会非常令人困惑——尤其是如果你在同一屏幕上有多个文本字段。为了帮助用户理解要输入什么,你可以利用 placeholder
和 helperText
,除了 label
之外。
如何做到这一点...
让我们编写一些代码,展示你可以与 TextField
组件一起使用的各种 label
、placeholder
和 helperText
配置:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField';
const styles = theme => ({
container: { margin: theme.spacing(2) }
});
const PlaceholderAndHelperText = withStyles(styles)(({ classes }) => (
<Grid container spacing={4} className={classes.container}>
<Grid item>
<TextField label="The Value" />
</Grid>
<Grid item>
<TextField placeholder="Example Value" />
</Grid>
<Grid item>
<TextField helperText="Brief explanation of the value" />
</Grid>
<Grid item>
<TextField
label="The Value"
placeholder="Example Value"
helperText="Brief explanation of the value"
/>
</Grid>
</Grid>
));
export default PlaceholderAndHelperText;
这是四个文本字段的模样:
它是如何工作的...
让我们逐一查看这些文本字段,并分析它们的优缺点。
首先,有一个只包含 label
组件的文本字段:
<TextField label="The Value" />
当你只有 label
时,它将显示在用户输入文本的位置:
当用户导航到文本字段并获得焦点时,label
缩小并移开:
下一个文本字段使用 placeholder
属性指定占位文本:
<TextField placeholder="Example Value" />
如果可能的话,placeholder
文本应向用户提供一个有效值的示例:
当用户开始输入文本时,placeholder
值消失:
下一个文本字段提供了 helperText
属性的值:
文本字段的辅助文本在静态意义上是始终可见的,即使用户开始输入也不会移动。最后,文本字段可以具有所有三个帮助用户确定应提供什么值的属性:
-
一个告诉用户值是什么的标签
-
提供示例值的占位文本
-
提供更多解释为什么需要值的辅助文本
当你结合这三个属性时,你正在增加用户理解应输入什么内容的可能性。当文本字段未获得焦点时,标签和辅助文本是可见的:
当文本字段获得焦点时,标签缩小,占位符值被揭示:
相关内容
-
TextField
示例:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
验证和错误显示
即使有辅助文本、占位符和标签,用户也难免会输入一些不太正确的东西。并不是他们试图搞砸事情(公平地说,有些人确实如此);而是错误总是会发生。当出现错误时,文本输入字段需要标记为处于错误状态。
如何做到这一点...
假设你有两个输入:一个电话号码和一个电子邮件地址,并且你想要确保用户提供的值是正确的。
请注意:验证并不完美。幸运的是,这个组件可以工作,只要你需要它,你仍然会得到所有的 Material-UI 组件。
这是实现它的代码:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
container: { margin: theme.spacing(2) }
}));
export default function ValidationAndErrorDisplay() {
const classes = useStyles();
const [inputs, setInputs] = useState([
{
id: 'phone',
label: 'Phone',
placeholder: '999-999-9999',
value: '',
error: false,
helperText: 'Any valid phone number will do',
getHelperText: error =>
error
? 'Woops. Not a valid phone number'
: 'Any valid phone number will do',
isValid: value =>
/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/.test(
value
)
},
{
id: 'email',
label: 'Email',
placeholder: 'john@acme.com',
value: '',
error: false,
helperText: 'Any valid email address will do',
getHelperText: error =>
error
? 'Woops. Not a valid email address'
: 'Any valid email address will do',
isValid: value => /\S+@\S+\.\S+/.test(value)
}
]);
const onChange = ({ target: { id, value } }) => {
const newInputs = [...inputs];
const index = inputs.findIndex(input => input.id === id);
const input = inputs[index];
const isValid = input.isValid(value);
newInputs[index] = {
...input,
value: value,
error: !isValid,
helperText: input.getHelperText(!isValid)
};
setInputs(newInputs);
};
return (
<Grid container spacing={4} className={classes.container}>
{inputs.map(input => (
<Grid item key={input.id}>
<TextField
id={input.id}
label={input.label}
placeholder={input.placeholder}
helperText={input.helperText}
value={input.value}
onChange={onChange}
error={input.error}
/>
</Grid>
))}
</Grid>
);
}
ValidationAndErrorDisplay
组件将在屏幕上渲染两个 TextField
组件。这是屏幕首次加载时的样子:
电话和电子邮件文本字段只是带有标签、辅助文本和占位符的常规文本字段。例如,当电话字段获得焦点时,它看起来像这样:
当你开始输入时,文本字段的值会与电话格式的正则表达式进行验证。以下是当字段具有无效电话号码值时的样子:
然后,一旦你有一个有效的电话号码值,文本字段的状况就会恢复到正常:
电子邮件字段的工作方式相同——唯一的区别是用于验证值格式的正则表达式。
它是如何工作的...
让我们先看看 ValidationAndErrorDisplay
组件的状态:
const [inputs, setInputs] = useState([
{
id: 'phone',
label: 'Phone',
placeholder: '999-999-9999',
value: '',
error: false,
helperText: 'Any valid phone number will do',
getHelperText: error =>
error
? 'Woops. Not a valid phone number'
: 'Any valid phone number will do',
isValid: value =>
/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/.test(
value
)
},
{
id: 'email',
label: 'Email',
placeholder: 'john@acme.com',
value: '',
error: false,
helperText: 'Any valid email address will do',
getHelperText: error =>
error
? 'Woops. Not a valid email address'
: 'Any valid email address will do',
isValid: value => /\S+@\S+\.\S+/.test(value)
}
]);
inputs
数组通过 render()
方法映射到 TextField
组件。这个数组中的每个对象都有直接映射到 TextField
组件的属性。例如,id
、label
、placeholder
——这些都是 TextField
的属性。每个对象都有两个帮助验证文本字段值的函数。首先,getHelperText()
返回默认的辅助文本,或者如果 error
参数为真,则替换辅助文本的错误文本。isValid()
函数将 value
参数与正则表达式进行验证,如果匹配则返回 true
。
接下来,让我们看看 onChange()
处理程序:
const onChange = ({ target: { id, value } }) => {
const newInputs = [...inputs];
const index = inputs.findIndex(input => input.id === id);
const input = inputs[index];
const isValid = input.isValid(value);
newInputs[index] = {
...input,
value: value,
error: !isValid,
helperText: input.getHelperText(!isValid)
};
setInputs(newInputs);
};
随着用户输入,此函数会更新给定文本字段的值状态。它还会调用 isValid()
函数,并将更新后的值传递给它。如果值无效,则将 error
状态设置为 true
。helperText
状态也会通过 getHelperText()
更新,这同样取决于值的有效性。
还有更多...
如果这个例子可以被修改,以至于您不需要将错误信息作为状态存储,或者不需要一个函数来更改文本框的辅助文本?为了做到这一点,您可以引入一个新的TextField
抽象,该抽象处理设置error
属性,并在值无效时更改helperText
组件。以下是新的组件:
const MyTextField = ({ isInvalid, ...props }) => {
const invalid = isInvalid(props.value);
return (
<TextField
{...props}
error={invalid}
helperText={invalid || props.helperText}
/>
);
};
与返回true
表示数据有效的函数不同,MyTextField
组件期望一个isInvalid()
属性,当数据有效时返回false
,当数据无效时返回错误信息。然后,error
属性可以使用这个值,这将改变文本框的颜色以指示它处于错误状态,而helperText
属性可以使用isInvalid()
函数返回的字符串,或者传递给组件的helperText
属性。
接下来,让我们看看ValidationAndErrorDisplay
组件现在使用的状态:
const [inputs, setInputs] = useState([
{
id: 'phone',
label: 'Phone',
placeholder: '999-999-9999',
value: '',
helperText: 'Any valid phone number will do',
isInvalid: value =>
value === '' ||
/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/.test(
value
)
? false
: 'Woops. Not a valid phone number'
},
{
id: 'email',
label: 'Email',
placeholder: 'john@acme.com',
value: '',
helperText: 'Any valid email address will do',
isInvalid: value =>
value === '' || /\S+@\S+\.\S+/.test(value)
? false
: 'Woops. Not a valid email address'
}
]);
输入不再需要getHelperText()
函数或error
状态。isInvalid()
函数在值无效时返回错误辅助文本。接下来,让我们看看onChange()
处理程序:
const onChange = ({ target: { id, value } }) => {
const newInputs = [...inputs];
const index = inputs.findIndex(input => input.id === id);
newInputs[index] = {
...inputs[index],
value: value
};
setInputs(newInputs);
};
现在,它不需要接触error
状态,也不必担心更新辅助文本,或者调用任何验证函数——所有这些现在都由MyTextField
处理。
参见
-
TextField
演示:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
密码字段
密码字段是一种特殊的文本输入类型,在输入时隐藏单个字符。Material-UI TextField
组件通过改变type
属性的值来支持这种类型的字段。
如何做到这一点...
这里有一个简单的例子,它将常规文本输入转换为防止在屏幕上显示值的password
输入:
import React, { useState } from 'react';
import TextField from '@material-ui/core/TextField';
export default function PasswordFields() {
const [password, setPassword] = useState('12345');
const onChange = e => {
setPassword(e.target.value);
};
return (
<TextField
type="password"
label="Password"
value={password}
onChange={onChange}
/>
);
}
这里是首次加载时的屏幕样子:
如果您更改密码字段的值,任何新的字符都将保持隐藏,尽管实际输入的值存储在PasswordFields
组件的password
状态中。
它是如何工作的...
type
属性告诉TextField
组件使用密码 HTML input
元素。这就是为什么用户在输入时值保持隐藏,或者如果字段预先填充了密码值。有时,密码字段可以被自动填充。
还有更多...
您可以使用autoComplete
属性来控制浏览器如何自动填充密码值。这个值的一个常见用例是在用户名字段填写后,自动在登录屏幕上填充密码字段。以下是一个在屏幕上具有用户名和密码字段时如何使用此属性的示例:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
container: { margin: theme.spacing(2) }
}));
export default function PasswordFields() {
const classes = useStyles();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
return (
<Grid container spacing={4} className={classes.container}>
<Grid item>
<TextField
id="username"
label="Username"
autoComplete="username"
InputProps={{ name: 'username' }}
value={username}
onChange={e => setUsername(e.target.value)}
/>
</Grid>
<Grid item>
<TextField
id="password"
type="password"
label="Password"
autoComplete="current-password"
value={password}
onChange={e => setPassword(e.target.value)}
/>
</Grid>
</Grid>
);
}
第一个 TextField
组件使用了 autoComplete
的 username
值。它还向 InputProps
传递了 { name: 'username' }
,以便在 <input>
元素上设置 name
属性。你需要这样做的原因是,在第二个 TextField
组件中,autoComplete
的 current-password
值告诉浏览器根据 username
字段值查找密码。
并非所有浏览器都实现了这一功能。为了使任何凭据能够自动填充到文本字段中,它们必须使用原生的浏览器凭据记住工具保存。
参见
-
TextField
演示:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
多行输入
对于某些字段,用户需要提供多行文本值的能力。multiline
属性有助于实现这一目标。
如何实现...
假设你有一个可能需要多行文本的字段,由用户提供。您可以指定 multiline
属性以允许这样做:
import React, { useState } from 'react';
import TextField from '@material-ui/core/TextField';
export default function MultilineInput() {
const [multiline, setMultiline] = useState('');
return (
<TextField
multiline
value={multiline}
onChange={e => setMultiline(e.target.value)}
/>
);
}
当屏幕首次加载时,文本字段看起来像一个普通字段,因为它默认只有一行:
你可以在这个文本字段中输入你需要的任意多行。新行通过按下 Enter 键开始:
它是如何工作的...
multiline
布尔属性用于向 TextField
组件指示该字段需要 multiline
支持。在先前的示例中,如果你计划在一个拥挤的空间中使用 multiline
输入,比如屏幕上有许多其他字段或是在对话框中,你可能会遇到一些问题:
-
当用户按下 Enter 键时,字段的高度会改变,向组件添加更多行。这可能会引起布局问题,因为其他元素会被移动。
-
如果字段从一行开始并且看起来像常规的单行文本输入,那么用户可能不会意识到他们可以在字段中输入多行文本。
还有更多...
为了帮助防止动态大小的 multiline
文本字段可能引起的问题,您可以指定 multiline
文本字段使用的行数。以下是如何使用 rows
属性的示例:
<TextField
multiline
rows={5}
label="Address"
value={multiline}
onChange={e => setMultiline(e.target.value)}
/>
现在,文本字段将正好有五行:
如果用户输入超过五行的文本,将显示垂直滚动条——文本的高度不会改变,并且不会影响其他周围组件的布局。您可以通过使用 rowsMax
属性而不是 rows
来对 TextField
组件施加相同类型的高度限制。区别在于文本字段将从一个行开始,并在用户添加新行时增长。但如果将 rowsMax
设置为 5
,文本字段将不会超过五行。
参见
-
TextField
演示:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
输入装饰
Material-UI Input
组件具有允许你自定义其外观和行为属性的属性。想法是你可以用其他 Material-UI 组件装饰输入,以扩展基本文本输入的功能,使其对应用用户有意义。
如何实现...
假设你的应用有几个屏幕,这些屏幕都有密码输入。你的应用用户喜欢在输入密码时能够看到密码。默认情况下,值将被隐藏,但如果输入组件本身有一个切换值可见性的按钮,这将使你的用户感到高兴。
这里是一个示例,展示了一个通用的组件,它将为密码字段添加一个可见性切换按钮:
import React, { useState } from 'react';
import TextField from '@material-ui/core/TextField';
import IconButton from '@material-ui/core/IconButton';
import InputAdornment from '@material-ui/core/InputAdornment';
import VisibilityIcon from '@material-ui/icons/Visibility';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
function PasswordField() {
const [visible, setVisible] = useState(false);
const toggleVisibility = () => {
setVisible(!visible);
};
return (
<TextField
type={visible ? 'text' : 'password'}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={toggleVisibility}>
{visible ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
)
}}
/>
);
}
export default function InputAdornments() {
const [password, setPassword] = useState('');
return (
<PasswordField
value={password}
onChange={e => setPassword(e.target.value)}
/>
);
}
如果你开始输入而不点击切换可见性按钮,你会看到这样的效果:
如果我们点击切换可见性按钮,密码字段看起来是这样的:
它是如何工作的...
让我们更仔细地看看 PasswordField
组件:
function PasswordField() {
const [visible, setVisible] = useState(false);
const toggleVisibility = () => {
setVisible(!visible);
};
return (
<TextField
type={visible ? 'text' : 'password'}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton onClick={toggleVisibility}>
{visible ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
</InputAdornment>
)
}}
/>
);
}
这个组件维护一个名为 visible
的状态。PasswordField
维护这个状态而不是父组件的原因是关注点分离原则。例如,父组件可能需要访问密码字段的值。这个值作为属性传递到 PasswordField
。然而,只有 PasswordField
关心 visibility
状态。因此,通过将其封装在这个组件中,你简化了任何使用 PasswordField
的代码。
这个抽象的另一个有价值的方面是装饰本身。type
属性随着 visible
状态的变化而变化——这是显示或隐藏密码值的机制。endAdornment
属性传递给 TextField
渲染的 Input
组件,通过 InputProps
传递。这就是你如何向字段添加组件的方式。在这个例子中,你正在向输入的右侧(末端)添加一个图标按钮。这里的图标根据可见性状态变化,并且当点击时,会调用 toggleVisible()
方法来实际改变可见性状态。
还有更多...
你可以使用输入装饰来做的不仅仅是显示密码字段的值按钮。例如,在一个需要验证的字段中,你可以使用输入装饰来帮助可视化字段的状态。假设你需要验证用户输入的电子邮件字段。你可以创建一个组件形式的抽象,根据用户提供的验证结果改变组件的颜色和装饰。这个组件看起来是这样的:
const ValidationField = props => {
const { isValid, ...rest } = props;
const empty = props.value === '';
const valid = isValid(props.value);
let startAdornment;
if (empty) {
startAdornment = null;
} else if (valid) {
startAdornment = (
<InputAdornment position="start">
<CheckCircleIcon color="primary" />
</InputAdornment>
);
} else {
startAdornment = (
<InputAdornment position="start">
<ErrorIcon color="error" />
</InputAdornment>
);
}
return (
<TextField
{...rest}
error={!empty && !valid}
InputProps={{ startAdornment }}
/>
);
};
ValidationField
的想法是获取一个 isValid()
函数属性,并使用它来测试值属性。如果它返回 true
,则 startAdornment
是一个勾选标记。如果 isValid()
返回 false
,则 startAdornment
是一个红色的 x。以下是组件的使用方法:
<ValidationField
label="Email"
value={this.state.email}
onChange={this.onEmailChange}
isValid={v => /\S+@\S+\.\S+/.test(v)}
/>
ValidationField
组件几乎可以与 TextField
一样使用。唯一的增加是 isValid
属性。任何状态都在 ValidationField
之外处理,这意味着 isValid()
会在值更改时被调用,并将更新组件的外观以反映数据的有效性。作为额外的奖励:你实际上不需要在任何地方存储任何类型的错误状态,因为 ValidationField
从值和 isValid
属性中推导出它所需的一切。
这是带有无效电子邮件地址的字段看起来像什么:
这是带有有效电子邮件地址的字段看起来像什么:
参见
-
TextField
演示:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
-
IconButton
API 文档:material-ui.com/api/icon-button/
-
InputAdornment
API 文档:material-ui.com/api/input-adornment/
输入掩码
一些文本输入需要具有特定格式的值。使用 Material-UI TextField
组件,你可以添加掩码功能,这有助于引导用户提供正确的格式。
如何操作...
假设你有一个电话号码和电子邮件字段,并且你想要为每个字段提供一个输入掩码。以下是你可以如何使用来自 react-text-mask
的 MaskedInput
组件与 TextField
组件一起添加掩码功能:
import React, { Fragment, useState } from 'react';
import MaskedInput from 'react-text-mask';
import emailMask from 'text-mask-addons/dist/emailMask';
import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
input: { margin: theme.spacing.unit * 3 }
}));
const PhoneInput = ({ inputRef, ...props }) => (
<MaskedInput
{...props}
ref={ref => {
inputRef(ref ? ref.inputElement : null);
}}
mask={[
'(',
/[1-9]/,
/\d/,
/\d/,
')',
' ',
/\d/,
/\d/,
/\d/,
'-',
/\d/,
/\d/,
/\d/,
/\d/
]}
placeholderChar={'\u2000'}
/>
);
const EmailInput = ({ inputRef, ...props }) => (
<MaskedInput
{...props}
ref={ref => {
inputRef(ref ? ref.inputElement : null);
}}
mask={emailMask}
placeholderChar={'\u2000'}
/>
);
export default function InputMasking() {
const classes = useStyles();
const [phone, setPhone] = useState('');
const [email, setEmail] = useState('');
return (
<Fragment>
<TextField
label="Phone"
className={classes.input}
value={phone}
onChange={e => setPhone(e.target.value)}
InputProps={{ inputComponent: PhoneInput }}
/>
<TextField
label="Email"
className={classes.input}
value={email}
onChange={e => setEmail(e.target.value)}
InputProps={{ inputComponent: EmailInput }}
/>
</Fragment>
);
}
这是屏幕首次加载时的样子:
当你开始在电话字段中输入值时,格式掩码就会出现:
这就是完成后的值看起来像什么——用户永远不需要输入 (
,)
或 -
:
这就是完成后的电子邮件值看起来像什么:
在电子邮件输入中,用户实际上必须输入 @
和 .
,因为掩码不知道电子邮件地址的任何部分有多少个字符。然而,它确实阻止用户将这两个字符放在错误的位置。
它是如何工作的...
为了使这起作用,你创建了一个 PhoneInput
组件和一个 EmailInput
组件。每个组件的想法是围绕 MaskedInput
组件提供基本的抽象。让我们更详细地看看每个组件,从 PhoneInput
开始:
const PhoneInput = ({ inputRef, ...props }) => (
<MaskedInput
{...props}
ref={ref => {
inputRef(ref ? ref.inputElement : null);
}}
mask={[
'(',
/[1-9]/,
/\d/,
/\d/,
')',
' ',
/\d/,
/\d/,
/\d/,
'-',
/\d/,
/\d/,
/\d/,
/\d/
]}
placeholderChar={'\u2000'}
/>
);
传递给PhoneInput
的属性大部分被转发到MaskedInput
。由于名称不同,ref
属性需要显式设置。placeholder
属性被设置为空白。mask
属性是最重要的——这是用户在开始输入时看到的模式。传递给mask
的值是一个包含正则表达式和字符串字符的数组。字符串字符是用户开始输入时显示的内容——在电话号码的情况下,这些是(
、)
和-
字符。正则表达式是与用户输入匹配的动态部分。对于电话号码,任何数字都可以,但不允许符号和字母。
现在我们来看看EmailInput
组件:
const EmailInput = ({ inputRef, ...props }) => (
<MaskedInput
{...props}
ref={ref => {
inputRef(ref ? ref.inputElement : null);
}}
mask={emailMask}
placeholderChar={'\u2000'}
/>
);
这与PhoneInput
采用相同的方法。主要区别在于,不是传递一个字符串数组和正则表达式,而是使用从react-text-mask
导入的emailMask
函数。
现在你有了这两个掩码输入,你可以通过将它们传递给inputComponent
属性来使用它们:
<TextField
label="Phone"
className={classes.input}
value={phone}
onChange={e => setPhone(e.target.value)}
InputProps={{ inputComponent: PhoneInput }}
/>
<TextField
label="Email"
className={classes.input}
value={email}
onChange={e => setEmail(e.target.value)}
InputProps={{ inputComponent: EmailInput }}
/>
参见
-
TextField
演示:material-ui.com/demos/text-fields/
-
TextField
API 文档:material-ui.com/api/text-field/
-
React 文本掩码:
github.com/text-mask/text-mask
第十二章:自动完成和芯片 - 多个项目的文本输入建议
在本章中,你将学习以下主题:
-
构建自动完成组件
-
选择自动完成建议
-
API 驱动的自动完成
-
突出显示搜索结果
-
独立芯片输入
简介
当有太多选择时,Web 应用程序通常会提供自动完成输入字段。自动完成字段类似于文本输入字段——当用户开始输入时,他们会根据输入的内容得到一个更小的选择列表。一旦用户准备好进行选择,实际的输入将被称为 Chips
的组件填充——特别是当用户需要能够进行多项选择时。
构建自动完成组件
Material-UI 并不实际包含一个 Autocomplete
组件。原因是,在 React 生态系统中已经存在许多不同的自动完成选择组件的实现,因此再提供一个没有意义。相反,你可以选择现有的实现,并用 Material-UI 组件增强它,以便它可以很好地与你的 Material-UI 应用程序集成。
如何做到这一点...
假设你有一个用于选择曲棍球队的选择器。但是,由于球队太多,无法合理地放入简单的选择组件中——你需要自动完成功能。你可以使用来自 react-select
包的 Select
组件来提供所需的自动完成功能。你可以使用 Select
属性来替换关键自动完成组件,以便自动完成与你的应用程序的其他部分保持一致的外观和感觉。
让我们创建一个可重用的 Autocomplete
组件。Select
组件允许你替换自动完成体验的某些方面。特别是,以下是你将替换的组件:
-
控制
:要使用的文本输入组件 -
菜单
:当用户开始输入时显示带有建议的菜单 -
NoOptionsMessage
:当没有建议显示时显示的消息 -
Option
:用于菜单
中每个建议的组件 -
Placeholder
:文本输入的占位文本组件 -
SingleValue
:用于显示已选择的值的组件 -
ValueContainer
:包装SingleValue
的组件 -
IndicatorSeparator
:分隔自动完成右侧的按钮 -
ClearIndicator
:用于清除当前值的按钮的组件 -
DropdownIndicator
:用于显示菜单
的按钮的组件
这些组件中的每一个都将被替换为改变自动完成外观和感觉的 Material-UI 组件。此外,你将拥有所有这些作为新的 Autocomplete
组件,你可以在整个应用程序中重用它们。
在深入研究每个替换组件的实现之前,让我们看看结果。以下是在屏幕首次加载时你会看到的内容:
如果你点击向下箭头,你会看到一个包含所有值的菜单,如下所示:
尝试在自动完成文本字段中输入 tor,如下所示:
如果你进行选择,菜单将关闭,文本字段将填充所选值,如下所示:
你可以通过打开菜单并选择另一个值来更改你的选择,或者你可以通过点击文本右侧的清除按钮来清除选择。
它是如何工作的...
让我们通过查看组成 Autocomplete
组件的各个组件以及替换 Select
组件的部分来分解源代码。然后,我们将查看最终的 Autocomplete
组件。
文本输入控件
这是 Control
组件的源代码:
const inputComponent = ({ inputRef, ...props }) => (
<div ref={inputRef} {...props} />
);
const Control = props => (
<TextField
fullWidth
InputProps={{
inputComponent,
inputProps: {
className: props.selectProps.classes.input,
inputRef: props.innerRef,
children: props.children,
...props.innerProps
}
}}
{...props.selectProps.textFieldProps}
/>
);
inputComponent()
函数是一个组件,它将 inputRef
值(对底层输入元素的引用)传递给 ref
属性。然后,inputComponent
传递给 InputProps
以设置 TextField
使用的输入组件。这个组件有点令人困惑,因为它在传递引用并使用一个 helper
组件来完成这个目的。重要的是要记住,Control
的任务是设置 Select
组件以使用 Material-UI TextField
组件。
选项菜单
这是当用户开始输入或点击向下箭头时显示自动完成选项的组件:
const Menu = props => (
<Paper
square
className={props.selectProps.classes.paper}
{...props.innerProps}
>
{props.children}
</Paper>
);
Menu
组件渲染一个 Material-UI Paper
组件,以便围绕选项的元素相应地主题化。
没有可用的选项
这是 NoOptionsMessage
组件。当没有自动完成选项可以显示时,它会被渲染,如下所示:
const NoOptionsMessage = props => (
<Typography
color="textSecondary"
className={props.selectProps.classes.noOptionsMessage}
{...props.innerProps}
>
{props.children}
</Typography>
);
这会渲染一个具有 textSecondary
作为 color
属性值的 Typography
组件。
单个选项
在自动完成菜单中显示的单独选项使用 MenuItem
组件渲染,如下所示:
const Option = props => (
<MenuItem
buttonRef={props.innerRef}
selected={props.isFocused}
component="div"
style={{
fontWeight: props.isSelected ? 500 : 400
}}
{...props.innerProps}
>
{props.children}
</MenuItem>
);
selected
和 style
属性根据 isSelected
和 isFocused
属性改变项目显示的方式。children
属性设置项目的值。
占位文本
Autocomplete
组件的 Placeholder
文本在用户输入任何内容或进行选择之前显示,如下所示:
const Placeholder = props => (
<Typography
color="textSecondary"
className={props.selectProps.classes.placeholder}
{...props.innerProps}
>
{props.children}
</Typography>
);
Material-UI Typography
组件用于主题化 Placeholder
文本。
SingleValue
再次强调,Material-UI Typography
组件用于在自动完成输入中渲染来自菜单的选中值,如下所示:
const SingleValue = props => (
<Typography
className={props.selectProps.classes.singleValue}
{...props.innerProps}
>
{props.children}
</Typography>
);
ValueContainer
使用 ValueContainer
组件将 SingleValue
组件包裹在一个 div
元素和 valueContainer
CSS 类中,如下所示:
const ValueContainer = props => (
<div className={props.selectProps.classes.valueContainer}>
{props.children}
</div>
);
IndicatorSeparator
默认情况下,Select
组件使用管道字符作为自动完成菜单右侧按钮之间的分隔符。由于它们将被 Material-UI 按钮组件替换,因此这个分隔符不再必要,如下所示:
const IndicatorSeparator = () => null;
通过让组件返回 null
,不渲染任何内容。
清除选项指示器
此按钮用于清除用户之前所做的任何选择,如下所示:
const ClearIndicator = props => (
<IconButton {...props.innerProps}>
<CancelIcon />
</IconButton>
);
此组件的目的是使用 Material-UI 的 IconButton
组件并渲染 Material-UI 图标。点击处理程序通过 innerProps
传入。
显示菜单指示器
就像 ClearIndicator
组件一样,DropdownIndicator
组件用 Material-UI 的图标替换了显示自动完成菜单的按钮,如下所示:
const DropdownIndicator = props => (
<IconButton {...props.innerProps}>
<ArrowDropDownIcon />
</IconButton>
);
样式
这里是自动完成各个子组件使用的样式:
const useStyles = makeStyles(theme => ({
root: {
flexGrow: 1,
height: 250
},
input: {
display: 'flex',
padding: 0
},
valueContainer: {
display: 'flex',
flexWrap: 'wrap',
flex: 1,
alignItems: 'center',
overflow: 'hidden'
},
noOptionsMessage: {
padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`
},
singleValue: {
fontSize: 16
},
placeholder: {
position: 'absolute',
left: 2,
fontSize: 16
},
paper: {
position: 'absolute',
zIndex: 1,
marginTop: theme.spacing(1),
left: 0,
right: 0
}
}));
自动完成
最后,以下是在整个应用程序中可以重用的 Autocomplete
组件:
export default function Autocomplete(props) {
const classes = useStyles();
const [value, setValue] = useState(null);
return (
<div className={classes.root}>
<Select
value={value}
onChange={v => setValue(v)}
textFieldProps={{
label: 'Team',
InputLabelProps: {
shrink: true
}
}}
{...{ ...props, classes }}
/>
</div>
);
}
Autocomplete.defaultProps = {
isClearable: true,
components: {
Control,
Menu,
NoOptionsMessage,
Option,
Placeholder,
SingleValue,
ValueContainer,
IndicatorSeparator,
ClearIndicator,
DropdownIndicator
},
options: [
{ label: 'Boston Bruins', value: 'BOS' },
{ label: 'Buffalo Sabres', value: 'BUF' },
{ label: 'Detroit Red Wings', value: 'DET' },
{ label: 'Florida Panthers', value: 'FLA' },
{ label: 'Montreal Canadiens', value: 'MTL' },
{ label: 'Ottawa Senators', value: 'OTT' },
{ label: 'Tampa Bay Lightning', value: 'TBL' },
{ label: 'Toronto Maple Leafs', value: 'TOR' },
{ label: 'Carolina Hurricanes', value: 'CAR' },
{ label: 'Columbus Blue Jackets', value: 'CBJ' },
{ label: 'New Jersey Devils', value: 'NJD' },
{ label: 'New York Islanders', value: 'NYI' },
{ label: 'New York Rangers', value: 'NYR' },
{ label: 'Philadelphia Flyers', value: 'PHI' },
{ label: 'Pittsburgh Penguins', value: 'PIT' },
{ label: 'Washington Capitals', value: 'WSH' },
{ label: 'Chicago Blackhawks', value: 'CHI' },
{ label: 'Colorado Avalanche', value: 'COL' },
{ label: 'Dallas Stars', value: 'DAL' },
{ label: 'Minnesota Wild', value: 'MIN' },
{ label: 'Nashville Predators', value: 'NSH' },
{ label: 'St. Louis Blues', value: 'STL' },
{ label: 'Winnipeg Jets', value: 'WPG' },
{ label: 'Anaheim Ducks', value: 'ANA' },
{ label: 'Arizona Coyotes', value: 'ARI' },
{ label: 'Calgary Flames', value: 'CGY' },
{ label: 'Edmonton Oilers', value: 'EDM' },
{ label: 'Los Angeles Kings', value: 'LAK' },
{ label: 'San Jose Sharks', value: 'SJS' },
{ label: 'Vancouver Canucks', value: 'VAN' },
{ label: 'Vegas Golden Knights', value: 'VGK' }
]
};
将所有上述组件联系在一起的是传递给 Select
的 components
属性。实际上,它在 Autocomplete
中被设置为 default
属性,因此可以进一步覆盖。传递给 components
的值是一个简单的对象,将组件名称映射到其实例。
相关内容
-
React 的
Select
组件:react-select.com/
-
Autocomplete
示例:material-ui.com/demos/autocomplete/
-
TextField
API 文档:material-ui.com/api/text-field/
-
Typography
API 文档:material-ui.com/api/typography/
-
Paper
API 文档:material-ui.com/api/paper/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
IconButton
API 文档:material-ui.com/api/icon-button/
选择自动完成建议
在上一节中,你构建了一个能够选择单个值的 Autocomplete
组件。有时,你需要从 Autocomplete
组件中选择多个值。好消息是,通过一些小的添加,上一节中创建的组件已经完成了大部分工作。
如何操作...
让我们逐一查看需要添加以支持 Autocomplete
组件中多值选择的修改,从新的 MultiValue
组件开始,如下所示:
const MultiValue = props => (
<Chip
tabIndex={-1}
label={props.children}
className={clsx(props.selectProps.classes.chip, {
[props.selectProps.classes.chipFocused]: props.isFocused
})}
onDelete={props.removeProps.onClick}
deleteIcon={<CancelIcon {...props.removeProps} />}
/>
);
MultiValue
组件使用 Material-UI 的 Chip
组件来渲染选定的值。为了将 MultiValue
传递给 Select
,请将其添加到传递给 Select
的 components
对象中:
components: {
Control,
Menu,
NoOptionsMessage,
Option,
Placeholder,
SingleValue,
MultiValue,
ValueContainer,
IndicatorSeparator,
ClearIndicator,
DropdownIndicator
},
现在,你可以使用你的 Autocomplete
组件进行单值选择,或者进行多值选择。你可以在 defaultProps
中添加 isMulti
属性,默认值为 true
,如下所示:
isMulti: true,
现在,你应该能够从自动完成中选择多个值。
它是如何工作的...
当首次渲染或显示菜单时,自动完成看起来并没有什么不同。当你做出选择时,Chip
组件用于显示值。Chips 非常适合显示此类小块信息。此外,关闭按钮与它很好地集成,使用户在做出选择后很容易移除单个选择。
这是多次选择后自动完成的样子:
已选择的价值将从菜单中移除。
参见
-
React 的选择组件:
react-select.com/
-
Autocomplete
演示:material-ui.com/demos/autocomplete/
-
TextField
API 文档:material-ui.com/api/text-field/
-
Typography
API 文档:material-ui.com/api/typography/
-
Paper
API 文档:material-ui.com/api/paper/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
IconButton
API 文档:material-ui.com/api/icon-button/
-
Chip
API 文档:material-ui.com/api/chip/
API 驱动的自动完成
你并不总是能在页面初始加载时准备好自动完成的数据。想象一下在用户能够与任何东西交互之前尝试加载数百或数千个项目。更好的方法是保持数据在服务器上,并提供一个带有自动完成文本的 API 端点。然后你只需要加载 API 返回的较小数据集。
如何实现...
让我们重新整理前一个示例。我们将保留所有相同的自动完成功能,除了,我们不会将数组传递给 options
属性,而是传递一个返回 Promise
的 API 函数。以下是一个模拟 API 调用并解决 Promise
的 API 函数:
const someAPI = searchText =>
new Promise(resolve => {
setTimeout(() => {
const teams = [
{ label: 'Boston Bruins', value: 'BOS' },
{ label: 'Buffalo Sabres', value: 'BUF' },
{ label: 'Detroit Red Wings', value: 'DET' },
...
];
resolve(
teams.filter(
team =>
searchText &&
team.label
.toLowerCase()
.includes(searchText.toLowerCase())
)
);
}, 1000);
});
此函数接受一个搜索字符串参数,并返回一个 Promise
。这里过滤的是本应传递给 Select
组件 options
属性的相同数据。将此函数中发生的任何操作视为在真实应用程序中的 API 后面发生。然后,返回的 Promise
在模拟的 1 秒延迟后解决为匹配项的数组。
你还需要将几个组件添加到 Select
组件的组成中(现在我们有 13 个了),如下所示:
const LoadingIndicator = () => <CircularProgress size={20} />;
const LoadingMessage = props => (
<Typography
color="textSecondary"
className={props.selectProps.classes.noOptionsMessage}
{...props.innerProps}
>
{props.children}
</Typography>
);
LoadingIndicator
组件显示在自动完成文本输入的右侧。它使用 Material-UI 中的CircularProgress
组件来指示自动完成正在执行某些操作。LoadingMessage
组件与示例中用于Select
的其他文本替换组件遵循相同的模式。当菜单显示时,会显示加载文本,但解析options
的Promise
仍然挂起。
最后,还有Select
组件。您需要使用AsyncSelect
版本而不是Select
,如下所示:
import AsyncSelect from 'react-select/lib/Async';
否则,AsyncSelect
与Select
的工作方式相同,如下所示:
<AsyncSelect
value={value}
onChange={value => setValue(value)}
textFieldProps={{
label: 'Team',
InputLabelProps: {
shrink: true
}
}}
{...{ ...props, classes }}
/>
它是如何工作的...
Select
自动完成和AsyncSelect
自动完成的唯一区别在于 API 请求挂起时发生的情况。以下是发生这种情况时自动完成的外观:
随着用户输入,CircularProgress
组件将在右侧渲染,同时使用Typography
组件在菜单中渲染加载消息。
另请参阅
-
React 的 Select 组件:
react-select.com/
-
Autocomplete
演示:material-ui.com/demos/autocomplete/
-
TextField
API 文档:material-ui.com/api/text-field/
-
Typography
API 文档:material-ui.com/api/typography/
-
Paper
API 文档:material-ui.com/api/paper/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
IconButton
API 文档:material-ui.com/api/icon-button/
-
Chip
API 文档:material-ui.com/api/chip/
突出显示搜索结果
当用户在自动完成中开始输入并在下拉菜单中显示结果时,并不总是明显地知道某个项目是如何与搜索条件匹配的。您可以通过突出显示字符串值的匹配部分来帮助用户更好地理解结果。
如何实现...
您将需要使用autosuggest-highlight
包中的两个函数来帮助突出显示
自动完成下拉菜单中呈现的文本,如下所示:
import match from 'autosuggest-highlight/match';
import parse from 'autosuggest-highlight/parse';
现在,您可以构建一个新的组件,该组件将渲染项目文本,并在必要时进行突出显示,如下所示:
const ValueLabel = ({ label, search }) => {
const matches = match(label, search);
const parts = parse(label, matches);
return parts.map((part, index) =>
part.highlight ? (
<span key={index} style={{ fontWeight: 500 }}>
{part.text}
</span>
) : (
<span key={index}>{part.text}</span>
)
);
};
最终结果是ValueLabel
渲染一个由parse()
和match()
函数确定的span
元素数组。如果part.highlight
为 true,则其中一个 span 将被加粗。现在,您可以在Option
组件中使用ValueLabel
,如下所示:
const Option = props => (
<MenuItem
buttonRef={props.innerRef}
selected={props.isFocused}
component="div"
style={{
fontWeight: props.isSelected ? 500 : 400
}}
{...props.innerProps}
>
<ValueLabel
label={props.children}
search={props.selectProps.inputValue}
/>
</MenuItem>
);
它是如何工作的...
现在,当您在自动完成的文本输入中搜索值时,结果将突出显示每个项目中的搜索条件,如下所示:
另请参阅
-
React 的自动建议:
github.com/moroshko/autosuggest-highlight
-
React 的
Select
组件:react-select.com/
-
Autocomplete
示例:material-ui.com/demos/autocomplete/
-
TextField
API 文档:material-ui.com/api/text-field/
-
Typography
API 文档:material-ui.com/api/typography/
-
Paper
API 文档:material-ui.com/api/paper/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
IconButton
API 文档:material-ui.com/api/icon-button/
-
Chip
API 文档:material-ui.com/api/chip/
独立的芯片输入
一些应用程序需要多值输入但没有为用户预定义选择列表。这排除了使用自动完成或 select
组件的可能性,例如,如果您要求用户提供姓名列表。
如何实现...
您可以安装 material-ui-chip-input
包并使用 ChipInput
组件,该组件将 Material-UI 中的 Chip
和 TextInput
组件结合在一起。代码如下:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import ChipInput from 'material-ui-chip-input';
const useStyles = makeStyles(theme => ({
chipInput: { minWidth: 300 }
}));
export default function StandaloneChipInput() {
const classes = useStyles();
const [values, setValues] = useState([]);
const onAdd = chip => {
setValues([...values, chip]);
};
const onDelete = (chip, index) => {
setValues(values.slice(0, index).concat(values.slice(index + 1)));
};
return (
<ChipInput
className={classes.chipInput}
helperText="Type name, hit enter to type another"
value={values}
onAdd={onAdd}
onDelete={onDelete}
/>
);
}
当屏幕首次加载时,字段看起来像一个普通的文本字段,您可以在其中输入,如下所示:
如辅助文本所示,您可以按 Enter 添加项目并输入更多文本,如下所示:
您可以随意向字段中添加项目,如下所示:
确保辅助文本提到了回车键。否则,用户可能无法弄清楚他们可以输入多个值。
它是如何工作的...
保存 chip
输入字段值的 state
是一个数组——因为存在多个值。与 chip
输入状态相关的两个操作是从该数组中添加和删除字符串。让我们更详细地看看 onAdd()
和 onDelete()
函数,如下所示:
const onAdd = chip => {
setValues([...values, chip]);
};
const onDelete = (chip, index) => {
setValues(values.slice(0, index).concat(values.slice(index + 1)));
};
onAdd()
函数将 chip
添加到数组中,而 onDelete()
函数删除给定 index
的 chip
。当用户点击芯片中的 Delete
图标时,芯片将被删除。最后,让我们看看 ChipInput
组件本身,如下所示:
<ChipInput
className={classes.chipInput}
helperText="Type name, hit enter to type another"
value={values}
onAdd={onAdd}
onDelete={onDelete}
/>
它非常类似于 TextInput
组件。它实际上接受相同的属性,例如 helperText
。它还接受在 TextInput
中找不到的附加属性,例如 onAdd
和 onDelete
。
参见
- 一个 Material-UI
ChipInput
组件:www.npmjs.com/package/material-ui-chip-input
第十三章:选择 - 从选项中进行选择
在本章中,你将了解以下内容:
-
抽象复选框组
-
自定义复选框项
-
抽象单选按钮组
-
使用单选按钮类型
-
将复选框替换为开关
-
使用状态控制选择
-
选择多个项目
简介
任何包含用户交互的应用程序都涉及用户进行选择。这可以从简单的开/关开关到允许选择多个项目的多个项目选择。Material-UI 有不同类型的选择组件,最适合特定的用户场景。
抽象复选框组
复选框通常向用户提供一组相关的选项,用户可以选择或取消选择。Material-UI 的Checkbox
组件提供了基本的功能,但你可能想要一个更高级别的功能,可以在整个应用程序中重用。
如何实现...
让我们为复选框选项组创建一个抽象。以下是CheckboxGroup
组件的代码:
import React, { useState } from 'react';
import FormLabel from '@material-ui/core/FormLabel';
import FormControl from '@material-ui/core/FormControl';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormHelperText from '@material-ui/core/FormHelperText';
import Checkbox from '@material-ui/core/Checkbox';
const CheckboxGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
export default function AbstractingCheckboxGroups() {
const [values, setValues] = useState([
{ label: 'First', checked: false },
{ label: 'Second', checked: false },
{ label: 'Third', checked: false }
]);
const onChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
setValues(newValues);
};
return (
<CheckboxGroup
label="Choices"
values={values}
onChange={onChange}
/>
);
}
当你首次加载屏幕时,你会看到以下内容:
当你选择前两个选项时,它看起来是这样的:
它是如何工作的...
让我们更详细地看看CheckboxGroup
组件:
const CheckboxGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
这是允许你在应用程序的各个屏幕上渲染复选框选项组的抽象。有几个 Material-UI 组件涉及到渲染一组复选框——CheckboxGroup
为你处理这些,所以你只需要传递一个包含values
、label
和onChange
处理程序的数组。
接下来,让我们看看你的应用程序组件是如何渲染CheckboxGroup
的:
<CheckboxGroup
label="Choices"
values={values}
onChange={onChange}
/>
你只需要关注结构化值数组,并在你的应用程序需要渲染一组相关复选框选项时将其传递给CheckboxGroup
组件。最后,让我们看看state
和用于切换值选中状态的onChange()
处理程序:
const [values, setValues] = useState([
{ label: 'First', checked: false },
{ label: 'Second', checked: false },
{ label: 'Third', checked: false }
]);
const onChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
setValues(newValues);
};
checked
属性根据索引参数和target.checked
值进行更改。
还有更多...
让我们在这个例子中添加一个List
组件,这样你可以更好地可视化当复选框被选中/取消选中时发生的状态变化。以下是你需要导入的附加 Material-UI 组件:
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';
策略是让这个列表渲染已选中项的标签。让我们在CheckboxGroup
组件下方渲染这个列表:
<Fragment>
<CheckboxGroup
label="Choices"
values={values}
onChange={onChange}
/>
<Typography variant="h6">Selection</Typography>
<List>
{values
.filter(value => value.checked)
.map((value, index) => (
<ListItem key={index}>
<ListItemText>{value.label}</ListItemText>
</ListItem>
))}
</List>
</Fragment>
在values
上的filter()
调用只会包含checked
属性为true
的值。当屏幕首次加载时,你会看到一个空列表,因为没有默认选中任何内容:
当你开始进行选择时,你会看到选择列表随着应用程序状态的变化而变化:
参见
-
Checkbox
API 文档:material-ui.com/api/checkbox/
-
FormHelperText
API 文档:material-ui.com/api/form-helper-text/
-
FormControlLabel
API 文档:material-ui.com/api/form-control-label/
-
FormGroup
API 文档:material-ui.com/api/form-group/
-
FormControl
API 文档:material-ui.com/api/form-control/
-
FormLabel
API 文档:material-ui.com/api/form-label/
定制复选框项
Material-UI Checkbox
组件的默认外观试图与原生的浏览器复选框输入元素相似。你可以更改组件的选中和不选中状态所使用的图标。即使你更改了Checkbox
使用的图标,任何颜色更改仍然会被尊重。
如何做到这一点...
下面是一些导入几个 Material-UI 图标并使用它们来配置Checkbox
组件使用的图标的代码:
import React, { useState, useEffect } from 'react';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import AccountBalance from '@material-ui/icons/AccountBalance';
import AccountBalanceOutlined from '@material-ui/icons/AccountBalanceOutlined';
import Backup from '@material-ui/icons/Backup';
import BackupOutlined from '@material-ui/icons/BackupOutlined';
import Build from '@material-ui/icons/Build';
import BuildOutlined from '@material-ui/icons/BuildOutlined';
const initialItems = [
{
name: 'AccountBalance',
Icon: AccountBalanceOutlined,
CheckedIcon: AccountBalance
},
{
name: 'Backup',
Icon: BackupOutlined,
CheckedIcon: Backup
},
{
name: 'Build',
Icon: BuildOutlined,
CheckedIcon: Build
}
];
export default function CustomizingCheckboxItems() {
const [items, setItems] = useState({});
useEffect(() => {
setItems(
initialItems.reduce(
(state, item) => ({ ...state, [item.name]: false }),
{}
)
);
}, []);
const onChange = e => {
setItems({ [e.target.name]: e.target.checked });
};
return (
<FormGroup>
{initialItems.map(({ name, Icon, CheckedIcon }, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={items[name]}
onChange={onChange}
inputProps={{ name }}
icon={<Icon fontSize="small" />}
checkedIcon={<CheckedIcon fontSize="small" />}
/>
}
label={name}
/>
))}
</FormGroup>
);
}
当屏幕首次加载时,复选框看起来是这样的:
这些复选框是未选中的。当它们被选中时,它们看起来是这样的:
它是如何工作的...
让我们一步步分析这里发生的事情。initialItems
数组是构建复选框的起点:
const initialItems = [
{
name: 'AccountBalance',
Icon: AccountBalanceOutlined,
CheckedIcon: AccountBalance
},
{
name: 'Backup',
Icon: BackupOutlined,
CheckedIcon: Backup
},
{
name: 'Build',
Icon: BuildOutlined,
CheckedIcon: Build
}
];
每个项目都有一个name
组件来标识复选框,以及选中/未选中的Icon
组件。接下来,让我们看看CustomizingCheckboxItems
组件的状态是如何初始化的:
const [items, setItems] = useState({});
useEffect(() => {
setItems(
initialItems.reduce(
(state, item) => ({ ...state, [item.name]: false }),
{}
)
);
}, []);
状态通过减少initialItems
数组初始化为一个对象。对于数组中的每个项目,该组件的状态将有一个属性初始化为 false。属性的名称基于项目的name
属性。例如,组件状态在减少后可能看起来像这样:
{
AccountBalance: false,
Backup: false,
Build: false
}
这些属性用于存储每个复选框的选中状态。接下来,让我们看看每个Checkbox
组件是如何根据initialItems
数组渲染的:
<FormGroup>
{initialItems.map(({ name, Icon, CheckedIcon }, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={items[name]}
onChange={onChange}
inputProps={{ name }}
icon={<Icon fontSize="small" />}
checkedIcon={<CheckedIcon fontSize="small" />}
/>
}
label={name}
/>
))}
</FormGroup>
定制每个复选框的关键属性是icon
和checkedIcon
。这些属性分别使用项目数组中的Icon
和CheckIcon
属性。
还有更多...
由于你用来定制Checkbox
组件的图标是 Material-UI 组件,你可以更改复选框的颜色,并且它将像没有自定义图标一样工作。例如,你可以将此示例中复选框的颜色设置为默认值:
<Checkbox
color="default"
checked={items[name]}
onChange={onChange}
inputProps={{ name }}
icon={<Icon fontSize="small" />}
checkedIcon={<CheckedIcon fontSize="small" />}
/>
这是所有复选框都被选中时的样子:
当复选框从未选中变为选中时,颜色设置为默认值,颜色不会改变。不过,这并不重要,因为图标从轮廓主题变为填充主题。仅形状的变化就足以表明项目已被选中。
让我们尝试一下primary
,只是为了好玩:
<Checkbox
color="primary"
checked={items[name]}
onChange={onChange}
inputProps={{ name }}
icon={<Icon fontSize="small" />}
checkedIcon={<CheckedIcon fontSize="small" />}
/>
如果所有选项都被选中,看起来是这样的:
参见
-
Checkbox
API 文档:material-ui.com/api/checkbox/
-
FormControlLabel
API 文档:material-ui.com/api/form-control-label/
-
FormGroup
API 文档:material-ui.com/api/form-group/
抽象单选按钮组
单选按钮组与复选框组类似。关键区别在于单选按钮用于只应选择一个值的情况。此外,与复选框组一样,单选按钮组需要几个可以在整个应用程序中封装和重用的 Material-UI 组件。
它是如何工作的...
下面是一些代码,它捕获了将单选按钮组组合成一个组件所需的所有组件:
import React, { useState } from 'react';
import Radio from '@material-ui/core/Radio';
import { default as MaterialRadioGroup } from '@material-ui/core/RadioGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
const options = [
{ label: 'First', value: 'first' },
{ label: 'Second', value: 'second' },
{ label: 'Third', value: 'third' }
];
const RadioGroup = ({ value, options, name, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<MaterialRadioGroup
name={name}
value={value}
onChange={onChange}
disabled
>
{options.map((option, index) => (
<FormControlLabel
key={index}
control={<Radio />}
value={option.value}
label={option.label}
/>
))}
</MaterialRadioGroup>
</FormControl>
);
export default function AbstractingRadioButtonGroups() {
const [value, setValue] = useState('first');
const onChange = e => {
setValue(e.target.value);
};
return (
<RadioGroup
value={value}
options={options}
name="radio1"
label="Pick One"
onChange={onChange}
/>
);
}
当您首次加载屏幕时,您会看到以下内容:
如果您点击第三个选项,组件看起来是这样的:
因为这些选项都属于同一个单选组,所以一次只能选择一个选项。
它是如何工作的...
让我们更仔细地看看这个例子中的RadioGroup
组件:
const RadioGroup = ({ value, options, name, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<MaterialRadioGroup name={name} value={value} onChange={onChange}>
{options.map((option, index) => (
<FormControlLabel
key={index}
control={<Radio />}
value={option.value}
label={option.label}
/>
))}
</MaterialRadioGroup>
</FormControl>
);
options
属性应该有一个数组值,然后映射到FormControlLabel
组件。control
属性使用Radio
组件来渲染每个单选控制。与复选框组不同,onChange
属性位于MaterialRadioGroup
组件上,而不是每个单独的Radio
上。这是因为只有一个活动值,由MaterialRadioGroup
管理。
由于我们正在创建一个同名的组件,所以使用MaterialRadioGroup
别名导入 Material-UI 的RadioGroup
组件。只要您清楚哪些包拥有哪些组件,这就可以了。
接下来,让我们看看RadioGroup
组件是如何渲染的:
<RadioGroup
value={value}
options={options}
name="radio1"
label="Pick One"
onChange={onChange}
/>
name
属性是连接一切的关键。确保同一组中的单选按钮具有相同的名称非常重要。这种抽象通过只要求在一个地方提供名称来为您处理这个问题。下面是options
数组的样子:
const options = [
{ label: 'First', value: 'first' },
{ label: 'Second', value: 'second' },
{ label: 'Third', value: 'third' }
];
单选组的理念是它们始终只有一个值。options
数组中的值属性是允许的值——但只有一个处于活动状态。在下面的例子中,最后要查看的是 onChange
处理器和应用程序组件的状态结构:
const [value, setValue] = useState('first');
const onChange = e => {
setValue(e.target.value);
};
这是设置初始单选选择的步骤。当它改变时,值状态会更新为所选单选按钮的值。
更多内容...
你可以通过在 FormControl
组件上设置 disabled
属性来禁用整个单选按钮组:
<FormControl component="fieldset" disabled>
...
</FormControl>
当你禁用控件时,你将无法与之交互。下面是这种情况的示例:
在其他场景中,你可能只想禁用一个选项。你可以在 RadioGroup
组件中通过检查 options
数组中的 disabled
属性来实现这一点:
<FormControlLabel
key={index}
control={<Radio disabled={option.disabled} />}
value={option.value}
label={option.label}
/>
这是如何在 options
数组中禁用选项的方法:
const options = [
{ label: 'First', value: 'first' },
{ label: 'Second', value: 'second', disabled: true },
{ label: 'Third', value: 'third' }
];
这是禁用第二个选项后单选组的样式:
当第二个选项被禁用时,没有方法可以激活它,因为用户无法与之交互。
注意禁用默认激活的选项。这可能会使用户感到困惑。你可以激活组中的另一个选项,但之后你将无法激活最初激活的选项。
参见
-
Radio
API 文档:material-ui.com/api/radio/
-
RadioGroup
API 文档:material-ui.com/api/radio-group/
-
FormControlLabel
API 文档:material-ui.com/api/form-control-label/
-
FormControl
API 文档:material-ui.com/api/form-control/
-
FormLabel
API 文档:material-ui.com/api/form-label/
单选按钮类型
有许多单选按钮方面可以自定义,以创建您自己的单选按钮组。虽然从多个选项中选择单个值的基本原则没有改变,但您可以设计单选按钮组以适应任何应用程序。
如何实现...
假设根据你屏幕的布局,并且为了与你的应用程序中的其他屏幕保持一致,你需要创建一个具有以下设计特性的单选组:
-
单行用于展示选项
-
每个选项都有图标和文本
-
主要主题颜色用于选中的选项
下面是一些实现此功能的代码:
import React, { Fragment, useState } from 'react';
import Radio from '@material-ui/core/Radio';
import RadioGroup from '@material-ui/core/RadioGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormControl from '@material-ui/core/FormControl';
import FormLabel from '@material-ui/core/FormLabel';
import Car from '@material-ui/icons/DirectionsCar';
import CarOutlined from '@material-ui/icons/DirectionsCarOutlined';
import Bus from '@material-ui/icons/DirectionsBus';
import BusOutlined from '@material-ui/icons/DirectionsBusOutlined';
import Train from '@material-ui/icons/Train';
import TrainOutlined from '@material-ui/icons/TrainOutlined';
export default function RadioButtonTypes() {
const [value, setValue] = useState('train');
const onChange = e => {
setValue(e.target.value);
};
return (
<FormControl component="fieldset">
<FormLabel component="legend">Travel Mode</FormLabel>
<RadioGroup name="travel" value={value} onChange={onChange} row>
<FormControlLabel
value="car"
control={
<Radio
color="primary"
icon={<CarOutlined />}
checkedIcon={<Car />}
/>
}
label="Car"
labelPlacement="bottom"
/>
<FormControlLabel
value="bus"
control={
<Radio
color="primary"
icon={<BusOutlined />}
checkedIcon={<Bus />}
/>
}
label="Bus"
labelPlacement="bottom"
/>
<FormControlLabel
value="train"
control={
<Radio
color="primary"
icon={<TrainOutlined />}
checkedIcon={<Train />}
/>
}
label="Train"
labelPlacement="bottom"
/>
</RadioGroup>
</FormControl>
);
}
这是屏幕首次加载时单选组的样式:
你可以通过点击其他图标或标签来更改默认选择。图标状态会更新以反映更改:
它是如何工作的...
看起来我们能够满足为单选按钮组设定的标准。让我们通过代码来查看每个要求是如何满足的。首先,组是水平渲染的,每个单选按钮都在同一行。这是通过向RadioGroup
组件传递row
属性来实现的:
<RadioGroup
name="travel"
value={value}
onChange={onChange}
row
>
每个单选按钮的标签都显示在每个单选按钮下方,因为这与组的行布局更协调。这是通过设置FormControlLabel
的labelPlacement
属性值来实现的。当单选按钮被选中时,它使用 Material-UI 主题的默认颜色。它还使用自定义图标来表示选中状态和未选中状态:
<Radio
color="primary"
icon={<BusOutlined />}
checkedIcon={<Bus />}
/>
这两个增强功能都是由Radio
组件处理的。
参见
-
Selection
演示:material-ui.com/demos/selection-controls/
-
Radio
API 文档:material-ui.com/api/radio/
-
RadioGroup
API 文档:material-ui.com/api/radio-group/
-
FormControlLabel
API 文档:material-ui.com/api/form-control-label/
-
FormControl
API 文档:material-ui.com/api/form-control/
-
FormLabel
API 文档:material-ui.com/api/form-label/
用开关替换复选框
Material-UI 有一个与复选框非常相似的控制组件,称为开关。这两个组件之间的主要视觉区别是开关更强调开关/关断动作。在移动环境中,用户可能更习惯于Switch
组件。在其他任何环境中,你可能最好坚持使用常规的Checkbox
组件。
如何做到这一点...
假设你不想创建一个抽象一组Checkbox
组件的组件,而是想用Switch
组件做同样的事情。以下是代码:
import React, { Fragment, useState } from 'react';
import FormLabel from '@material-ui/core/FormLabel';
import FormControl from '@material-ui/core/FormControl';
import FormGroup from '@material-ui/core/FormGroup';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import FormHelperText from '@material-ui/core/FormHelperText';
import Switch from '@material-ui/core/Switch';
const SwitchGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Switch
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
export default function ReplacingCheckboxesWithSwitches() {
const [values, setValues] = useState([
{ label: 'First', checked: false },
{ label: 'Second', checked: false },
{ label: 'Third', checked: false }
]);
const onChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
setValues(newValues);
};
return (
<SwitchGroup
label="Choices"
values={values}
onChange={onChange}
/>
);
}
这是屏幕首次加载时开关组的外观:
这是所有开关都打开时开关组的外观:
它是如何工作的...
在任何可以使用Checkbox
组件的地方,你同样可以使用Switch
组件。这段代码是从本章早期部分的“抽象复选框组”部分摘取的。Checkbox
组件被替换成了Switch
组件。
更多内容...
而不是为处理 Checkbox
和 Switch
组件编写不同的代码路径,你可以增强 SwitchGroup
组件以接受一个 checkbox
布尔属性,当 true
时,使用 Checkbox
作为控制而不是 Switch
。以下是新的 SwitchGroup
的外观:
const SwitchGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Switch
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
以下是一个示例,展示了两种版本的控件并排渲染:
<Fragment>
<SwitchGroup
label="Switch Choices"
values={values}
onChange={this.onChange}
/>
<SwitchGroup
label="Switch Choices"
values={values}
onChange={onChange}
checkbox
/>
</Fragment>
第二个 SwitchGroup
组件使用 checkbox
属性来渲染 Checkbox
组件而不是 Switch
组件。以下是结果的外观:
如果你选择开关选项组或复选框选项组中的第一个选项,你会看到以下内容:
它们都进行了更新,因为这两个领域共享相同的应用状态。
参见
使用状态控制选择框
一些表单涉及从值列表中进行选择。这有点像从单选按钮组中选择单选按钮选项。使用 Material-UI Select
组件,你得到的东西更像是传统的 HTML 选择元素。通常,Web 应用程序表单有几个相互依赖的选择框。在 React/Material-UI 应用程序中,这些选择框通过 state
组件进行控制。
如何实现...
假设你的屏幕上有两个选择框——一个类别选择框和一个产品选择框。最初,只有类别选择框被填充并启用。产品选择框依赖于类别选择框——一旦选择了一个类别,产品选择框就会被启用并填充适当的产 品。以下是实现此功能的代码:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormHelperText from '@material-ui/core/FormHelperText';
import FormControl from '@material-ui/core/FormControl';
import Select from '@material-ui/core/Select';
const useStyles = makeStyles(theme => ({
control: { margin: theme.spacing(2), minWidth: 200 }
}));
export default function ControllingSelectsWithState() {
const classes = useStyles();
const [categories, setCategories] = useState([
{ label: 'Category 1', id: 1 },
{ label: 'Category 2', id: 2 },
{ label: 'Category 3', id: 3 }
]);
const [products, setProducts] = useState([
{ label: 'Product 1', id: 1, category: 1 },
{ label: 'Product 2', id: 2, category: 1 },
{ label: 'Product 3', id: 3, category: 1 },
{ label: 'Product 4', id: 4, category: 2 },
{ label: 'Product 5', id: 5, category: 2 },
{ label: 'Product 6', id: 6, category: 2 },
{ label: 'Product 7', id: 7, category: 3 },
{ label: 'Product 8', id: 8, category: 3 },
{ label: 'Product 9', id: 9, category: 3 }
]);
const setters = {
categories: setCategories,
products: setProducts
};
const collections = { categories, products };
const onChange = e => {
const setCollection = setters[e.target.name];
const collection = collections[e.target.name].map(item => ({
...item,
selected: false
}));
const index = collection.findIndex(
item => item.id === e.target.value
);
collection[index] = { ...collection[index], selected: true };
setCollection(collection);
};
const category = categories.find(category => category.selected) || {
id: ''
};
const product = products.find(product => product.selected) || {
id: ''
};
return (
<Fragment>
<FormControl className={classes.control}>
<InputLabel htmlFor="categories">Category</InputLabel>
<Select
value={category.id}
onChange={onChange}
inputProps={{
name: 'categories',
id: 'categories'
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{categories.map(category => (
<MenuItem key={category.id} value={category.id}>
{category.label}
</MenuItem>
))}
</Select>
</FormControl>
<FormControl
className={classes.control}
disabled={category.id === ''}
>
<InputLabel htmlFor="Products">Product</InputLabel>
<Select
value={product.id}
onChange={onChange}
inputProps={{
name: 'products',
id: 'values'
}}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{products
.filter(product => product.category === category.id)
.map(product => (
<MenuItem key={product.id} value={product.id}>
{product.label}
</MenuItem>
))}
</Select>
</FormControl>
</Fragment>
);
}
当屏幕首次加载时,你会看到以下内容:
类别选择框填充了可供你选择的选项。产品选择框处于禁用状态,因为没有选择任何类别。以下是类别选择框打开时的外观:
一旦你选择了一个类别,你应该能够打开产品选择框并做出产品选择:
它是如何工作的...
本例中的两个 Select
组件存在状态依赖。也就是说,产品选择的状 态依赖于类别选择的状 态。这是因为产品选择中显示的选项是根据所选类别进行筛选的。让我们更仔细地看看这个状态:
const [categories, setCategories] = useState([
{ label: 'Category 1', id: 1 },
{ label: 'Category 2', id: 2 },
{ label: 'Category 3', id: 3 }
]);
const [products, setProducts] = useState([
{ label: 'Product 1', id: 1, category: 1 },
{ label: 'Product 2', id: 2, category: 1 },
{ label: 'Product 3', id: 3, category: 1 },
{ label: 'Product 4', id: 4, category: 2 },
{ label: 'Product 5', id: 5, category: 2 },
{ label: 'Product 6', id: 6, category: 2 },
{ label: 'Product 7', id: 7, category: 3 },
{ label: 'Product 8', id: 8, category: 3 },
{ label: 'Product 9', id: 9, category: 3 }
]);
categories
和 products
数组代表屏幕上两个选择框的选项。选中的选项用 selected
布尔属性值 true
标记。默认情况下没有选项被选中。两个选择框都使用相同的 onChange()
处理器:
const setters = {
categories: setCategories,
products: setProducts
};
const collections = { categories, products };
const onChange = e => {
const setCollection = setters[e.target.name];
const collection = collections[e.target.name].map(item => ({
...item,
selected: false
}));
const index = collection.findIndex(
item => item.id === e.target.value
);
collection[index] = { ...collection[index], selected: true };
setCollection(collection);
};
要使用的数组取决于e.target.name
的值——它将是类别或产品。一旦使用适当的数组初始化了集合值,selected
属性就被设置为每个值的false
。然后,根据e.target.value
查找选中的值,并将selected
属性设置为true
。
接下来,让我们分析ControllingSelectsWithState
组件其余部分发生的情况。首先,从组件状态中查找category
和product
的选中项:
const category = categories.find(category => category.selected) || {
id: ''
};
const product = products.find(product => product.selected) || {
id: ''
};
您必须确保始终将这些常量分配给具有id
属性的对象,因为稍后会有所期待。空字符串将匹配空值选项,因此它默认被选中。接下来,让我们看看类别选项是如何渲染的:
{categories.map(category => (
<MenuItem key={category.id} value={category.id}>
{category.label}
</MenuItem>
))}
这是对categories
数组中的值到MenuItem
组件的直接映射。选择category
中的选项永远不会改变;换句话说,产品选项根据选中的类别改变——让我们看看这是如何实现的:
{products
.filter(product => product.category === category.id)
.map(product => (
<MenuItem key={product.id} value={product.id}>
{product.label}
</MenuItem>
))}
在将每个产品映射到MenuItem
组件之前,使用filter()
根据选中的类别过滤products
数组。
相关内容
-
Selection
示例:material-ui.com/demos/selects/
-
InputLabel
API 文档:material-ui.com/api/input-label/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
FormHelperText
API 文档:material-ui.com/api/form-helper-text/
-
FormControl
API 文档:material-ui.com/api/form-control/
-
Select
API 文档:material-ui.com/api/select/
选择多个项目
用户可以从Select
组件中选择多个值。这涉及到使用数组作为选中的值状态。
如何实现...
这里有一些渲染具有多个值的Select
的代码。您可以选择您喜欢的任意多个值:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Select from '@material-ui/core/Select';
import Input from '@material-ui/core/Input';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import FormControl from '@material-ui/core/FormControl';
const options = [
{ id: 1, label: 'First' },
{ id: 2, label: 'Second' },
{ id: 3, label: 'Third' },
{ id: 4, label: 'Fourth' },
{ id: 5, label: 'Fifth' }
];
const useStyles = makeStyles(theme => ({
formControl: {
margin: theme.spacing(1),
minWidth: 100,
maxWidth: 280
}
}));
export default function SelectingMultipleItems() {
const classes = useStyles();
const [selected, setSelected] = useState([]);
const onChange = e => {
setSelected(e.target.value);
};
return (
<FormControl className={classes.formControl}>
<InputLabel htmlFor="multi">Value</InputLabel>
<Select
multiple
value={selected}
onChange={onChange}
input={<Input id="multi" />}
>
{options.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.label}
</MenuItem>
))}
</Select>
</FormControl>
);
}
这是首次打开选择时的样子:
这是选中了第一个、第三个和第五个选项时的样子:
现在您已经做出了选择,您可以在菜单外的屏幕上点击某个位置来关闭它,或者您可以按E**sc 键。您将在文本输入中看到您的选择:
它是如何工作的...
让我们从查看Select
组件是如何渲染的开始:
<Select
multiple
value={selected}
onChange={onChange}
input={<Input id="multi" />}
>
{options.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.label}
</MenuItem>
))}
</Select>
options
数组的值被映射到 MenuItem
组件,就像任何其他的 Select
一样。multiple
属性告诉组件允许用户进行多项选择。SelectingMultipleItems
组件的 selected
状态是一个数组,它包含选中值。这个数组由 onChange
处理程序填充:
const onChange = e => {
setSelected(e.target.value);
};
因为使用了 multiple
属性,e.target.value
是一个包含选中值的数组——你可以直接使用这个值来更新选中状态。
还有更多...
而不是让选中的项目以逗号分隔的 test
列表形式显示,你可以通过将选中的值映射到 Chip
组件来使项目突出。让我们创建一个将处理此功能的组件:
function Selected({ selected }) {
const classes = useStyles();
return selected.map(value => (
<Chip
key={value}
label={options.find(option => option.id === value).label}
className={classes.chip}
/>
));
}
以下代码块展示了如何在 Select
组件的 renderValue
属性中使用此组件:
<Select
multiple
value={selected}
onChange={onChange}
input={<Input id="multi" />}
renderValue={selected => <Selected selected={selected} />}
>
{options.map(option => (
<MenuItem key={option.id} value={option.id}>
{option.label}
</MenuItem>
))}
</Select>
现在,当你进行多项选择时,它们将以 Chip
组件的形式渲染:
参见
-
Selection
示例:material-ui.com/demos/selects/
-
Select
API 文档:material-ui.com/api/select/
-
Input
API 文档:material-ui.com/api/input/
-
InputLabel
API 文档:material-ui.com/api/input-label/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
FormControl
API 文档:material-ui.com/api/form-control/
-
Chip
API 文档:material-ui.com/api/chip/
第十四章:选择器 - 选择日期和时间
在本章中,我们将涵盖以下主题:
-
使用日期选择器
-
使用时间选择器
-
设置初始日期和时间值
-
结合日期和时间组件
-
集成其他日期和时间包
简介
大多数应用程序都需要允许用户选择日期和时间值。例如,如果表单包含一个调度部分,用户需要一个直观的方式来选择日期和时间值。在 Material-UI 应用程序中,你可以使用库中提供的日期和时间选择器组件。
使用日期选择器
要在 Material-UI 应用程序中使用日期选择器,你可以利用TextField
组件。它接受一个type
属性,你可以将其设置为date
。然而,除了更改文本字段类型之外,你还需要注意其他一些事情。
如何实现...
下面是一段代码,用于为用户渲染一个日期选择器文本字段,并在日期选择改变时显示另一个格式的文本字段:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
textField: { margin: theme.spacing(1) }
}));
export default function UsingDatePickers() {
const classes = useStyles();
const [date, setDate] = useState('');
const onChange = e => {
setDate(e.target.value);
};
const dateFormatted = date
? new Date(`${date}T00:00:00`).toLocaleDateString()
: null;
return (
<Fragment>
<TextField
value={date}
onChange={onChange}
label="My Date"
type="date"
className={classes.textField}
InputLabelProps={{
shrink: true
}}
/>
<TextField
value={dateFormatted}
label="Updated Date Value"
className={classes.textField}
InputLabelProps={{
shrink: true
}}
InputProps={{ readOnly: true }}
/>
</Fragment>
);
}
当页面首次加载时,你会看到以下内容:
左侧的“我的日期”字段是日期选择器。右侧的“更新日期值”字段以不同的格式显示选定的日期。以下是日期选择器在获得焦点时的外观:
日期的年份部分被突出显示。你可以输入年份,或者可以使用上下箭头按钮来更改选定的值。通过按Tab键或使用鼠标指针,你可以切换到日期的月份或日部分。最右侧的向下箭头在点击时会显示以下原生浏览器日期选择器:
一旦你选择了日期,下面是“我的日期”和“更新日期值”字段的外观:
它是如何工作的...
让我们先看看日期选择器TextField
组件:
<TextField
value={date}
onChange={onChange}
label="My Date"
type="date"
className={classes.textField}
InputLabelProps={{
shrink: true
}}
/>
大多数日期选择器功能来自设置为date
的type
属性。这应用了输入掩码和原生浏览器日期选择器控件。由于输入掩码值,shrink
输入属性需要设置为true
以避免重叠。value
属性来自UsingDatePickers
组件的状态。此值默认为空字符串,但需要以特定格式。日期选择器文本字段将把日期值放入正确的格式,因此onChange()
处理程序实际上不需要做任何事情,只需设置date
状态即可。
“更新日期值”字段使用不同的日期格式。让我们看看这是如何实现的:
const dateFormatted = date
? new Date(`${date}T00:00:00`).toLocaleDateString()
: null;
首先,你需要从组件状态中获取date
字符串,并使用它来构造一个新的Date
实例。为此,你需要将时间字符串附加到日期字符串上。这使得它成为一个有效的 ISO 字符串,并使得日期可以无任何意外地构造。现在你可以使用任何可用的日期格式化函数,例如toLocaleDateString()
。
现在,你可以将dateFormatted
传递给第二个文本字段,该字段是只读的,因为它只用于显示值:
<TextField
value={dateFormatted}
label="Updated Date Value"
className={classes.textField}
InputLabelProps={{
shrink: true
}}
InputProps={{ readOnly: true }}
/>
还有更多...
可以对前面的示例进行一些改进。首先,你可以有一个DatePicker
组件,它隐藏了一些将TextField
组件转换为选择日期的组件的细节。此外,如果新的DatePicker
组件支持实际的Date
实例作为值,那就更好了。
首先,你需要一个实用函数,可以将Date
实例格式化为TextField
组件作为日期选择器使用时预期的字符串格式:
function formatDate(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return [
year,
month < 10 ? `0${month}` : month,
day < 10 ? `0${day}` : day
].join('-');
}
formatDate()
函数接受一个Date
实例,并返回一个格式为YYYY-MM-dd
的字符串。现在,你已经准备好构建DatePicker
组件了:
const DatePicker = ({ date, ...props }) => (
<TextField
value={date instanceof Date ? formatDate(date) : date}
type="date"
InputLabelProps={{
shrink: true
}}
{...props}
/>
);
DatePicker
组件渲染一个TextField
组件。它将type
属性值设置为date
,将shrink
输入属性设置为true
。它还设置了value
属性——首先检查date
属性是否是Date
实例,如果是,则调用formatDate()
。否则,直接使用date
参数。
现在,让我们用DatePicker
组件替换前面示例中的TextField
组件:
<Fragment>
<DatePicker
date={date}
onChange={onChange}
label="My Date"
className={classes.textField}
/>
<TextField
value={dateFormatted}
label="Updated Date Value"
className={classes.textField}
InputLabelProps={{
shrink: true
}}
InputProps={{ readOnly: true }}
/>
</Fragment>
onChange
、label
和className
属性以与之前相同的方式传递给TextField
组件。与DatePicker
组件的主要区别是,你不需要传递type
或InputProps
,而是使用date
而不是value
。
参见
-
TextField
API 文档:material-ui.com/api/text-field/
使用时间选择器
与日期选择器一样,时间选择器帮助用户输入时间值。同样,Material-UI 应用程序中的时间选择器也是从TextInput
组件派生出来的。
如何实现...
让我们创建与使用日期选择器部分中使用的相同抽象,但这次是为了time
选择器:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
textField: { margin: theme.spacing(1) }
}));
const TimePicker = ({ time, ...props }) => (
<TextField
value={time}
type="time"
InputLabelProps={{
shrink: true
}}
inputProps={{
step: 300
}}
{...props}
/>
);
export default function UsingTimePickers() {
const classes = useStyles();
const [time, setTime] = useState('');
const onChange = e => {
setTime(e.target.value);
};
return (
<Fragment>
<TimePicker
time={time}
onChange={onChange}
label="My Time"
className={classes.textField}
/>
<TextField
value={time}
label="Updated Time Value"
className={classes.textField}
InputLabelProps={{
shrink: true
}}
InputProps={{ readOnly: true }}
/>
</Fragment>
);
}
页面首次加载时,你会看到以下内容:
当 My Time 字段获得焦点后,你可以使用上/下箭头键或显示在时间值右侧的上/下箭头按钮来更改单个时间部分:
更新时间值字段不会更新,直到在 My Time 字段中选择了完整的时间,因为在此发生之前没有时间值:
它是如何工作的...
TimePicker
组件的结构与上一道菜谱中的DatePicker
组件非常相似。主要区别在于TimePicker
不支持Date
实例,因为它只处理时间。因为没有日期部分,使用Date
实例仅表达时间比仅表达日期要困难得多:
const TimePicker = ({ time, ...props }) => (
<TextField
value={time}
type="time"
InputLabelProps={{
shrink: true
}}
inputProps={{
step: 300
}}
{...props}
/>
);
TimePicker
组件在TextField
上设置的属性与DatePicker
组件相同。此外,step
值为300
使得时间部分的分钟数每次移动五分钟。
参见
-
TextField
API 文档:material-ui.com/api/text-field/
设置初始日期和时间值
日期和时间选择器可以分别具有默认的日期和时间值。例如,一个常见的场景是让这些输入默认为当前日期和时间。
如何实现...
假设你在应用的屏幕上有一个日期选择器和时间选择器。你希望date
字段默认为当前日期,time
字段默认为当前时间。为此,最好依赖于Date
实例来设置初始的Date/Time
值。然而,这需要一点工作,因为你不能原生地将Date
实例传递给TextField
组件。以下是一个示例,说明这是如何工作的:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
textField: { margin: theme.spacing.unit }
}));
function formatDate(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return [
year,
month < 10 ? `0${month}` : month,
day < 10 ? `0${day}` : day
].join('-');
}
function formatTime(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
return [
hours < 10 ? `0${hours}` : hours,
minutes < 10 ? `0${minutes}` : minutes
].join(':');
}
const DatePicker = ({ date, ...props }) => (
<TextField
value={date instanceof Date ? formatDate(date) : date}
type="date"
InputLabelProps={{
shrink: true
}}
{...props}
/>
);
const TimePicker = ({ time, ...props }) => (
<TextField
value={time instanceof Date ? formatTime(time) : time}
type="time"
InputLabelProps={{
shrink: true
}}
inputProps={{
step: 300
}}
{...props}
/>
);
export default function SettingInitialDateAndTimeValues() {
const classes = useStyles();
const [datetime, setDatetime] = useState(new Date());
const onChangeDate = e => {
if (!e.target.value) {
return;
}
const [year, month, day] = e.target.value
.split('-')
.map(n => Number(n));
const newDatetime = new Date(datetime);
newDatetime.setYear(year);
newDatetime.setMonth(month - 1);
newDatetime.setDate(day);
setDatetime(newDatetime);
};
const onChangeTime = e => {
const [hours, minutes] = e.target.value
.split(':')
.map(n => Number(n));
const newDatetime = new Date(datetime);
newDatetime.setHours(hours);
newDatetime.setMinutes(minutes);
setDatetime(newDatetime);
};
return (
<Fragment>
<DatePicker
date={datetime}
onChange={onChangeDate}
label="My Date"
className={classes.textField}
/>
<TimePicker
time={datetime}
onChange={onChangeTime}
label="My Time"
className={classes.textField}
/>
</Fragment>
);
}
当屏幕首次加载时,你会看到以下内容:
你看到的日期和时间将取决于你何时加载屏幕。然后你可以更改日期和时间值。
它是如何工作的...
这种方法的优点是,你只需要处理一个state
,即datetime
,它是一个Date
实例。让我们逐步查看代码,看看这是如何实现的,从UsingDatePickers
组件的初始状态开始:
const [datetime, setDatetime] = useState(new Date());
当前日期和时间被分配给datetime
状态。接下来,让我们看看两个格式化函数,它们使Date
实例能够与TextField
组件一起工作:
function formatDate(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
return [
year,
month < 10 ? `0${month}` : month,
day < 10 ? `0${day}` : day
].join('-');
}
function formatTime(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
return [
hours < 10 ? `0${hours}` : hours,
minutes < 10 ? `0${minutes}` : minutes
].join(':');
}
这两个函数,formatDate()
和formatTime()
,都接受一个Date
实例作为参数,并返回一个与TextField
组件一起工作的字符串格式值。接下来,让我们看看onChangeDate()
处理程序:
const onChangeDate = e => {
if (!e.target.value) {
return;
}
const [year, month, day] = e.target.value
.split('-')
.map(n => Number(n));
const newDatetime = new Date(datetime);
newDatetime.setYear(year);
newDatetime.setMonth(month - 1);
newDatetime.setDate(day);
setDatetime(newDatetime);
};
在onChangeDate()
中发生的第一个检查是对value
属性的检查。这个检查之所以需要发生,是为了让日期选择器实际上允许用户选择一个无效的日期,比如 2 月 31 日。当选择这个无效日期时,不改变state
实际上是在防止选择无效日期。
接下来,year
、month
和 day
值被分割并映射到数字。然后,通过使用 datetime
作为值创建一个新的 Date
实例来初始化新的 newDatetime
值。这样做是为了保留时间选择。最后,使用 setYear()
、setMonth()
和 setDate()
更新 Date
实例而不改变时间。
最后,让我们来看看 onChangeTime()
处理器:
const onChangeTime = e => {
const [hours, minutes] = e.target.value
.split(':')
.map(n => Number(n));
const newDatetime = new Date(datetime);
newDatetime.setHours(hours);
newDatetime.setMinutes(minutes);
setDatetime(newDatetime);
};
onChangeTime()
处理器遵循与 onChangeDate()
相同的一般模式。它更简单,因为值更少,且不需要检查无效的时间 - 每天都有 24 小时。
参见
-
TextField
API 文档:material-ui.com/api/text-field/
合并日期和时间组件
如果你的应用程序需要从用户那里收集日期和时间,你不必一定需要两个 TextField
组件。相反,你可以将它们合并成一个字段。
如何实现...
你可以通过将 type
属性设置为 datetime-local
来使用单个 TextInput
组件收集用户的日期和时间输入:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import TextField from '@material-ui/core/TextField';
const useStyles = makeStyles(theme => ({
textField: { margin: theme.spacing(1) }
}));
const formatDate = date =>
date
.toISOString()
.split(':')
.slice(0, 2)
.join(':');
const DateTimePicker = ({ date, ...props }) => (
<TextField
value={
date instanceof Date
? date.toISOString().replace('Z', '')
: date
}
type="datetime-local"
InputLabelProps={{
shrink: true
}}
{...props}
/>
);
export default function CombiningDateAndTimeComponents() {
const classes = useStyles();
const [datetime, setDatetime] = useState(new Date());
const onChangeDate = e => {
setDatetime(new Date(`${e.target.value}Z`));
};
return (
<DateTimePicker
date={formatDate(datetime)}
onChange={onChangeDate}
label="My Date/Time"
className={classes.textField}
/>
);
}
当屏幕首次加载时,你会看到以下内容:
这是字段获得焦点并且显示更改日期/时间控件时的外观:
它是如何工作的...
当你使用 datetime-local
类型的输入时,它简化了与 Date
实例的工作。让我们看看 onChangeDate()
处理器:
const onChangeDate = e => {
setDatetime(new Date(`${e.target.value}Z`));
};
你可以将 e.target.value
作为参数传递给一个新的 Date
实例,然后它将成为新的 datetime
状态值。最后,让我们看看用于将正确值传递给 TextField
的 value
属性的 formatDate()
函数:
const formatDate = date =>
date
.toISOString()
.split(':')
.slice(0, 2)
.join(':');
使用此函数的原因是删除 value
属性中的秒和毫秒。否则,这些将显示为用户可以选择的输入字段中的值。当选择时间时,用户选择秒或毫秒的情况非常罕见。
参见
-
TextField
API 文档:material-ui.com/api/text-field/
集成其他日期和时间包
你不必只使用 TextField
组件在你的 Material-UI 应用程序中进行 日期/时间
选择。有可用的包可以让 日期/时间
选择体验更接近传统的 Material Design 组件。
如何实现...
material-ui-pickers
包包含一个 DatePicker
组件和一个 TimePicker
组件。以下是一些代码示例,展示了如何使用这两个组件:
import React, { useState } from 'react';
import 'date-fns';
import DateFnsUtils from '@date-io/date-fns';
import { makeStyles } from '@material-ui/styles';
import Grid from '@material-ui/core/Grid';
import {
MuiPickersUtilsProvider,
TimePicker,
DatePicker
} from 'material-ui-pickers';
const useStyles = makeStyles(theme => ({
grid: {
width: '65%'
}
}));
export default function IntegratingWithOtherDateAndTimePackages() {
const classes = useStyles();
const [datetime, setDatetime] = useState(new Date());
const onChange = datetime => {
setDatetime(datetime);
};
return (
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<Grid container className={classes.grid} justify="space-around">
<DatePicker
margin="normal"
label="Date picker"
value={datetime}
onChange={onChange}
/>
<TimePicker
margin="normal"
label="Time picker"
value={datetime}
onChange={onChange}
/>
</Grid>
</MuiPickersUtilsProvider>
);
}
当屏幕首次加载时,你会看到以下内容:
当你点击日期选择器字段时,你会看到以下内容:
你可以使用这个对话框来选择你的日期,然后点击“确定”来更改它。当你点击时间选择器字段时,你会看到以下内容:
它是如何工作的...
来自 material-ui-pickers
包的 DatePicker
和 TimePicker
组件显示的对话框可以渲染其他 Material-UI 组件,这使得选择日期/时间更加容易。你无需直接与文本输入进行交互,可以展示给用户这样的对话框,这些对话框的主题与你的应用程序的其他部分相匹配,并提供视觉交互来选择日期/时间。
相关内容
-
Material-UI 选择器:
github.com/chingyawhao/material-ui-next-pickers
第十五章:对话框 - 用户交互的模态屏幕
在本章中,我们将涵盖以下主题:
-
收集表单输入
-
确认操作
-
显示警报
-
API 集成
-
创建全屏对话框
-
滚动对话框内容
简介
在与应用程序的交互过程中,用户将不得不在某个时刻向应用程序提供一些信息,做出是/否的决定,或确认重要信息。Material-UI 有一个对话框组件,非常适合这些场景——当你需要一个不会干扰当前屏幕内容的模态显示时。
收集表单输入
当你需要从用户那里收集输入,但又不想失去当前屏幕时,对话框很有用。例如,用户正在查看显示项目列表的屏幕,并想创建一个新项目。对话框可以显示必要的表单字段,一旦创建了新项目,对话框就会关闭,用户就会回到他们的项目列表。
如何操作...
假设你的应用程序允许创建新用户。例如,从显示用户列表的屏幕中,用户点击一个按钮,显示包含创建新用户字段的对话框。以下是如何操作的示例:
import React, { Fragment, useState } from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import Snackbar from '@material-ui/core/Snackbar';
export default function CollectingFormInput() {
const [dialogOpen, setDialogOpen] = useState(false);
const [snackbarOpen, setSnackbarOpen] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const [first, setFirst] = useState('');
const [last, setLast] = useState('');
const [email, setEmail] = useState('');
const onDialogOpen = () => {
setDialogOpen(true);
};
const onDialogClose = () => {
setDialogOpen(false);
setFirst('');
setLast('');
setEmail('');
};
const onSnackbarClose = (e, reason) => {
if (reason === 'clickaway') {
return;
}
setSnackbarOpen(false);
setSnackbarMessage('');
};
const onCreate = () => {
setSnackbarOpen(true);
setSnackbarMessage(`${first} ${last} created`);
onDialogClose();
};
return (
<Fragment>
<Button color="primary" onClick={onDialogOpen}>
New User
</Button>
<Dialog open={dialogOpen} onClose={onDialogClose}>
<DialogTitle>New User</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="normal"
label="First Name"
InputProps={{ name: 'first' }}
onChange={e => setFirst(e.target.value)}
value={first}
fullWidth
/>
<TextField
margin="normal"
label="Last Name"
InputProps={{ name: 'last' }}
onChange={e => setLast(e.target.value)}
value={last}
fullWidth
/>
<TextField
margin="normal"
label="Email Address"
type="email"
InputProps={{ name: 'email' }}
onChange={e => setEmail(e.target.value)}
value={email}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={onDialogClose} color="primary">
Cancel
</Button>
<Button
variant="contained"
onClick={onCreate}
color="primary"
>
Create
</Button>
</DialogActions>
</Dialog>
<Snackbar
open={snackbarOpen}
message={snackbarMessage}
onClose={onSnackbarClose}
autoHideDuration={4000}
/>
</Fragment>
);
}
这是屏幕首次加载时你会看到的按钮:
这是点击 NEW USER 按钮时你会看到的对话框:
然后,你可以填写创建新用户的三个字段,并点击 CREATE 按钮。对话框将关闭,你将看到以下Snackbar
组件显示:
它是如何工作的...
对话框和 Snackbar 的可见性由布尔状态值dialogOpen
和snackbarOpen
分别控制。dialog
组件内字段的值也存储在CollectingFormInput
组件的状态中。让我们更仔细地看看dialog
标记:
<Dialog open={dialogOpen} onClose={onDialogClose}>
<DialogTitle>New User</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="normal"
label="First Name"
InputProps={{ name: 'first' }}
onChange={e => setFirst(e.target.value)}
value={first}
fullWidth
/>
<TextField
margin="normal"
label="Last Name"
InputProps={{ name: 'last' }}
onChange={e => setLast(e.target.value)}
value={last}
fullWidth
/>
<TextField
margin="normal"
label="Email Address"
type="email"
InputProps={{ name: 'email' }}
onChange={e => setEmail(e.target.value)}
value={email}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={onDialogClose} color="primary">
Cancel
</Button>
<Button
variant="contained"
onClick={onCreate}
color="primary"
>
Create
</Button>
</DialogActions>
</Dialog>
Dialog
组件是其他几个组件的父组件,这些组件构成了dialog
的不同部分。DialogTitle
组件渲染对话框标题,而DialogActions
组件用于在对话框底部渲染操作按钮。DialogContent
组件用于渲染对话框的主要内容——创建新用户的三个文本字段。
对于这些TextField
组件,有两个属性与在对话框内渲染相关。首先,fullWidth
属性将字段水平扩展,使其与对话框宽度相同。这通常与只有几个字段的表单配合得很好。其次,margin
属性设置为normal
,这为对话框中的字段提供了适当的垂直间距。
接下来,让我们浏览这个组件的事件处理器,从onDialogOpen()
开始:
const onDialogOpen = () => {
setDialogOpen(true);
};
这将通过将 dialogOpen
状态更改为 true
来显示对话框。接下来,让我们看看 onDialogClose()
:
const onDialogClose = () => {
setDialogOpen(false);
setFirst('');
setLast('');
setEmail('');
};
这将通过将 dialogOpen
状态设置为 false
来关闭对话框。它还将表单字段值重置为空字符串,以便在下一次对话框显示时为空。接下来,我们有 onSnackbarClose()
:
const onSnackbarClose = (e, reason) => {
if (reason === 'clickaway') {
return;
}
setSnackbarOpen(false);
setSnackbarMessage('');
};
如果 reason
参数是 clickaway
,则无需操作。否则,snackbarOpen
状态将更改为 false
,这将隐藏 snackbar。snackbarMessage
状态设置为空字符串,以防 snackbar 在未设置新消息的情况下打开。最后,我们有 onCreate()
处理器:
const onCreate = () => {
setSnackbarOpen(true);
setSnackbarMessage(`${first} ${last} created`);
onDialogClose();
};
这将通过将 snackbarOpen
设置为 true
来显示 snackbar。它还设置了包含访问 first
和 last
状态值的 snackbarMessage
值。然后,调用 onDialogClose()
来隐藏对话框并重置表单字段。由于 autoHideDuration
值设置为 4000
,snackbar 在四秒后关闭。
相关内容
-
Dialog
示例:material-ui.com/demos/dialogs/
-
Dialog
API 文档:material-ui.com/api/dialog/
-
DialogActions
API 文档:material-ui.com/api/dialog-actions/
-
DialogContent
API 文档:material-ui.com/api/dialog-content/
-
DialogContentText
API 文档:material-ui.com/api/dialog-content-text/
-
Snackbar
API 文档:material-ui.com/api/snackbar/
-
TextField
API 文档:material-ui.com/api/text-field/
-
Button
API 文档:material-ui.com/api/button/
确认操作
确认对话框充当用户的安全网。当用户即将执行可能具有潜在危险的操作时,它们很有用,但不是针对应用中每个可想象的操作。如果执行后无法撤销,则操作可以被认为是危险的。删除账户或处理付款的操作就是一个危险的例子。在这些情况下,您应该始终使用确认对话框。
如何操作...
确认对话框应该简单明了,以便用户可以轻松阅读即将发生的事情,并决定是否取消操作或继续。以下是一些在执行操作前显示确认对话框的代码示例:
import React, { Fragment, useState } from 'react';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
export default function ConfirmingActions() {
const [open, setOpen] = useState(false);
const onShowConfirm = () => {
setOpen(true);
};
const onConfirm = () => {
setOpen(false);
};
return (
<Fragment>
<Button color="primary" onClick={onShowConfirm}>
Confirm Action
</Button>
<Dialog
disableBackdropClick
disableEscapeKeyDown
maxWidth="xs"
open={open}
>
<DialogTitle>Confirm Delete Asset</DialogTitle>
<DialogContent>
<DialogContentText>
Are you sure you want to delete the asset? This action
cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onDialogClose} color="primary">
Cancel
</Button>
<Button
variant="contained"
onClick={onConfirm}
color="primary"
>
Confirm
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
当通过点击“确认”按钮显示确认对话框时,它看起来是这样的:
您可以点击取消对话框操作来关闭对话框而不做任何事情,或者点击确认对话框操作,这将实际执行操作然后再关闭对话框。
它是如何工作的...
DialogContentText
组件用于在对话框中渲染确认消息。它实际上只是一个围绕 Typography
组件的薄包装。传递给对话框组件的两个有趣的属性是 disableBackdropClick
和 disableEscapeKeyDown
,它们分别防止通过点击对话框外的屏幕或按 Esc 键来关闭确认对话框。
这两个属性的想法是让用户明确承认他们正在执行需要他们密切注意的操作,或者他们选择不执行该操作。
相关内容
-
Dialog
演示:material-ui.com/demos/dialogs/
-
Dialog
API 文档:material-ui.com/api/dialog/
-
DialogActions
API 文档:material-ui.com/api/dialog-actions/
-
DialogContent
API 文档:material-ui.com/api/dialog-content/
-
DialogContentText
API 文档:material-ui.com/api/dialog-content-text/
-
Button
API 文档:material-ui.com/api/button/
显示警报
警报对话框类似于确认对话框。你可以把警报看作是真正重要的 snackbars,不能被忽视。像确认一样,警报会引起干扰,并且必须明确承认才能消除它们。此外,警报对话框可能不是用户采取直接行动的直接结果。警报可以是用户交互的环境发生变化的结果。
如何做...
假设你的应用程序需要能够提醒用户当他们的磁盘空间即将用尽时。以下是一个示例,展示了警报可能的样子:
import React, { Fragment, useState } from 'react';
import Button from '@material-ui/core/Button';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
export default function ConfirmingActions() {
const [open, setOpen] = useState(false);
return (
<Fragment>
<Button color="primary" onClick={() => setOpen(true)}>
Show Alert
</Button>
<Dialog open={open}>
<DialogContent>
<DialogContentText>
Disk space critically low. You won't be able to perform
any actions until you free up some space by deleting
assets.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => setOpen(false)}
color="primary"
>
Got It
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
当你点击显示警报按钮时,这就是警报对话框的显示效果:
它是如何工作的...
警报与常规对话框没有太大区别,你使用它们来收集用户的输入。警报的原则是保持简短并直截了当。例如,这个警报对话框没有标题。它不需要标题就能传达要点——如果用户不开始删除内容,他们将无法做任何事情。
还有更多...
你可以通过在警报消息和关闭警报的按钮上添加图标来让你的警报更加引人注目。以下是一个修改后的示例:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import Grid from '@material-ui/core/Grid';
import WarningIcon from '@material-ui/icons/Warning';
import CheckIcon from '@material-ui/icons/Check';
const useStyles = makeStyles(theme => ({
rightIcon: {
marginLeft: theme.spacing(1)
}
}));
export default function ConfirmingActions() {
const classes = useStyles();
const [open, setOpen] = useState(false);
return (
<Fragment>
<Button color="primary" onClick={() => setOpen(true)}>
Show Alert
</Button>
<Dialog open={open}>
<DialogContent>
<Grid container>
<Grid item xs={2}>
<WarningIcon fontSize="large" color="secondary" />
</Grid>
<Grid item xs={10}>
<DialogContentText>
Disk space critically low. You won't be able to
perform any actions until you free up some space by
deleting assets.
</DialogContentText>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button
variant="contained"
onClick={() => setOpen(false)}
color="primary"
>
Got It
<CheckIcon className={classes.rightIcon} />
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
这是新警报的样子:
相关内容
-
Dialog
示例:material-ui.com/demos/dialogs/
-
Dialog
API 文档:material-ui.com/api/dialog/
-
DialogActions
API 文档:material-ui.com/api/dialog-actions/
-
DialogContent
API 文档:material-ui.com/api/dialog-content/
-
DialogContentText
API 文档:material-ui.com/api/dialog-content-text/
-
Button
API 文档:material-ui.com/api/button/
API 集成
对话框通常需要从 API 端点提供数据。挑战是在用户等待后台加载 API 数据的同时显示加载状态。
如何实现...
假设你的应用程序需要显示一个带有 Select
组件的对话框来选择一个项目。选择框的选项是从 API 端点填充的,因此你需要处理用户打开对话框和 API 数据到达之间的延迟。以下是一个示例,展示了实现这一点的其中一种方法:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogActions from '@material-ui/core/DialogActions';
import Dialog from '@material-ui/core/Dialog';
import LinearProgress from '@material-ui/core/LinearProgress';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
const useStyles = makeStyles(theme => ({
dialog: { minHeight: 200 },
select: { width: '100%' }
}));
const fetchItems = () =>
new Promise(resolve => {
setTimeout(() => {
resolve([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
}, 3000);
});
const MaybeLinearProgress = ({ loading, ...props }) =>
loading ? <LinearProgress {...props} /> : null;
const MaybeSelect = ({ loading, ...props }) =>
loading ? null : <Select {...props} />;
export default function APIIntegration() {
const classes = useStyles();
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [items, setItems] = useState([]);
const [selected, setSelected] = useState('');
const onShowItems = () => {
setOpen(true);
setLoading(true);
fetchItems().then(items => {
setLoading(false);
setItems(items);
});
};
const onClose = () => {
setOpen(false);
};
const onSelect = e => {
setSelected(e.target.value);
};
return (
<Fragment>
<Button color="primary" onClick={onShowItems}>
Select Item
</Button>
<Dialog
open={open}
classes={{ paper: classes.dialog }}
maxWidth="xs"
fullWidth
>
<DialogTitle>Select Item</DialogTitle>
<DialogContent>
<MaybeLinearProgress loading={loading} />
<MaybeSelect
value={selected}
onChange={onSelect}
className={classes.select}
loading={loading}
>
<MenuItem value="">
<em>None</em>
</MenuItem>
{items.map(item => (
<MenuItem key={item.id} index={item.id} value={item.id}>
{item.name}
</MenuItem>
))}
</MaybeSelect>
</DialogContent>
<DialogActions>
<Button
disabled={loading}
onClick={onClose}
color="primary"
>
Cancel
</Button>
<Button
disabled={loading}
variant="contained"
onClick={onClose}
color="primary"
>
Select
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
这是对话框首次打开时的样子:
对话框显示一个 LinearProgress
组件,并在 API 数据加载时禁用对话框操作按钮。一旦响应到达,对话框看起来是这样的:
线性进度条消失了,对话框操作按钮被启用,并且有一个可供用户选择项目的“选择项”字段可见。以下是显示从 API 加载的项目选择项的“选择项”:
工作原理...
让我们逐步分析这段代码的主要部分,从模拟的 API 函数开始:
const fetchItems = () =>
new Promise(resolve => {
setTimeout(() => {
resolve([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]);
}, 3000);
});
fetchItems()
函数通过返回一个在三个秒后解析为数组数据的承诺来模拟一个 API 函数。这允许你在等待实际的 API 端点响应时看到用户将看到的内容。接下来,让我们看看两个帮助渲染或隐藏选择和进度指示器的实用组件:
const MaybeLinearProgress = ({ loading, ...props }) =>
loading ? <LinearProgress {...props} /> : null;
const MaybeSelect = ({ loading, ...props }) =>
loading ? null : <Select {...props} />;
理念是,你不想在加载为 false
时渲染 LinearProgress
组件。相反,你不想在加载为 true
时渲染 Select
组件。接下来,让我们看看 onShowItems()
:
const onShowItems = () => {
setOpen(true);
setLoading(true);
fetchItems().then(items => {
setLoading(false);
setItems(items);
});
};
首先,通过将 open
设置为 true
来打开对话框,并通过将 loading
设置为 true
来显示进度指示器。然后,调用 API 的 fetchItems()
函数,当它返回的 Promise
解析时,将 loading
设置为 false
并更新 items
数组。这隐藏了进度指示器并显示了现在已填充了项目的选择框。
相关内容
-
Dialog
示例:material-ui.com/demos/dialogs/
-
Dialog
API 文档:material-ui.com/api/dialog/
-
DialogActions
API 文档:material-ui.com/api/dialog-actions/
-
DialogContent
API 文档:material-ui.com/api/dialog-content/
-
DialogContentText
API 文档:material-ui.com/api/dialog-content-text/
-
Button
API 文档:material-ui.com/api/button/
-
LinearProgress
API 文档:material-ui.com/api/linear-progress/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
-
Select
API 文档:material-ui.com/api/select/
创建全屏对话框
在全屏对话框中,您有更多空间来渲染信息。大多数情况下,您不需要全屏对话框。在不常见的情况下,您的对话框需要尽可能多的空间来渲染信息。
如何做到...
假设,在您的应用程序的某个屏幕上有一个按钮,用于导出用户数据。当点击时,您想在用户确认之前给他们一个即将导出的数据的预览。以下是代码的样子:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import Typography from '@material-ui/core/Typography';
import Slide from '@material-ui/core/Slide';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import CloseIcon from '@material-ui/icons/Close';
const useStyles = makeStyles(theme => ({
appBar: {
position: 'relative'
},
flex: {
flex: 1
}
}));
const Transition = props => <Slide direction="up" {...props} />;
const id = (function*() {
let id = 0;
while (true) {
id += 1;
yield id;
}
})();
const rowData = (name, calories, fat, carbs, protein) => ({
id: id.next().value,
name,
calories,
fat,
carbs,
protein
});
const rows = [
rowData('Frozen yoghurt', 159, 6.0, 24, 4.0),
rowData('Ice cream sandwich', 237, 9.0, 37, 4.3),
rowData('Eclair', 262, 16.0, 24, 6.0),
rowData('Cupcake', 305, 3.7, 67, 4.3),
rowData('Gingerbread', 356, 16.0, 49, 3.9)
];
export default function FullScreenDialogs() {
const classes = useStyles();
const [open, setOpen] = useState(false);
const onOpen = () => {
setOpen(true);
};
const onClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button variant="outlined" color="primary" onClick={onOpen}>
Export Data
</Button>
<Dialog
fullScreen
open={open}
onClose={onClose}
TransitionComponent={Transition}
>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
color="inherit"
onClick={onClose}
aria-label="Close"
>
<CloseIcon />
</IconButton>
<Typography
variant="h6"
color="inherit"
className={classes.flex}
>
Export Data
</Typography>
<Button color="inherit" onClick={onClose}>
Export
</Button>
</Toolbar>
</AppBar>
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Dessert (100g serving)</TableCell>
<TableCell align="right">Calories</TableCell>
<TableCell align="right">Fat (g)</TableCell>
<TableCell align="right">Carbs (g)</TableCell>
<TableCell align="right">Protein (g)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
<TableRow key={row.id}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">{row.calories}</TableCell>
<TableCell align="right">{row.fat}</TableCell>
<TableCell align="right">{row.carbs}</TableCell>
<TableCell align="right">{row.protein}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Dialog>
</Fragment>
);
}
这里是对话框打开时的样子:
您可以点击对话框标题旁边的 X 按钮,关闭对话框,或者点击右侧的导出按钮。
它是如何工作的...
让我们看看传递给 Dialog
组件的属性:
<Dialog
fullScreen
open={open}
onClose={onClose}
TransitionComponent={Transition}
>
fullScreen
布尔属性决定了对话框在全屏模式下的渲染方式。TransitionComponent
属性改变了对话框过渡到屏幕上的方式。
因为对话框以全屏模式显示,您可能想要更改标题和操作对用户显示的方式,如下例所示。您可以使用 AppBar
和 Toolbar
组件而不是使用 DialogTitle
和 DialogAction
组件:
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton
color="inherit"
onClick={onClose}
aria-label="Close"
>
<CloseIcon />
</IconButton>
<Typography
variant="h6"
color="inherit"
className={classes.flex}
>
Export Data
</Typography>
<Button color="inherit" onClick={onClose}>
Export
</Button>
</Toolbar>
</AppBar>
这使得标题、关闭操作和主要操作对用户更加可见。
相关内容
-
Dialog
演示:material-ui.com/demos/dialogs/
-
Dialog
API 文档:material-ui.com/api/dialog/
-
AppBar
API 文档:material-ui.com/api/app-bar/
-
Toolbar
API 文档:material-ui.com/api/toolbar/
-
Table
API 文档:material-ui.com/api/table/
滚动对话框内容
可能很难找到足够的空间将所有内容放入对话框中。当对话框空间不足时,会添加一个垂直滚动条。
如何操作...
假设你有一个需要显示在对话框中供用户在导出为其他格式之前查看的长数据表格。用户将需要能够滚动浏览表格行。以下是一个示例:
import React, { Fragment, useState } from 'react';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogTitle from '@material-ui/core/DialogTitle';
import DialogContent from '@material-ui/core/DialogContent';
import DialogActions from '@material-ui/core/DialogActions';
import Table from '@material-ui/core/Table';
import TableBody from '@material-ui/core/TableBody';
import TableCell from '@material-ui/core/TableCell';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
const id = (function*() {
let id = 0;
while (true) {
id += 1;
yield id;
}
})();
const rowData = (name, calories, fat, carbs, protein) => ({
id: id.next().value,
name,
calories,
fat,
carbs,
protein
});
const rows = new Array(50)
.fill(null)
.reduce(
result =>
result.concat([
rowData('Frozen yoghurt', 159, 6.0, 24, 4.0),
rowData('Ice cream sandwich', 237, 9.0, 37, 4.3),
rowData('Eclair', 262, 16.0, 24, 6.0),
rowData('Cupcake', 305, 3.7, 67, 4.3),
rowData('Gingerbread', 356, 16.0, 49, 3.9)
]),
[]
);
export default function FullScreenDialogs() {
const [open, setOpen] = useState(false);
const onOpen = () => {
setOpen(true);
};
const onClose = () => {
setOpen(false);
};
return (
<Fragment>
<Button variant="outlined" color="primary" onClick={onOpen}>
Export Data
</Button>
<Dialog open={open} onClose={onClose}>
<DialogTitle>Desserts</DialogTitle>
<DialogContent>
<Table>
<TableHead>
<TableRow>
<TableCell>Dessert (100g serving)</TableCell>
<TableCell align="right">Calories</TableCell>
<TableCell align="right">Fat (g)</TableCell>
<TableCell align="right">Carbs (g)</TableCell>
<TableCell align="right">Protein (g)</TableCell>
</TableRow>
</TableHead>
<TableBody>
{rows.map(row => (
<TableRow key={row.id}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">{row.calories}</TableCell>
<TableCell align="right">{row.fat}</TableCell>
<TableCell align="right">{row.carbs}</TableCell>
<TableCell align="right">{row.protein}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
Cancel
</Button>
<Button
variant="contained"
onClick={onClose}
color="primary"
>
Export
</Button>
</DialogActions>
</Dialog>
</Fragment>
);
}
这是对话框打开时的样子:
如果你将鼠标指针移到表格行上并开始滚动,表格行将在对话框标题和对话框操作按钮之间上下滚动。
它是如何工作的...
默认情况下,对话框内容将在对话框的Paper
组件(即DialogContent
组件)内部滚动,因此无需指定属性。但是,你可以将body
值传递给Dialog
组件的scroll
属性。这将使对话框的高度改变以适应内容。
相关内容
-
Dialog
演示:material-ui.com/demos/dialogs/
-
Dialog
API 文档:material-ui.com/api/dialog/
-
Table
API 文档:material-ui.com/api/table/
第十六章:菜单 - 显示弹出动作
在本章中,我们将涵盖以下主题:
-
使用状态组合菜单
-
菜单滚动选项
-
使用菜单过渡
-
自定义菜单项
简介
菜单用于组织用户可以执行的一组命令。通常,菜单有一些上下文,比如应用程序中某个资源的详细信息屏幕。Material-UI 提供了一个Menu
组件,它允许你为给定的屏幕组织命令。
使用状态组合菜单
Menu
组件用于执行某些操作。将菜单视为列表和按钮的组合。菜单最适合在只想临时显示菜单项的场景中使用。菜单和菜单项的可见性可以通过组件状态来控制。
如何做到...
假设你的应用程序中有一个组件有一个菜单按钮,点击后会显示一个包含几个选项的菜单。这些选项可以根据应用程序中的其他状态变化,比如权限或另一个资源的状态。以下是构建此组件的源代码:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import MenuIcon from '@material-ui/icons/Menu';
const useStyles = makeStyles(theme => ({
rightIcon: {
marginLeft: theme.spacing(1)
}
}));
export default function ComposingMenusWithState() {
const onOpen = e => {
setAnchorEl(e.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const [items, setItems] = useState([
{ name: 'First', onClick: onClose },
{ name: 'Second', onClick: onClose },
{ name: 'Third', onClick: onClose },
{ name: 'Fourth', onClick: onClose, disabled: true }
]);
return (
<Fragment>
<Button onClick={onOpen}>
Menu
<MenuIcon className={classes.rightIcon} />
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
>
{items.map((item, index) => (
<MenuItem
key={index}
onClick={item.onClick}
disabled={item.disabled}
>
{item.name}
</MenuItem>
))}
</Menu>
</Fragment>
);
}
当屏幕首次加载时,你会看到以下内容:
当你点击菜单按钮时,菜单会按以下方式显示:
它是如何工作的...
让我们先看看ComposingMenusWithState
组件的状态:
const [anchorEl, setAnchorEl] = useState(null);
const [items, setItems] = useState([
{ name: 'First', onClick: onClose },
{ name: 'Second', onClick: onClose },
{ name: 'Third', onClick: onClose },
{ name: 'Fourth', onClick: onClose, disabled: true }
]);
anchorEl
状态引用了当菜单打开时菜单所锚定的元素。当它是 null 时,菜单是关闭的。items
数组包含菜单项。name
属性被渲染为菜单项文本。当菜单项被选中时,会调用onClick
函数。当disabled
属性为true
时,会禁用该项。接下来,让我们看看onOpen()
和onClose()
处理程序:
const onOpen = e => {
setAnchorEl(e.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
当用户点击菜单按钮时,anchorEl
状态被设置为e.currentTarget
——这是被点击的按钮,也是菜单知道如何渲染自己的方式。当菜单关闭时,这被设置为null
,导致菜单被隐藏。最后,让我们看看Menu
标记:
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
>
{items.map((item, index) => (
<MenuItem
key={index}
onClick={item.onClick}
disabled={item.disabled}
>
{item.name}
</MenuItem>
))}
</Menu>
open
属性期望一个布尔值,这就是为什么改变anchorEl
状态会导致Boolean(anchorEL)
根据用户与它的交互打开或关闭菜单。然后items
状态被映射到MenuItem
组件。
还有更多...
如果你的应用程序有多个屏幕,你可以创建自己的Menu
组件,该组件负责将项目映射到MenuItem
组件。让我们修改这个示例来构建菜单抽象,并进一步说明菜单项如何随着应用程序数据随时间变化而改变状态。以下是修改后的示例:
import React, { Fragment, useState, useEffect } from 'react';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import MenuIcon from '@material-ui/icons/Menu';
const useStyles = makeStyles(theme => ({
rightIcon: {
marginLeft: theme.spacing.unit
}
}));
const MyMenu = ({ items, onClose, anchorEl }) => (
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
>
{items.map((item, index) => (
<MenuItem
key={index}
onClick={item.onClick}
disabled={item.disabled}
>
{item.name}
</MenuItem>
))}
</Menu>
);
export default function ComposingMenusWithState() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const [items, setItems] = useState([
{ name: 'Enable Fourth' },
{ name: 'Second', onClick: onClose },
{ name: 'Third', onClick: onClose },
{ name: 'Fourth', onClick: onClose, disabled: true }
]);
useEffect(() => {
const toggleFourth = () => {
let newItems = [...items];
newItems[3] = { ...items[3], disabled: !items[3].disabled };
newItems[0] = {
...items[0],
name: newItems[3].disabled
? 'Enable Fourth'
: 'Disable Fourth'
};
setItems(newItems);
};
const newItems = [...items];
newItems[0] = { ...items[0], onClick: toggleFourth };
setItems(newItems);
});
const onOpen = e => {
setAnchorEl(e.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
return (
<Fragment>
<Button onClick={onOpen}>
Menu
<MenuIcon className={classes.rightIcon} />
</Button>
<MyMenu items={items} onClose={onClose} anchorEl={anchorEl} />
</Fragment>
);
}
MyMenu
组件接受 onClose
处理程序、anchorEl
状态和 items
数组作为属性。为了展示您如何更新菜单项状态并使它们渲染(即使在菜单打开时),有一个新的 toggleFourth()
处理程序应用于第一个菜单项的 onClick
属性。它是在 useEffect()
内部应用的,因为这是 toggleFourth()
获取新 items
值的唯一方式;当它改变时,我们必须重新定义函数并将其重新分配给 onClick
。这将切换第一菜单项的文本和 Fourth
项的禁用状态。以下是菜单首次打开时的样子:
点击第一个菜单项后,菜单看起来是这样的:
第一项的文本已被切换,第四项现在处于启用状态。您可以继续点击第一项以切换这两个项目的状态。
参见
-
Menu
演示:material-ui.com/demos/menus/
-
Menu
API 文档:material-ui.com/api/menu/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
菜单滚动选项
有时菜单有很多选项。这可能会对菜单的高度造成问题。您不必显示非常长的菜单,可以为菜单设置最大高度,并使其垂直滚动。
如何做到这一点...
假设您需要渲染一个菜单,其中包含的选项比一次合理显示在屏幕上的多。此外,菜单中的一个选项可能处于选中状态。以下是一些代码示例,展示了如何处理这种情况:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import IconButton from '@material-ui/core/IconButton';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import MenuIcon from '@material-ui/icons/Menu';
const items = [
'None',
'Atria',
'Callisto',
'Dione',
'Ganymede',
'Hangouts Call',
'Luna',
'Oberon',
'Phobos',
'Pyxis',
'Sedna',
'Titania',
'Triton',
'Umbriel'
];
const ITEM_HEIGHT = 48;
const useStyles = makeStyles(theme => ({
menuPaper: { maxHeight: ITEM_HEIGHT * 4.5, width: 200 }
}));
export default function MenuScrollingOptions() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const [selected, setSelected] = useState('');
const onOpen = e => {
setAnchorEl(e.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
const onSelect = selected => () => {
setSelected(selected);
setAnchorEl(null);
};
return (
<Fragment>
<IconButton onClick={onOpen}>
<MenuIcon />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
PaperProps={{
classes: { elevation8: classes.menuPaper }
}}
>
{items.map((item, index) => (
<MenuItem
key={index}
selected={index === selected}
onClick={onSelect(index)}
>
{item}
</MenuItem>
))}
</Menu>
</Fragment>
);
}
初始时,没有项目被选中。以下是菜单首次打开时的样子:
您可以滚动菜单项。以下是菜单底部的样子:
您可以选择一个选项来关闭菜单。选择将被保留,因此下次您打开菜单时,您将看到所选的项目:
当菜单有一个选中项时,Menu
组件会自动滚动到选中项。您可以通过在关闭菜单后再重新打开它,将选中项滚动出视图来测试这一点。您将在菜单中间看到选中项。
它是如何工作的...
让我们先看看这个例子中使用的 menuPaper
样式:
const ITEM_HEIGHT = 48;
const useStyles = makeStyles(theme => ({
menuPaper: { maxHeight: ITEM_HEIGHT * 4.5, width: 200 }
}));
ITEM_HEIGHT
值是每个菜单项高度的近似值。乘数(4.5
)是屏幕上应适合多少菜单项的近似值。现在,让我们跳入 Menu
组件的标记:
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
PaperProps={{
classes: { elevation8: classes.menuPaper }
}}
>
{items.map((item, index) => (
<MenuItem
key={index}
selected={index === selected}
onClick={onSelect(index)}
>
{item}
</MenuItem>
))}
</Menu>
每个 MenuItem
组件的 selected
属性在 selected
状态与当前项的 index
匹配时设置为 true
。menuPaper
类通过 PaperProps
属性应用,但其中有一个 elevation8
属性,实际上应用了该类。这是因为如果您只是通过 className
分配类,Menu
组件将覆盖 maxHeight
样式。为了解决这个问题,您必须使用更具体的 CSS API。Paper
组件有几个高度级别——数字越高,应用的阴影就越多(使元素看起来更高)。
Paper
的默认高度为 2
。但 Menu
组件使用 Popover
组件来渲染 Paper
,将高度更改为 8
。简而言之,elevation8
CSS API 允许您应用覆盖默认值的类样式。这就是如何得到可滚动的菜单。
相关内容
-
Menu
示例:material-ui.com/demos/menus/
-
Menu
API 文档:material-ui.com/api/menu/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
使用菜单过渡
您可以更改 Menu
组件使用的过渡效果。默认情况下,Menu
使用 Grow
过渡组件。
如何实现...
为了演示如何将不同的过渡效果应用到 Menu
组件,我们将为这个示例添加一些过渡选项到 Storybook。您可以使用以下代码更改使用的过渡组件,以及过渡效果的持续时间:
import React, { Fragment, useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Collapse from '@material-ui/core/Collapse';
import Fade from '@material-ui/core/Fade';
import Grow from '@material-ui/core/Grow';
import Slide from '@material-ui/core/Slide';
import MenuIcon from '@material-ui/icons/Menu';
const useStyles = makeStyles(theme => ({
rightIcon: {
marginLeft: theme.spacing.unit
}
}));
export default function UsingMenuTransitions({
transition,
duration
}) {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const onOpen = e => {
setAnchorEl(e.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
return (
<Fragment>
<Button onClick={onOpen}>
Menu
<MenuIcon className={classes.rightIcon} />
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
transitionDuration={duration}
TransitionComponent={
{
collapse: Collapse,
fade: Fade,
grow: Grow,
slide: Slide
}[transition]
}
>
<MenuItem onClick={onClose}>Profile</MenuItem>
<MenuItem onClick={onClose}>My account</MenuItem>
<MenuItem onClick={onClose}>Logout</MenuItem>
</Menu>
</Fragment>
);
}
您将在 Storybook Knobs 面板中看到不同的过渡选项。当您更改过渡效果时,您将在打开和关闭菜单时注意到差异。不幸的是,我无法捕获这些过渡效果的截图。
它是如何工作的...
传递给 UsingMenuTransitions
组件的 transition
属性来自 Storybook,并用于确定使用的过渡效果。让我们更详细地看看 Menu
使用的 TransitionComponent
属性,以确定使用哪种过渡效果:
TransitionComponent={
{
collapse: Collapse,
fade: Fade,
grow: Grow,
slide: Slide
}[transition]
}
transition
字符串映射到 Material-UI 过渡组件,您可以将其传递给 Menu
。
相关内容
-
Menu
示例:material-ui.com/demos/menus/
-
菜单
API 文档:material-ui.com/api/menu/
-
菜单项
API 文档:material-ui.com/api/menu-item/
-
Collapse
API 文档:material-ui.com/api/collapse/
-
Fade
API 文档:material-ui.com/api/collapse/
-
Grow
API 文档:material-ui.com/api/grow/
-
Slide
API 文档:material-ui.com/api/slide/
自定义菜单项
你可以将具有 onClick
处理器的常规菜单项改为更复杂的形式。例如,你可能希望菜单中包含指向你应用中其他屏幕的链接。
如何实现...
假设你在你的应用程序中使用 react-router
来控制从一个屏幕到另一个屏幕的导航,并且你希望使用 Menu
组件来 渲染
链接。以下是一个示例,展示了如何实现这一点:
import React, { Fragment, useState } from 'react';
import { Switch, Route, Link } from 'react-router-dom';
import { makeStyles } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Typography from '@material-ui/core/Typography';
import MenuIcon from '@material-ui/icons/Menu';
const NavMenuItem = ({ color, ...props }) => (
<Switch>
<Route
exact
path={props.to}
render={() => <MenuItem selected component={Link} {...props} />}
/>
<Route
path="/"
render={() => <MenuItem component={Link} {...props} />}
/>
</Switch>
);
const useStyles = makeStyles(theme => ({
rightIcon: {
marginLeft: theme.spacing(1)
}
}));
export default function CustomizingMenuItems() {
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState(null);
const onOpen = e => {
setAnchorEl(e.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
return (
<Fragment>
<Button onClick={onOpen}>
Menu
<MenuIcon className={classes.rightIcon} />
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
>
<NavMenuItem to="/" onClick={onClose}>
Home
</NavMenuItem>
<NavMenuItem to="/page1" onClick={onClose}>
Page 1
</NavMenuItem>
<NavMenuItem to="/page2" onClick={onClose}>
Page 2
</NavMenuItem>
</Menu>
<Switch>
<Route
exact
path="/"
render={() => <Typography>home content</Typography>}
/>
<Route
path="/page1"
render={() => <Typography>page 1 content</Typography>}
/>
<Route
path="/page2"
render={() => <Typography>page 2 content</Typography>}
/>
</Switch>
</Fragment>
);
}
当屏幕首次加载时,你会看到以下内容:
当菜单打开时,菜单看起来是这样的:
尝试点击第 1 页。这应该会关闭菜单并更改菜单下方显示的内容,因为你已经导航到了另一个屏幕,如下面的截图所示:
活跃链接会在菜单中反映出来。如果你从第 1 页打开菜单,菜单看起来是这样的:
它是如何工作的...
让我们从查看 NavMenuItem
组件开始:
const NavMenuItem = ({ color, ...props }) => (
<Switch>
<Route
exact
path={props.to}
render={() => <MenuItem selected component={Link} {...props} />}
/>
<Route
path="/"
render={() => <MenuItem component={Link} {...props} />}
/>
</Switch>
);
这将基于当前路由渲染一个 MenuItem
组件。如果 to
属性的值与当前路由匹配,则 selected
属性将为 true
——这就是当你打开菜单时菜单项看起来被选中时的原因。接下来,让我们看看 Menu
的标记:
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={onClose}
>
<NavMenuItem to="/" onClick={onClose}>
Home
</NavMenuItem>
<NavMenuItem to="/page1" onClick={onClose}>
Page 1
</NavMenuItem>
<NavMenuItem to="/page2" onClick={onClose}>
Page 2
</NavMenuItem>
</Menu>
你可以渲染 NavMenuItem
组件而不是渲染 MenuItem
组件。这些组件将渲染带有 selected
属性设置为 true
的当前路由的链接。请注意,这里需要 to
属性来链接到另一个页面,并且需要 onClick
处理器来在页面过渡时关闭菜单。
参见
-
Menu
API 文档:material-ui.com/api/menu/
-
MenuItem
API 文档:material-ui.com/api/menu-item/
第十七章:字体排版 - 控制字体外观和感觉
在本章中,我们将涵盖以下主题:
-
字体排版类型
-
使用主题颜色
-
文本对齐
-
文本换行
简介
Typography
组件由 Material-UI 用于在屏幕上渲染文本。你可以单独使用Typography
,但它也被其他渲染文本的 Material-UI 组件内部使用。使用Typography
组件而不是其他 HTML 元素来渲染你的文本,允许 Material-UI 使用一致的字体类型应用主题样式,并且以统一的方式处理字体行为。
字体排版类型
当你想在 Material-UI 应用程序中渲染文本时,Typography
组件就会被使用。文本类型或变体被指定为一个字符串值,该值传递给variant
属性。
如何做到这一点...
下面是一个示例,展示了如何渲染所有可用的Typography
变体:
import React, { Fragment } from 'react';
import Typography from '@material-ui/core/Typography';
const MyTypography = ({ variant, ...props }) => (
<Typography variant={variant || 'inherit'} {...props} />
);
const TypesOfTypography = () => (
<Fragment>
<Typography variant="h1">h1 variant</Typography>
<Typography variant="h2">h2 variant</Typography>
<Typography variant="h3">h3 variant</Typography>
<Typography variant="h4">h4 variant</Typography>
<Typography variant="h5">h5 variant</Typography>
<Typography variant="h6">h6 variant</Typography>
<Typography variant="subtitle1">subtitle1 variant</Typography>
<Typography variant="subtitle2">subtitle2 variant</Typography>
<Typography variant="body1">body1 variant</Typography>
<Typography variant="body2">body2 variant</Typography>
<Typography variant="subtitle1">subtitle1 variant</Typography>
<Typography variant="caption">caption variant</Typography>
<Typography variant="button">button variant</Typography>
<Typography variant="overline">overline variant</Typography>
<Typography variant="title" component="div">
<Typography variant="inherit">
inherited title variant
</Typography>
<Typography variant="inherit">
another inherited title variant
</Typography>
<Typography variant="caption">
overridden caption variant
</Typography>
</Typography>
<MyTypography variant="title" component="div">
<MyTypography>inherited title variant</MyTypography>
<MyTypography>another inherited title variant</MyTypography>
<MyTypography variant="caption">
overridden caption variant
</MyTypography>
</MyTypography>
</Fragment>
);
export default TypesOfTypography;
下面是标题变体的样子:
最后,以下是剩余变体的样子:
它是如何工作的...
您传递给variant
属性的值决定了应用于文本的样式。每个这些变体的样式由主题定义,并且可以从一个主题定制到另一个主题。
可能会诱使你添加自己的变体名称,或者在外部添加字体样式。我建议不要这样做,因为这样做会破坏基于 Material Design 的通用字体词汇。如果你偏离了字体变体约定,你最终会得到只有你才能理解的变体名称,或者更糟糕的是,由于从字体系统外部应用到文本上的字体样式,变体将无法工作。
还有更多...
如果你希望你的Typography
组件从其父组件继承变体样式,你可以使用inherit
变体值,如下面的示例所示:
<Typography variant="title" component="div">
<Typography variant="inherit">
inherited title variant
</Typography>
<Typography variant="inherit">
another inherited title variant
</Typography>
<Typography variant="caption">
overridden caption variant
</Typography>
</Typography>
父Typography
组件使用title
变体。它还将它的组件更改为div
元素,因为它实际上并没有将文本作为直接子元素渲染——把它看作是字体样式的容器。在里面,有三个子Typography
组件。前两个将inherit
作为variant
属性值,因此它们实际上会得到title
变体。第三个Typography
子组件使用caption
作为其变体,因此它不会继承title
。
下面是结果的样子:
你可能考虑对这个方法的一个调整是将inherit
作为默认变体。这样,如果你有很多需要继承字体样式的子Typography
组件,你就不必一直输入variant="inherit"
。以下是一个执行此操作的组件:
const MyTypography = ({ variant, ...props }) => (
<Typography variant={variant || 'inherit'} {...props} />
);
MyTypography
组件将渲染一个 Typography
组件,其 variant
值为 inherit
,但前提是未传递 variant
属性。让我们将前面的代码更改为使用这个新组件:
<MyTypography variant="title" component="div">
<MyTypography>inherited title variant</MyTypography>
<MyTypography>another inherited title variant</MyTypography>
<MyTypography variant="caption">
overridden caption variant
</MyTypography>
</MyTypography>
结果完全相同。唯一的区别是现在你不需要为想要继承的变体提供 variant
属性。
相关内容
-
Typography
演示:material-ui.com/style/typography/
-
Typography
API 文档:material-ui.com/api/typography/
使用主题颜色
使用 Typography
组件渲染的文本可以使用应用使用的 Material-UI 主题中的颜色。
如何做到...
对于这个示例,你将找到一个 Storybook 控件,允许你使用主题中预定义的颜色名称更改文本颜色,如下面的截图所示:
这是使用通过传递给每个 Typography
组件的 color
属性来选择颜色的示例的源代码:
import React, { Fragment } from 'react';
import Typography from '@material-ui/core/Typography';
const UsingThemeColors = ({ color }) => (
<Fragment>
<Typography variant="h1" color={color}>
h1 variant
</Typography>
<Typography variant="h2" color={color}>
h2 variant
</Typography>
<Typography variant="h3" color={color}>
h3 variant
</Typography>
<Typography variant="h4" color={color}>
h4 variant
</Typography>
<Typography variant="h5" color={color}>
h5 variant
</Typography>
<Typography variant="h6" color={color}>
h6 variant
</Typography>
<Typography variant="subtitle1" color={color}>
subtitle1 variant
</Typography>
<Typography variant="subtitle2" color={color}>
subtitle2 variant
</Typography>
<Typography variant="body1" color={color}>
body1 variant
</Typography>
<Typography variant="body2" color={color}>
body2 variant
</Typography>
<Typography variant="caption" color={color}>
caption variant
</Typography>
<Typography variant="button" color={color}>
button variant
</Typography>
<Typography variant="overline" color={color}>
overline variant
</Typography>
</Fragment>
);
export default UsingThemeColors;
它是如何工作的...
让我们来看看这些颜色如何改变不同 Typography
变体的外观:
默认 | default 颜色使用为所讨论的 Typography 变体定义的任何颜色 |
![]() |
---|---|---|
错误 | error 颜色将 palette.error.main 主题颜色应用到文本上 |
![]() |
继承 | Typography 组件将继承其父组件的字体颜色 |
![]() |
主要 | primary 颜色将 palette.primary.main 主题颜色应用到文本上 |
![]() |
次要 | secondary 颜色将 palette.secondary.main 主题颜色应用到文本上 |
![]() |
文本主要 | textPrimary 颜色将 palette.text.primary 主题颜色应用到文本上 |
![]() |
文本次要 | textSecondary 颜色将 palette.text.secondary 主题颜色应用到文本上 |
![]() |
相关内容
-
Typography
演示:material-ui.com/style/typography/
-
Typography
API 文档:material-ui.com/api/typography/
对齐文本
在用户界面中对齐文本是常见的。不幸的是,这并不容易。使用 Material-UI 网格和排版,你可以创建抽象,使对齐文本变得稍微容易一些。
如何做到...
如果你试图将文本水平对齐到左、右或居中,那么你可以使用 Typography
组件的 align
属性,如下面的代码所示:
<Typography align="center">My Centered Text</Typography>
这是一种使用 text-align
样式的简写,这样你就不必为更常见的对齐场景不断向你的组件添加 CSS。然而,有时你需要能够水平和垂直地对齐你的文本。
例如,假设你有一个 200x200 的 Paper
元素,你需要能够在右下角渲染文本。让我们通过一些代码来演示这个例子:
import React from 'react';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
import Grid from '@material-ui/core/Grid';
const styles = theme => ({
paper: {
width: 200,
height: 200,
padding: theme.spacing(1)
}
});
const MyPaper = withStyles(styles)(
({ horizontalAlign, verticalAlign, classes, ...props }) => (
<Grid
container
component={Paper}
className={classes.paper}
alignContent={verticalAlign}
justify={horizontalAlign}
{...props}
/>
)
);
const MyTypography = ({ ...props }) => (
<Grid item component={Typography} {...props} />
);
const AligningText = ({ ...props }) => (
<MyPaper {...props}>
<MyTypography {...props}>Text</MyTypography>
</MyPaper>
);
export default AligningText;
当屏幕首次加载时,你会看到以下内容:
它是如何工作的...
Storybook 中有两个用于对齐文本的控制,如下所示:
水平对齐控制会改变传递给 MyPaper
组件的 horizontalAlign
属性。同样,垂直对齐控制会改变 verticalAlign
属性的值。horizontalAlign
值传递给 Grid
组件的 justify
属性,而 verticalAlign
属性则传递给 alignContent
属性。
Grid
组件的妙处在于你可以传递一个 component
属性,这将渲染默认渲染的 div
元素。换句话说,你可以将 Paper
组件变成一个网格容器,而你要对齐的 Typography
组件则变成一个网格项。你不需要先渲染 Grid
组件,然后再将实际内容作为子元素渲染。你可以使你的内容成为网格。
当你设置 justify="center"
和 alignContent="flex-end"
时,网格看起来是这样的:
这里展示了当你设置 justify="flex-end"
和 alignContent="flex-start"
时它看起来是什么样子:
参见
-
Typography
演示:material-ui.com/style/typography/
-
Typography
API 文档:material-ui.com/api/typography/
文本换行
你在应用程序中用于渲染文本的 Typography
组件需要知道文本换行的情况。这意味着,当没有足够的空间渲染一行文本时,它将继续到下一行。如果你没有预料到文本可能会换行,这可能会导致不理想的布局后果。
如何做到这一点...
让我们看看一个例子,其中你有两个 Paper
组件,它们使用 Typography
组件来渲染文本:
import React, { Fragment } from 'react';
import clsx from 'clsx';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
const styles = theme => ({
paper: {
minWidth: 300,
padding: theme.spacing(2),
margin: theme.spacing(3)
},
fixedHeight: { height: 100 },
responsive: {
[theme.breakpoints.down('xs')]: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}
});
const WrappingText = withStyles(styles)(({ classes }) => (
<Fragment>
<Paper className={classes.paper}>
<Typography noWrap>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut labore
</Typography>
</Paper>
<Paper className={clsx(classes.paper, classes.fixedHeight)}>
<Typography className={classes.responsive}>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque
ipsa quae ab illo inventore veritatis et quasi architecto
beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem
quia voluptas sit aspernatur aut odit aut fugit, sed quia
consequuntur magni dolores eos qui ratione voluptatem sequi
nesciunt.
</Typography>
</Paper>
</Fragment>
));
export default WrappingText;
当屏幕首次加载时,你会看到以下内容:
第一个 Paper
组件没有设置 height
组件,并且只有一行文本适合当前屏幕宽度。第二个 Paper
组件设置了 height
,第二个 Paper
组件中的文本被换行,以便适合屏幕。
它是如何工作的...
现在,让我们尝试更改屏幕分辨率,使可用于渲染文本的可用宽度更小。你会看到以下内容:
两个Paper
组件中都有换行问题。在第一个中,换行文本导致组件的高度改变,因为它没有固定的高度。这可能会对布局产生多米诺效应,这可能是问题也可能是问题,这取决于你的设计。在第二个Paper
组件中,height
是固定的,这意味着换行文本会溢出组件,看起来很糟糕。
还有更多...
让我们修复这个例子中两个Paper
组件中的文本换行问题。以下是一个修改后的版本:
import React, { Fragment } from 'react';
import clsx from 'clsx';
import { withStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import Paper from '@material-ui/core/Paper';
const styles = theme => ({
paper: {
minWidth: 300,
padding: theme.spacing(2),
margin: theme.spacing(3)
},
fixedHeight: { height: 100 },
responsive: {
[theme.breakpoints.down('xs')]: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}
}
});
const WrappingText = withStyles(styles)(({ classes }) => (
<Fragment>
<Paper className={classes.paper}>
<Typography noWrap>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed
do eiusmod tempor incididunt ut labore
</Typography>
</Paper>
<Paper className={clsx(classes.paper, classes.fixedHeight)}>
<Typography className={classes.responsive}>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque
ipsa quae ab illo inventore veritatis et quasi architecto
beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem
quia voluptas sit aspernatur aut odit aut fugit, sed quia
consequuntur magni dolores eos qui ratione voluptatem sequi
nesciunt.
</Typography>
</Paper>
</Fragment>
));
export default WrappingText;
现在,当你缩小屏幕宽度时,这两个组件看起来是这样的:
第一个Paper
组件通过向Typography
组件添加noWrap
属性来修复。这将确保组件的height
组件永远不会改变,通过隐藏文本溢出并添加省略号来表示文本已被截断。这是因为你知道这只是一行文本,在更宽的显示上永远不会需要换行。另一方面,第二个Paper
组件需要不同的方法,因为它确实需要换行的能力。
解决方案是使用 Material-UI 的媒体查询功能。调用theme.breakpoints.down('xs')
会导致一个以媒体查询为前缀的类名,在这种情况下,是xs
。现在,当屏幕宽度缩小到xs
断点时,应用于组件的noWrap
属性的相同样式被应用。
参见
-
Typography
演示:material-ui.com/style/typography/
-
Typography
API 文档:material-ui.com/api/typography/
第十八章:图标 - 增强图标以匹配您的外观和感觉
在本章中,你将学习以下内容:
-
着色图标
-
缩放图标
-
动态加载图标
-
主题图标
-
安装更多图标
简介
图标在任何 Material-UI 应用程序中都扮演着重要角色。即使你没有打算明确使用它们,许多组件默认情况下也会使用图标。如果一个 Material-UI 组件默认不使用图标,你通常可以找到直接支持集成 Material-UI 图标的选项。图标在应用程序的可用性中发挥着重要作用——它们提供了一种快速扫描屏幕以获取意义的方法,而不是需要不断解析文本。
着色图标
Material-UI 图标组件接受一个color
属性,该属性接受一个命名主题颜色并将其应用到图标上。
如何做到...
此示例使用 Storybook 控件来更改渲染的图标的color
属性:
下面是一段代码,展示了如何使用所选的color
值渲染多个图标:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Cast from '@material-ui/icons/Cast';
import CastConnected from '@material-ui/icons/CastConnected';
import CastForEducation from '@material-ui/icons/CastForEducation';
import Computer from '@material-ui/icons/Computer';
import DesktopMac from '@material-ui/icons/DesktopMac';
import DesktopWindows from '@material-ui/icons/DesktopWindows';
import DeveloperBoard from '@material-ui/icons/DeveloperBoard';
import DeviceHub from '@material-ui/icons/DeviceHub';
import DeviceUnknown from '@material-ui/icons/DeviceUnknown';
import DevicesOther from '@material-ui/icons/DevicesOther';
import Dock from '@material-ui/icons/Dock';
import Gamepad from '@material-ui/icons/Gamepad';
const styles = theme => ({
icon: { margin: theme.spacing(3) }
});
const IconColorAndState = withStyles(styles)(({ color, classes }) => (
<Fragment>
<Cast className={classes.icon} color={color} />
<CastConnected className={classes.icon} color={color} />
<CastForEducation className={classes.icon} color={color} />
<Computer className={classes.icon} color={color} />
<DesktopMac className={classes.icon} color={color} />
<DesktopWindows className={classes.icon} color={color} />
<DeveloperBoard className={classes.icon} color={color} />
<DeviceHub className={classes.icon} color={color} />
<DeviceUnknown className={classes.icon} color={color} />
<DevicesOther className={classes.icon} color={color} />
<Dock className={classes.icon} color={color} />
<Gamepad className={classes.icon} color={color} />
</Fragment>
));
export default IconColorAndState;
它是如何工作的...
color
属性默认为inherit
,这意味着图标将与它们的父组件颜色相同。让我们看看不同的颜色值,并看看示例中的这些图标看起来如何:
继承 | inherit 颜色值将使用父组件样式的颜色值: |
![]() |
---|---|---|
主要 | primary 颜色将palette.primary.main 主题颜色应用到图标上: |
![]() |
次要 | secondary 颜色将palette.secondary.main 主题颜色应用到图标上: |
![]() |
操作 | action 颜色将palette.action.active 主题颜色应用到图标上: |
![]() |
错误 | error 颜色将palette.error.main 主题颜色应用到图标上: |
![]() |
禁用 | disabled 颜色将palette.action.disabled 主题颜色应用到图标上: |
![]() |
相关内容
-
图标 API 文档:
material-ui.com/api/icon/
缩放图标
Material-UI 图标组件的fontSize
属性接受一个表示预定义图标大小的字符串值。这个属性被称为fontSize
而不是size
的原因是,fontSize
CSS 属性决定了图标的大小。默认值为24px
。
如何做到...
此示例使用 Storybook 控件来更改渲染的图标的fontSize
属性:
下面是一段代码,展示了如何使用所选的fontSize
值渲染多个图标:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Cast from '@material-ui/icons/Cast';
import CastConnected from '@material-ui/icons/CastConnected';
import CastForEducation from '@material-ui/icons/CastForEducation';
import Computer from '@material-ui/icons/Computer';
import DesktopMac from '@material-ui/icons/DesktopMac';
import DesktopWindows from '@material-ui/icons/DesktopWindows';
import DeveloperBoard from '@material-ui/icons/DeveloperBoard';
import DeviceHub from '@material-ui/icons/DeviceHub';
import DeviceUnknown from '@material-ui/icons/DeviceUnknown';
import DevicesOther from '@material-ui/icons/DevicesOther';
import Dock from '@material-ui/icons/Dock';
import Gamepad from '@material-ui/icons/Gamepad';
const styles = theme => ({
icon: { margin: theme.spacing(3) }
});
const ScalingIcons = withStyles(styles)(({ fontSize, classes }) => (
<Fragment>
<Cast className={classes.icon} fontSize={fontSize} />
<CastConnected className={classes.icon} fontSize={fontSize} />
<CastForEducation className={classes.icon} fontSize={fontSize} />
<Computer className={classes.icon} fontSize={fontSize} />
<DesktopMac className={classes.icon} fontSize={fontSize} />
<DesktopWindows className={classes.icon} fontSize={fontSize} />
<DeveloperBoard className={classes.icon} fontSize={fontSize} />
<DeviceHub className={classes.icon} fontSize={fontSize} />
<DeviceUnknown className={classes.icon} fontSize={fontSize} />
<DevicesOther className={classes.icon} fontSize={fontSize} />
<Dock className={classes.icon} fontSize={fontSize} />
<Gamepad className={classes.icon} fontSize={fontSize} />
</Fragment>
));
export default ScalingIcons;
它是如何工作的...
fontSize
的默认值是default
。让我们浏览 Material-UI 图标的不同大小选项,看看它们的外观。
默认
default
值将图标大小设置为 24 像素:
继承
inherit
值将图标设置为父组件设置的fontSize
。在这个例子中,图标继承了 16 像素作为fontSize
:
小
small
值将图标大小设置为 20 像素:
大
large
值将图标大小设置为 36 像素:
相关内容
-
图标 API 文档:
material-ui.com/api/icon/
动态加载图标
在只有少量图标的屏幕上,你可以直接将它们作为组件导入而不会出现任何问题。如果你有一个图标很多的屏幕,或者你的应用程序整体上使用了大量图标(后者会增加包的大小),这可能会很具挑战性。在这两种情况下,答案都是懒加载/动态加载 Material-UI 图标。
如何实现...
你可以利用 React 的lazy()
高阶组件。同时,React 的Suspense
组件在lazy
组件被检索和渲染时在你的 UI 中提供占位符。这种整体方法就是 React 中处理代码拆分的方式——Material-UI 图标恰好是一个很好的用例。
这个例子使用 Storybook 控件来选择要加载的图标类别:
这是创建动态加载的lazy icon
组件的代码:
import React, { lazy, Suspense, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import CircularProgress from '@material-ui/core/CircularProgress';
const categories = {
Action: [
lazy(() => import('@material-ui/icons/ThreeDRotation')),
lazy(() => import('@material-ui/icons/Accessibility')),
lazy(() => import('@material-ui/icons/AccessibilityNew')),
lazy(() => import('@material-ui/icons/Accessible')),
lazy(() => import('@material-ui/icons/AccessibleForward')),
lazy(() => import('@material-ui/icons/AccountBalance')),
lazy(() => import('@material-ui/icons/AccountBalanceWallet')),
lazy(() => import('@material-ui/icons/AccountBox')),
lazy(() => import('@material-ui/icons/AccountCircle'))
],
Alert: [
lazy(() => import('@material-ui/icons/AddAlert')),
lazy(() => import('@material-ui/icons/Error')),
lazy(() => import('@material-ui/icons/ErrorOutline')),
lazy(() => import('@material-ui/icons/NotificationImportant')),
lazy(() => import('@material-ui/icons/Warning'))
],
Av: [
lazy(() => import('@material-ui/icons/FourK')),
lazy(() => import('@material-ui/icons/AddToQueue')),
lazy(() => import('@material-ui/icons/Airplay')),
lazy(() => import('@material-ui/icons/Album')),
lazy(() => import('@material-ui/icons/ArtTrack')),
lazy(() => import('@material-ui/icons/AvTimer')),
lazy(() => import('@material-ui/icons/BrandingWatermark')),
lazy(() => import('@material-ui/icons/CallToAction')),
lazy(() => import('@material-ui/icons/ClosedCaption'))
],
Communication: [
lazy(() => import('@material-ui/icons/AlternateEmail')),
lazy(() => import('@material-ui/icons/Business')),
lazy(() => import('@material-ui/icons/Call')),
lazy(() => import('@material-ui/icons/CallEnd')),
lazy(() => import('@material-ui/icons/CallMade')),
lazy(() => import('@material-ui/icons/CallMerge')),
lazy(() => import('@material-ui/icons/CallMissed')),
lazy(() => import('@material-ui/icons/CallMissedOutgoing')),
lazy(() => import('@material-ui/icons/CallReceived'))
]
};
const styles = theme => ({
icon: { margin: theme.spacing(3) }
});
const DynamicallyLoadingIcons = withStyles(styles)(
({ category, classes }) => (
<Suspense fallback={<CircularProgress />}>
{categories[category].map((Icon, index) => (
<Icon key={index} className={classes.icon} />
))}
</Suspense>
)
);
export default DynamicallyLoadingIcons;
当屏幕首次加载时,你会看到以下内容:
如果你选择Av
类别,你会看到以下内容:
工作原理...
lazy()
函数接收一个返回import()
调用的函数。它返回一个lazy
组件:
const LazyIcon = lazy(() => import('@material-ui/icons/ThreeDRotation'))
这段代码实际上并没有import
ThreeDRotation
图标。它构建了一个新组件,在渲染时导入图标。例如,以下代码将导致图标被导入:
<LazyIcon />
你可以在运行此示例时查看dev
工具中的网络标签来亲自看到这一点。默认选择Action
类别,因此你可以看到加载正在渲染的lazy
组件的网络请求:
然后,如果你将选择的类别更改为Communication
,你会看到更多网络请求来加载这个类别中正在渲染的懒加载图标:
相关内容
-
图标 API 文档:
material-ui.com/api/icon/
主题图标
Material-UI 图标有可以应用于它们的主题。它们不要与应用于你使用的每个 Material-UI 组件的样式主题混淆;图标主题专门用于图标。要使用主题图标,你必须导入它的不同版本。
如何操作...
为了帮助探索不同的图标主题,此示例使用了一个 Storybook 控件,允许你更改图标主题:
这是源代码:
import React, { lazy, Suspense, Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import CircularProgress from '@material-ui/core/CircularProgress';
const themes = {
Filled: [
lazy(() => import('@material-ui/icons/Attachment')),
lazy(() => import('@material-ui/icons/Cloud')),
lazy(() => import('@material-ui/icons/CloudCircle')),
lazy(() => import('@material-ui/icons/CloudDone')),
lazy(() => import('@material-ui/icons/CloudDownload')),
lazy(() => import('@material-ui/icons/CloudOff')),
lazy(() => import('@material-ui/icons/CloudQueue')),
lazy(() => import('@material-ui/icons/CloudUpload')),
lazy(() => import('@material-ui/icons/CreateNewFolder')),
lazy(() => import('@material-ui/icons/Folder')),
lazy(() => import('@material-ui/icons/FolderOpen')),
lazy(() => import('@material-ui/icons/FolderShared'))
],
Outlined: [
lazy(() => import('@material-ui/icons/AttachmentOutlined')),
lazy(() => import('@material-ui/icons/CloudOutlined')),
lazy(() => import('@material-ui/icons/CloudCircleOutlined')),
lazy(() => import('@material-ui/icons/CloudDoneOutlined')),
lazy(() => import('@material-ui/icons/CloudDownloadOutlined')),
lazy(() => import('@material-ui/icons/CloudOffOutlined')),
lazy(() => import('@material-ui/icons/CloudQueueOutlined')),
lazy(() => import('@material-ui/icons/CloudUploadOutlined')),
lazy(() => import('@material-ui/icons/CreateNewFolderOutlined')),
lazy(() => import('@material-ui/icons/FolderOutlined')),
lazy(() => import('@material-ui/icons/FolderOpenOutlined')),
lazy(() => import('@material-ui/icons/FolderSharedOutlined'))
],
Rounded: [
lazy(() => import('@material-ui/icons/AttachmentRounded')),
lazy(() => import('@material-ui/icons/CloudRounded')),
lazy(() => import('@material-ui/icons/CloudCircleRounded')),
lazy(() => import('@material-ui/icons/CloudDoneRounded')),
lazy(() => import('@material-ui/icons/CloudDownloadRounded')),
lazy(() => import('@material-ui/icons/CloudOffRounded')),
lazy(() => import('@material-ui/icons/CloudQueueRounded')),
lazy(() => import('@material-ui/icons/CloudUploadRounded')),
lazy(() => import('@material-ui/icons/CreateNewFolderRounded')),
lazy(() => import('@material-ui/icons/FolderRounded')),
lazy(() => import('@material-ui/icons/FolderOpenRounded')),
lazy(() => import('@material-ui/icons/FolderSharedRounded'))
],
TwoTone: [
lazy(() => import('@material-ui/icons/AttachmentTwoTone')),
lazy(() => import('@material-ui/icons/CloudTwoTone')),
lazy(() => import('@material-ui/icons/CloudCircleTwoTone')),
lazy(() => import('@material-ui/icons/CloudDoneTwoTone')),
lazy(() => import('@material-ui/icons/CloudDownloadTwoTone')),
lazy(() => import('@material-ui/icons/CloudOffTwoTone')),
lazy(() => import('@material-ui/icons/CloudQueueTwoTone')),
lazy(() => import('@material-ui/icons/CloudUploadTwoTone')),
lazy(() => import('@material-ui/icons/CreateNewFolderTwoTone')),
lazy(() => import('@material-ui/icons/FolderTwoTone')),
lazy(() => import('@material-ui/icons/FolderOpenTwoTone')),
lazy(() => import('@material-ui/icons/FolderSharedTwoTone'))
],
Sharp: [
lazy(() => import('@material-ui/icons/AttachmentSharp')),
lazy(() => import('@material-ui/icons/CloudSharp')),
lazy(() => import('@material-ui/icons/CloudCircleSharp')),
lazy(() => import('@material-ui/icons/CloudDoneSharp')),
lazy(() => import('@material-ui/icons/CloudDownloadSharp')),
lazy(() => import('@material-ui/icons/CloudOffSharp')),
lazy(() => import('@material-ui/icons/CloudQueueSharp')),
lazy(() => import('@material-ui/icons/CloudUploadSharp')),
lazy(() => import('@material-ui/icons/CreateNewFolderSharp')),
lazy(() => import('@material-ui/icons/FolderSharp')),
lazy(() => import('@material-ui/icons/FolderOpenSharp')),
lazy(() => import('@material-ui/icons/FolderSharedSharp'))
]
};
const styles = theme => ({
icon: { margin: theme.spacing(3) }
});
const ThemedIcons = withStyles(styles)(({ theme, classes }) => (
<Suspense fallback={<CircularProgress />}>
{themes[theme].map((Icon, index) => (
<Icon fontSize="large" key={index} className={classes.icon} />
))}
</Suspense>
));
export default ThemedIcons;
它是如何工作的...
如果你查看 themes
对象,你会看到每个主题都有相同的图标,但它们的 import
路径略有不同。例如,Attachment
图标是由 Filled
主题导入的,如下所示:
import('@material-ui/icons/Attachment')
在 Rounded
主题中,导入相同图标的示例如下:
import('@material-ui/icons/AttachmentOutlined')
你通过将主题名称附加到图标名称来更改图标的主题。每个都遵循相同的模式。
并非每个图标在主题更改时都会改变。这实际上完全取决于图标形状以及是否与给定主题相匹配。导入仍然会工作,但并不总是会有视觉上的变化。
让我们一起来探索它们:
填充 | Filled 主题是默认主题。以下是将其应用于示例时的外观: |
![]() |
---|---|---|
线框 | 查看前面的 Filled 主题——注意,一些图标实际上是默认轮廓的。以下是 Outlined 主题应用于示例时的外观: |
![]() |
圆角 | 这是将 Rounded 主题应用于示例时的外观: |
![]() |
双色调 | 这是将 TwoTone 主题应用于示例时的外观: |
![]() |
锋利 | 这是将 Sharp 主题应用于示例时的外观: |
![]() |
相关内容
-
图标 API 文档:
material-ui.com/api/icon/
安装更多图标
mdi-material-ui
包提供了大量的图标,你可以像使用内置图标一样,在你的 Material-UI 应用程序中使用它们。
如何操作...
第一步是安装包并将其添加到你的项目中:
npm install --save mdi-material-ui
现在,你已经准备好从这个包中 import
图标并使用它们:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Apple from 'mdi-material-ui/Apple';
import Facebook from 'mdi-material-ui/Facebook';
import Google from 'mdi-material-ui/Google';
import Hulu from 'mdi-material-ui/Hulu';
import Linkedin from 'mdi-material-ui/Linkedin';
import Lyft from 'mdi-material-ui/Lyft';
import Microsoft from 'mdi-material-ui/Microsoft';
import Netflix from 'mdi-material-ui/Netflix';
import Npm from 'mdi-material-ui/Npm';
import Reddit from 'mdi-material-ui/Reddit';
import Twitter from 'mdi-material-ui/Twitter';
import Uber from 'mdi-material-ui/Uber';
const styles = theme => ({
icon: { margin: theme.spacing(3) }
});
const InstallingMoreIcons = withStyles(styles)(({ classes }) => (
<Fragment>
<Apple className={classes.icon} />
<Facebook className={classes.icon} />
<Google className={classes.icon} />
<Hulu className={classes.icon} />
<Linkedin className={classes.icon} />
<Lyft className={classes.icon} />
<Microsoft className={classes.icon} />
<Netflix className={classes.icon} />
<Npm className={classes.icon} />
<Reddit className={classes.icon} />
<Twitter className={classes.icon} />
<Uber className={classes.icon} />
</Fragment>
));
export default InstallingMoreIcons;
这是加载屏幕时图标的外观:
它是如何工作的...
来自 mdi-material-design
的图标与 @material-ui/icons
的图标类似。它们被导入并作为 React 组件渲染。您可以通过在 materialdesignicons.com/
上查找来找到您需要的图标名称。在任何可以使用官方 Material-UI 图标的地方,例如在按钮中,您也可以使用 mdi-material-ui
的图标。
参见
-
材料设计图标包:
materialdesignicons.com/
-
图标 API 文档:
material-ui.com/api/icon/
第十九章:主题 - 中心化您的应用的外观和感觉
在本章中,您将学习以下内容:
-
理解调色板
-
比较浅色和深色主题
-
自定义字体
-
嵌套主题
-
理解组件主题设置
简介
所有 Material-UI 应用程序在某种程度上都共享一个共同的外观和感觉。这并不意味着您的银行应用程序会与我的音乐库应用程序看起来和感觉一样,仅仅因为我们都在使用相同的库。共同之处在于,两个应用程序都遵循 Material Design 原则。在这里,我不会深入探讨 Material Design,因为已经有大量资源做得比我好得多。相反,我想强调的是,Material-UI 应用程序可以以高度灵活的方式主题化,而无需牺牲 Material Design 的原则。
理解调色板
大多数人在构建新的 Material-UI 主题时,首先考虑的是调色板。调色板可能非常复杂,包含许多可变部分:Material-UI 主题也不例外,但 Material-UI 隐藏了很多复杂性。您需要关注主题的色彩意图,而 Material-UI 则使用这些色彩意图在必要时计算其他颜色。以下内容直接摘自 Material-UI 主题文档,意图如下:
-
主要:用于表示主要界面元素
-
二级:用于表示二级界面元素
-
错误:用于表示用户应了解的界面元素
如何操作...
让我们构建一个新的主题,使用 Material-UI 内置的颜色对象设置色彩意图。为了帮助调整您的主题,此示例使用了色调和阴影 Storybook 控件:
三个色彩意图以标签形式显示在顶部。目前 PRIMARY 意图被选中,它有一个色调选择器和阴影数值范围。每个意图都有相同的控件。色调选择器中填充了可以从 Material-UI import
的相同颜色:
下面是使用这些 Storybook 控件构建新主题并渲染一些 Button
和 Typography
组件的源代码:
import React, { Fragment } from 'react';
import {
withStyles,
createMuiTheme,
MuiThemeProvider
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
import red from '@material-ui/core/colors/red';
import pink from '@material-ui/core/colors/pink';
import purple from '@material-ui/core/colors/purple';
import deepPurple from '@material-ui/core/colors/deepPurple';
import indigo from '@material-ui/core/colors/indigo';
import blue from '@material-ui/core/colors/blue';
import lightBlue from '@material-ui/core/colors/lightBlue';
import cyan from '@material-ui/core/colors/cyan';
import teal from '@material-ui/core/colors/teal';
import green from '@material-ui/core/colors/green';
import lightGreen from '@material-ui/core/colors/lightGreen';
import lime from '@material-ui/core/colors/lime';
import yellow from '@material-ui/core/colors/yellow';
import amber from '@material-ui/core/colors/amber';
import orange from '@material-ui/core/colors/orange';
import deepOrange from '@material-ui/core/colors/deepOrange';
import brown from '@material-ui/core/colors/brown';
import grey from '@material-ui/core/colors/grey';
import blueGrey from '@material-ui/core/colors/blueGrey';
const styles = theme => ({
button: { margin: theme.spacing(2) }
});
const hues = {
red,
pink,
purple,
deepPurple,
indigo,
blue,
lightBlue,
cyan,
teal,
green,
lightGreen,
lime,
yellow,
amber,
orange,
deepOrange,
brown,
grey,
blueGrey
};
const UnderstandingThePallette = withStyles(styles)(
({
primaryHue,
primaryShade,
secondaryHue,
secondaryShade,
errorHue,
errorShade,
classes
}) => {
const theme = createMuiTheme({
palette: {
primary: { main: hues[primaryHue][primaryShade] },
secondary: { main: hues[secondaryHue][secondaryShade] },
error: { main: hues[errorHue][errorShade] }
}
});
return (
<MuiThemeProvider theme={theme}>
<Button className={classes.button} variant="contained">
Default
</Button>
<Button
className={classes.button}
variant="contained"
color="primary"
>
Primary
</Button>
<Button
className={classes.button}
variant="contained"
color="secondary"
>
Secondary
</Button>
<Typography className={classes.button} color="error">
Error
</Typography>
</MuiThemeProvider>
);
}
);
export default UnderstandingThePallette;
当您首次加载选择默认主题值的屏幕时,您将看到以下内容:
现在,让我们更改默认主题的色彩意图,从 PRIMARY 开始:
主要色调现在为青色,阴影值为 300。接下来,我们将更改二级意图:
二级色调现在为青绿色,阴影值为 100。最后,我们将更改错误意图:
对于这个主题,错误色调仍然是红色,但稍微浅一些,阴影值为 400。以下是最终结果:
它是如何工作的...
Material-UI 有核心的hues
可以导入,可以帮助你构建你的主题:
import red from '@material-ui/core/colors/red';
import pink from '@material-ui/core/colors/pink';
import purple from '@material-ui/core/colors/purple';
import deepPurple from '@material-ui/core/colors/deepPurple';
import indigo from '@material-ui/core/colors/indigo';
import blue from '@material-ui/core/colors/blue';
import lightBlue from '@material-ui/core/colors/lightBlue';
import cyan from '@material-ui/core/colors/cyan';
import teal from '@material-ui/core/colors/teal';
import green from '@material-ui/core/colors/green';
import lightGreen from '@material-ui/core/colors/lightGreen';
import lime from '@material-ui/core/colors/lime';
import yellow from '@material-ui/core/colors/yellow';
import amber from '@material-ui/core/colors/amber';
import orange from '@material-ui/core/colors/orange';
import deepOrange from '@material-ui/core/colors/deepOrange';
import brown from '@material-ui/core/colors/brown';
import grey from '@material-ui/core/colors/grey';
import blueGrey from '@material-ui/core/colors/blueGrey';
你不必import
每一个色调——这里这样做是因为 Storybook 控件可以动态更改调色板值。每个导入的颜色值都是一个按色调值(例如 500)索引的对象。例如,这些值是以十六进制表示的颜色,例如#fffffff
。当使用以十六进制表示的颜色时,你必须在创建theme
时将其传递给main
属性:
const theme = createMuiTheme({
palette: {
primary: { main: hues[primaryHue][primaryShade] },
secondary: { main: hues[secondaryHue][secondaryShade] },
error: { main: hues[errorHue][errorShade] }
}
});
属性primaryHue
、primaryShade
等是由 Storybook 控件设置的值。MuiThemeProvider
组件是将theme
实际应用于你的 Material-UI 组件的方式。它不必是应用程序的根组件,但任何依赖于主题样式的 Material-UI 组件(如Button
、Typography
等)都需要是这个组件的子组件。
每当在这个例子中渲染main
应用程序组件时,都会调用createMuiTheme()
函数。实际上,这种情况不应该发生。相反,主题应该只创建一次,然后传递给MuiThemeProvider
组件。这里之所以这样做,是为了当使用 Storybook 控件更改颜色值时,主题能够更新。
参见
-
Material-UI
theme
文档:material-ui.com/customization/themes/
-
Material-UI
color
文档:material-ui.com/style/color/
比较亮色和暗色主题
主题的颜色调色板接受一个type
属性值,可以是亮色或暗色。默认情况下,主题是亮色。将主题更改为暗色不会改变你的主题的其他调色板值(如primary
、secondary
、error
)。
如何实现...
让我们创建一个暗色主题和一个亮色主题。这两个主题都将使用相同的颜色值(如primary
、secondary
、error
)来表示意图。示例将使用 Storybook 控件来更改主题:
这里是使用此值在light
和dark
主题之间进行选择并将其应用于 Material-UI 组件的源代码:
import React, { Fragment } from 'react';
import {
withStyles,
createMuiTheme,
MuiThemeProvider
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import red from '@material-ui/core/colors/red';
import pink from '@material-ui/core/colors/pink';
import blue from '@material-ui/core/colors/blue';
const styles = theme => ({
button: { margin: theme.spacing(2) }
});
const light = createMuiTheme({
palette: {
type: 'light',
primary: blue,
secondary: pink,
error: { main: red[600] }
}
});
const dark = createMuiTheme({
palette: {
type: 'dark',
primary: blue,
secondary: pink,
error: { main: red[600] }
}
});
const LightVersusDarkThemes = withStyles(styles)(
({ themeType, classes }) => {
return (
<MuiThemeProvider theme={{ dark, light }[themeType]}>
<Dialog open={true}>
<DialogTitle>Use Google's location service?</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Let Google help apps determine location. This means
sending anonymous location data to Google, even when no
apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button color="secondary">Disagree</Button>
<Button variant="contained" color="primary" autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
</MuiThemeProvider>
);
}
);
export default LightVersusDarkThemes;
当屏幕首次加载时,你会看到以下对话框:
这里是主题类型更改为暗色时的相同对话框:
它是如何工作的...
当palette.type
主题值从light
变为dark
时,以下调色板值会发生变化:
-
palette.text
-
palette.divider
-
palette.background
-
palette.action
让我们看看这个例子中使用的两个主题:
const light = createMuiTheme({
palette: {
type: 'light',
primary: blue,
secondary: pink,
error: { main: red[600] }
}
});
const dark = createMuiTheme({
palette: {
type: 'dark',
primary: blue,
secondary: pink,
error: { main: red[600] }
}
});
这两个主题除了 palette.type
值相同之外,其他都一样。每次你更改这个值时,都会为该主题计算新的颜色值。例如,你在对话框中看到的新的文本颜色不是静态的——这是一个由 Material-UI 计算的颜色,以便在文本颜色和背景颜色之间提供最佳对比度。
相关内容
- Material-UI
主题
文档:material-ui.com/customization/themes/
自定义字体
Material-UI 主题的首选 字体
是 Roboto。这绝对不是唯一的选择,实际上,你可以安装新的字体并在自定义 Material-UI 主题中使用它们。
如何做到这一点...
让我们安装几个新的 字体
包,以便它们可用于你的应用程序:
npm install --save typeface-exo typeface-ubuntu
接下来,你可以为示例添加一个 Storybook 控制器,允许你切换主题,从而切换字体:
当你首次加载屏幕时,Dialog
组件看起来是这样的:
当你将字体类型更改为 Exo 时,Dialog
组件看起来是这样的:
最后,当你将字体类型更改为 Ubuntu 时,Dialog
组件看起来是这样的:
它是如何工作的...
在此示例中使用的两种字体已导入:
import 'typeface-exo';
import 'typeface-ubuntu';
在实际应用中,你只需 导入
你活动主题使用的字体,以减少构建的大小。本书中所有示例中使用的 roboto
字体是由 Storybook 的 index
文件导入的,因为这是默认主题字体,并在本书的每个示例中使用。
现在你已经导入了字体,你已使字体家族名称可用于主题:
const roboto = createMuiTheme({
typography: {
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif'
}
});
const exo = createMuiTheme({
typography: {
fontFamily: '"Exo", "Roboto", "Helvetica", "Arial", sans-serif'
}
});
const ubuntu = createMuiTheme({
typography: {
fontFamily: '"Ubuntu", "Roboto", "Helvetica", "Arial", sans-serif'
}
});
注意,在 exo
和 ubuntu
主题中,roboto
仍然作为字体家族的一部分使用,因为它是 Material-UI 的首选字体;它是一个很好的后备选项。
相关内容
- Material-UI
主题
文档:material-ui.com/customization/themes/
嵌套主题
通过嵌套 MuiThemeProvider
组件,你可以将处理主题不同方面的多个主题组合成一个适合在应用程序中使用的单一主题。
如何做到这一点...
假设你有一个设置颜色调板的主题和另一个改变边框半径的主题。你可以通过嵌套 MuiThemeProvider
组件来合并这两个主题。以下是一个例子:
import React from 'react';
import {
createMuiTheme,
MuiThemeProvider
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import red from '@material-ui/core/colors/red';
import pink from '@material-ui/core/colors/pink';
import blue from '@material-ui/core/colors/blue';
const Blue = createMuiTheme({
palette: {
type: 'light',
primary: blue,
secondary: pink,
error: { main: red[600] }
}
});
const Rounded = theme =>
createMuiTheme({
...theme,
shape: {
borderRadius: 8
}
});
const NestingThemes = () => (
<MuiThemeProvider theme={Blue}>
<MuiThemeProvider theme={Rounded}>
<Dialog open={true}>
<DialogTitle>Use Google's location service?</DialogTitle>
<DialogContent>
<DialogContentText>
Let Google help apps determine location. This means
sending anonymous location data to Google, even when no
apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button color="secondary">Disagree</Button>
<Button variant="contained" color="primary" autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
</MuiThemeProvider>
</MuiThemeProvider>
);
export default NestingThemes;
当屏幕加载时,你会看到以下内容:
它是如何工作的...
Blue
主题应用颜色调色板theme
设置,而Rounded
主题更改borderRadius
设置。这两个主题都应用于Dialog
组件——你可以看到蓝色的主要按钮,而且圆角更加圆润。让我们更仔细地看看Rounded
主题:
const Rounded = theme =>
createMuiTheme({
...theme,
shape: {
borderRadius: 8
}
});
Rounded
不是一个对象,而是一个返回theme
对象的函数。当你将函数传递给MuiThemeProvider
的theme
属性时,会传递一个theme
参数。这是外部theme
,或者在这个例子中,是Blue
主题。通过将spread
运算符应用于theme
参数并传递额外的theme
值到createMuiTheme()
,来扩展theme
。
参见
- Material-UI
theme
文档:material-ui.com/customization/themes/
理解组件主题设置
主题可以覆盖特定于组件类型的样式,例如按钮或抽屉。当你需要将样式更改应用到应用中组件的每个实例时,这非常有用。换句话说,样式是整体主题的一部分,但它只应用于一种类型的组件,而不是例如颜色调色板,它几乎适用于所有 Material-UI 组件。
如何操作
假设你想要Dialog
组件的标题和操作居中。由于你想要在应用中的每个Dialog
组件上应用相同的样式,因此theme
是覆盖此设置的合适位置。下面是如何操作的:
import React from 'react';
import {
createMuiTheme,
MuiThemeProvider
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
const theme = createMuiTheme({
overrides: {
MuiDialogTitle: { root: { textAlign: 'center' } },
MuiDialogActions: { root: { justifyContent: 'center' } }
}
});
const ComponentThemeSettings = () => (
<MuiThemeProvider theme={theme}>
<Dialog open={true}>
<DialogTitle>Use Google's location service?</DialogTitle>
<DialogContent>
<DialogContentText>
Let Google help apps determine location. This means sending
anonymous location data to Google, even when no apps are
running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button color="secondary">Disagree</Button>
<Button color="primary" autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
</MuiThemeProvider>
);
export default ComponentThemeSettings;
下面是自定义对话框的外观:
它是如何工作的...
让我们更仔细地看看主题的overrides
部分:
overrides: {
MuiDialogTitle: { root: { textAlign: 'center' } },
MuiDialogActions: { root: { justifyContent: 'center' } }
},
MuiDialogTitle
键对应于DialogTitle
组件,而MuiDialogActions
键对应于DialogActions
组件。两个对象中使用的root
键是规则的名称。在更复杂的组件中,你可以使用这些键来定位组件的特定部分。每个组件的 API 文档详细说明了你可以定位的每个样式规则名称。然后,就是覆盖或提供新的样式的问题。textAlign
属性默认不在DialogTitle
组件上设置,所以你添加了它。justifyContent
设置为DialogActions
组件的右侧,这意味着你正在覆盖一个现有的值。
参见
第二十章:样式 - 将样式应用于组件
在本章中,你将学习以下主题:
-
基本组件样式
-
作用域组件样式
-
扩展组件样式
-
将样式移动到主题中
-
其他样式选项
简介
应用于 Material-UI 组件的大多数样式都是主题样式的一部分。在某些情况下,你可能需要有能力为单个组件设置样式,而不改变主题。例如,一个功能中的按钮可能需要应用特定的样式,而不应该改变应用中的其他按钮。Material-UI 提供了多种方法来为组件整体或组件的特定部分应用自定义样式。
基本组件样式
材料设计使用 JavaScript 样式表(JSS)来为其组件设置样式。你可以使用 Material-UI 提供的实用工具来应用自己的 JSS。
如何做到这一点...
withStyles()
函数是一个高阶函数,它接受一个样式对象作为参数。它返回的函数将用于样式的组件作为参数。以下是一个示例:
import React, { useState } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
const styles = theme => ({
card: {
width: 135,
height: 135,
textAlign: 'center'
},
cardActions: {
justifyContent: 'center'
}
});
const BasicComponentStyles = withStyles(styles)(({ classes }) => {
const [count, setCount] = useState(0);
const onIncrement = () => {
setCount(count + 1);
};
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="h2">{count}</Typography>
</CardContent>
<CardActions className={classes.cardActions}>
<Button size="small" onClick={onIncrement}>
Increment
</Button>
</CardActions>
</Card>
);
});
export default BasicComponentStyles;
这是这个组件的外观:
它是如何工作的...
让我们更仔细地看看这个示例中定义的 styles
:
const styles = theme => ({
card: {
width: 135,
height: 135,
textAlign: 'center'
},
cardActions: {
justifyContent: 'center'
}
});
你传递给 withStyles()
的 styles
可以是一个普通对象,也可以是一个返回普通对象的函数,就像这个示例一样。使用函数的好处是,theme
值作为参数传递给函数,以防你的 styles
需要访问 theme
值。在这个示例中定义了两种样式:card
和 cardActions
。你可以把它们看作是 级联样式表(CSS)类。以下是这两个样式作为 CSS 的样子:
.card {
width: 135
height: 135
text-align: center
}
.cardActions {
justify-content: center
}
通过调用 withStyles(styles)(MyComponent)
,你返回一个新的组件,它有一个 classes
属性。这个对象包含了你现在可以应用于组件的所有类。你不能只是做如下这样的事情:
<Card className="card" />
当你定义你的 styles
时,它们有自己的构建过程,每个类最终都会得到一个自己的生成名称。这个生成名称就是你在 classes
对象中找到的,这就是为什么你会想使用它的原因。
更多...
与返回新组件的高阶函数一起工作,你可以利用 Material-UI 样式钩子。这个示例已经依赖于 React 的 useState()
钩子,所以在这个组件中使用另一个钩子感觉像是现有模式的一个自然扩展。以下是重构后利用 makeStyles()
函数的示例:
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/styles';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';
const useStyles = makeStyles(theme => ({
card: {
width: 135,
height: 135,
textAlign: 'center'
},
cardActions: {
justifyContent: 'center'
}
}));
export default function BasicComponentStyles() {
const classes = useStyles();
const [count, setCount] = useState(0);
const onIncrement = () => {
setCount(count + 1);
};
return (
<Card className={classes.card}>
<CardContent>
<Typography variant="h2">{count}</Typography>
</CardContent>
<CardActions className={classes.cardActions}>
<Button size="small" onClick={onIncrement}>
Increment
</Button>
</CardActions>
</Card>
);
}
useStyles()
钩子是通过 makeStyles()
函数构建的——它接受与 withStyles()
相同的 styles
参数。通过在组件内部调用 useStyles()
,你将拥有你的类对象。另一个需要指出的重要事情是,makeStyles
是从 @material-ui/styles
而不是 @material-ui/core/styles
导入的。
相关内容
- Material-UI CSS in JS 文档:
material-ui.com/css-in-js/basics/
.
作用域组件样式
大多数 Material-UI 组件都有一个针对组件的特定 CSS API。这意味着,您不需要为每个需要定制的组件分配类名到 className
属性,而是可以针对您想要更改的组件的特定方面进行定位。Material-UI 为作用域组件样式奠定了基础;您只需利用这些 API。
如何实现...
假设您有以下想要应用到您应用程序中所有 Button
组件上的样式定制:
-
默认情况下,每个按钮都需要一个边距。
-
每个使用
contained
变体的按钮都应该有额外的顶部和底部填充。 -
每个使用
contained
变体和主要颜色的按钮都应该有额外的顶部和底部填充,以及额外的左右填充。
下面是一个示例,展示了如何使用 Button
CSS API 通过 styles
来针对这三种不同的 Button
类型:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const styles = theme => ({
root: {
margin: theme.spacing(2)
},
contained: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
containedPrimary: {
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4)
}
});
const ScopedComponentStyles = withStyles(styles)(
({ classes: { root, contained, containedPrimary } }) => (
<Fragment>
<Button classes={{ root }}>My Default Button</Button>
<Button classes={{ root, contained }} variant="contained">
My Contained Button
</Button>
<Button
classes={{ root, contained, containedPrimary }}
variant="contained"
color="primary"
>
My Contained Primary Button
</Button>
</Fragment>
)
);
export default ScopedComponentStyles;
下面是三个渲染按钮的样式:
它是如何工作的...
Button
CSS API 接受命名样式并将它们应用到组件上。这些相同的名称也用于此代码中的样式。例如,root
应用于每个 Button
组件,而 contained
只应用于使用 contained
变体和 containedPrimary
样式的 Button
组件,containedPrimary
样式只应用于使用 contained
变体和 primary
颜色的 Button
组件。
还有更多...
每个样式都是从类属性中解构出来的,然后应用到适当的 Button
组件上。然而,您实际上并不需要做所有这些工作。由于 Material-UI CSS API 会以匹配您实际目标的方式将 styles
应用到组件上,因此您可以直接将类传递给按钮,并获得相同的结果。以下是这个示例的简化版本:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const styles = theme => ({
root: {
margin: theme.spacing(2)
},
contained: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
containedPrimary: {
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4)
}
});
const ScopedComponentStyles = withStyles(styles)(({ classes }) => (
<Fragment>
<Button classes={classes}>My Default Button</Button>
<Button classes={classes} variant="contained">
My Contained Button
</Button>
<Button classes={classes} variant="contained" color="primary">
My Contained Primary Button
</Button>
</Fragment>
));
export default ScopedComponentStyles;
输出看起来相同,因为只有符合 CSS API 约束的按钮才会应用样式。例如,第一个 Button
将 root
、contained
和 containedPrimary
样式传递给类属性,但只有 root
被应用,因为它没有使用 contained
变体的 primary
颜色。第二个 Button
也传递了所有三种样式,但只有 root
和 contained
被应用。第三个 Button
因为符合每种样式的标准,所以应用了所有三种样式。
参见
- Material-UI 样式覆盖文档:
material-ui.com/customization/overrides/
.
扩展组件样式
你可以用另一个组件应用的styles
来扩展一个组件应用的styles
。由于你的styles
是 JavaScript 对象,一个选项是将一个样式对象扩展到另一个样式对象上。这种方法的唯一问题是,你会在 CSS 输出中得到大量的重复styles
属性。一个更好的选择是使用jss
扩展插件。
如何做到这一点...
假设你想要渲染三个按钮并共享一些styles
。一种方法是使用jss
扩展插件将通用styles
扩展到更具体的styles
。下面是如何做到这一点:
import React, { Fragment } from 'react';
import { JssProvider, jss } from 'react-jss';
import {
withStyles,
createGenerateClassName
} from '@material-ui/styles';
import {
createMuiTheme,
MuiThemeProvider
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const styles = theme => ({
root: {
margin: theme.spacing(2)
},
contained: {
extend: 'root',
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
containedPrimary: {
extend: 'contained',
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4)
}
});
const App = ({ children }) => (
<JssProvider
jss={jss}
generateClassName={createGenerateClassName()}
>
<MuiThemeProvider theme={createMuiTheme()}>
{children}
</MuiThemeProvider>
</JssProvider>
);
const Buttons = withStyles(styles)(({ classes }) => (
<Fragment>
<Button className={classes.root}>My Default Button</Button>
<Button className={classes.contained} variant="contained">
My Contained Button
</Button>
<Button
className={classes.containedPrimary}
variant="contained"
color="primary"
>
My Contained Primary Button
</Button>
</Fragment>
));
const ExtendingComponentStyles = () => (
<App>
<Buttons />
</App>
);
export default ExtendingComponentStyles;
这是渲染后的按钮外观:
它是如何工作的...
在你的 Material-UI 应用中使用jss
扩展插件的最简单方法是使用默认的 JSS 插件预设,这包括jss
扩展。Material-UI 默认安装了几个 JSS 插件,但jss
扩展不是其中之一。让我们看看这个例子中的App
组件,看看这个 JSS 插件是如何提供的:
const App = ({ children }) => (
<JssProvider
jss={jss}
generateClassName={createGenerateClassName()}
>
<MuiThemeProvider theme={createMuiTheme()}>
{children}
</MuiThemeProvider>
</JssProvider>
);
JssProvider
组件是 JSS 在 Material-UI 应用中启用的方式。通常,你不需要直接与之交互,但在添加新的 JSS 插件时这是必要的。jss
属性接受包含jss extend
插件的 JSS 预设对象。generateClassName
属性接受一个来自 Material-UI 的函数,该函数有助于生成特定于 Material-UI 的类名。
接下来,让我们更仔细地看看一些styles
:
const styles = theme => ({
root: {
margin: theme.spacing(2)
},
contained: {
extend: 'root',
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
containedPrimary: {
extend: 'contained',
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4)
}
});
extend
属性接受你想要扩展的样式的名称。在这种情况下,contained
样式扩展了root
。containedPrimary
扩展了contained
和root
。现在让我们看看这如何转换为 CSS。这是root
样式的样子:
.Component-root-1 {
margin: 16px;
}
接下来,这是包含
样式:
.Component-contained-2 {
margin: 16px;
padding-top: 16px;
padding-bottom: 16px;
}
最后,这是containedPrimary
样式:
.Component-containedPrimary-3 {
margin: 16px;
padding-top: 16px;
padding-left: 32px;
padding-right: 32px;
padding-bottom: 16px;
}
注意,更通用的属性中的属性包含在更具体的样式里。有一些属性是重复的,但在 CSS 中,你不需要重复 JavaScript 对象属性。此外,你还可以将这些扩展的styles
放在代码库的更中心位置,这样多个组件就可以使用它们。
参见
- Material-UI JSS 文档:
material-ui.com/customization/css-in-js/
。
将样式移动到主题中
随着你开发 Material-UI 应用,你将开始注意到重复出现的样式模式。特别是,应用于一种类型组件(如按钮)的样式会演变成一个主题。
如何做到这一点...
让我们回顾一下来自作用域组件样式部分的例子:
import React, { Fragment } from 'react';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const styles = theme => ({
root: {
margin: theme.spacing(2)
},
contained: {
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2)
},
containedPrimary: {
paddingLeft: theme.spacing(4),
paddingRight: theme.spacing(4)
}
});
const ScopedComponentStyles = withStyles(styles)(({ classes }) => (
<Fragment>
<Button classes={classes}>My Default Button</Button>
<Button classes={classes} variant="contained">
My Contained Button
</Button>
<Button classes={classes} variant="contained" color="primary">
My Contained Primary Button
</Button>
</Fragment>
));
export default ScopedComponentStyles;
这是应用了这些样式后的按钮外观:
现在,假设你已经在应用程序的几个地方实现了这些相同的样式,因为这是你希望按钮看起来的方式。在这个时候,你已经将简单的组件定制发展成了一个主题。当这种情况发生时,你不需要反复实现相同的样式。相反,应该通过使用正确的组件和正确的属性值来自动应用样式。让我们将这些样式移动到theme
中:
import React from 'react';
import {
createMuiTheme,
MuiThemeProvider
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const defaultTheme = createMuiTheme();
const theme = createMuiTheme({
overrides: {
MuiButton: {
root: {
margin: 16
},
contained: {
paddingTop: defaultTheme.spacing(2),
paddingBottom: defaultTheme.spacing(2)
},
containedPrimary: {
paddingLeft: defaultTheme.spacing(4),
paddingRight: defaultTheme.spacing(4)
}
}
}
});
const MovingStylesToThemes = ({ classes }) => (
<MuiThemeProvider theme={theme}>
<Button>My Default Button</Button>
<Button variant="contained">My Contained Button</Button>
<Button variant="contained" color="primary">
My Contained Primary Button
</Button>
</MuiThemeProvider>
);
export default MovingStylesToThemes;
现在,你可以在不每次都应用相同样式的条件下使用Button
组件。
它是如何工作的...
让我们更仔细地看看你的样式是如何融入 Material-UI 主题的:
overrides: {
MuiButton: {
root: {
margin: 16
},
contained: {
paddingTop: defaultTheme.spacing(2),
paddingBottom: defaultTheme.spacing(2)
},
containedPrimary: {
paddingLeft: defaultTheme.spacing(4),
paddingRight: defaultTheme.spacing(4)
}
}
}
overrides
属性是一个对象,允许你覆盖主题的特定组件属性。在这种情况下,你想覆盖的是MuiButton
组件的样式。在MuiButton
中,你有用于针对组件特定方面的相同 CSS API。这使得将styles
移动到theme
中变得简单,因为没有什么需要改变。
在这个示例中必须改变的一点是间距的工作方式。在通过withStyles()
应用的正常styles
中,你可以访问当前的主题,因为它作为参数传递。你仍然需要访问间距数据,但由于你不在函数中,所以没有theme
参数。由于你只是扩展了默认的theme
,你可以通过调用createMuiTheme()
而不传递任何参数来访问它,就像这个示例所展示的那样。
参考也
- Material-UI 样式覆盖文档:
material-ui.com/customization/overrides/
。
其他样式选项
除了withStyles()
之外,你的 Material-UI 应用程序还有其他样式选项。有styled()
高阶组件函数,它模仿了 styled 组件。你也可以跳出 Material-UI 样式系统,使用内联 CSS 样式或导入 CSS 模块并应用这些样式。
如何实现...
这里是一个修改过的Scoped component styles
示例,展示了你在 Material-UI 应用程序中可用的几种替代样式机制:
import React, { Fragment } from 'react';
import { styled } from '@material-ui/styles';
import Button from '@material-ui/core/Button';
import styles from './OtherStylingOptions.module.css';
const MyStyledButton = styled(Button)({
margin: 16,
paddingTop: 16,
paddingBottom: 16
});
const OtherStylingOptions = () => (
<Fragment>
<Button style={{ margin: 16 }}>My Default Button</Button>
<MyStyledButton variant="contained">
My Contained Button
</MyStyledButton>
<Button
className={styles.primaryContained}
variant="contained"
color="primary"
>
My Contained Primary Button
</Button>
</Fragment>
);
export default OtherStylingOptions;
它是如何工作的...
第一个按钮使用内联 CSS 属性,以纯 JavaScript 对象的形式表达,并传递给Button
组件的style
属性。第二个Button
使用styled()
函数构建一个MyStyledButton
组件。这个函数与withStyles
非常相似,主要区别在于它的签名是针对习惯于 styled-component 样式组件方法的人。
第三个button
使用从导入的 CSS 模块中导入的样式。模块看起来是这样的:
button.primaryContained {
margin: 16px;
padding: 16px 32px;
}
在使用 CSS 模块和内联样式时要小心。这些方法工作得很好,但由于它们并没有紧密集成到 Material-UI 的样式和主题机制中,因此需要更多的工作来确保你的样式与 Material-UI 组件的其他样式相匹配。
参考也
-
Material-UI 样式覆盖文档:
material-ui.com/customization/overrides/
. -
Material-UI JSS 文档:
material-ui.com/css-in-js/api/
.