React-材质-UI-秘籍-三-

65 阅读14分钟

React 材质 UI 秘籍(三)

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

译者:飞龙

协议:CC BY-NC-SA 4.0

第七章:表格 - 显示复杂集合数据

在本章中,你将学习以下主题:

  • 状态化表格

  • 可排序的列

  • 过滤行

  • 选择行

  • 行操作

简介

如果你的应用程序需要显示表格数据,你可以使用 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状态被映射到TableRowTableCell组件。当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样式。这为进度指示器添加了margintextAlign属性已被添加到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>

当您的用户等待表格数据加载时,他们会看到以下内容:

图片

参见

可排序列

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接受activedirectiononClick属性。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参数——列索引——并返回一个新的列函数。返回的函数有两个目的:

  1. 为了更新列状态,以便正确标记为激活的列,并具有正确的排序方向

  2. 为了更新行状态,使表格行按正确顺序排列

一旦这些状态变化已经完成,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 }
]);

平均值列默认为激活状态。由于默认为升序,因此不需要指定顺序。以下是屏幕首次加载时的表格外观:

图片

相关内容

过滤行

在有表格的地方,信息量过多是潜在的问题。这就是为什么在表格中添加搜索功能是个好主意。它允许用户在输入时从表格中删除不相关的行。

如何实现...

假设你有一个包含许多行的表格,这意味着用户将很难滚动查看整个表格。为了让他们更容易操作,你决定在你的表格中添加一个搜索功能,通过检查搜索文本是否存在于名称列中来过滤行。以下是代码:

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 属性是否包含搜索字符串。但首先,你必须确保用户实际上正在进行过滤。例如,如果搜索字符串为空,则应返回每个项目。项目是如何被搜索的取决于你的应用程序——如果你想的话,你可以搜索每个项目的每个属性。

参见

选择行

用户经常需要与表格中的特定行进行交互。例如,他们可能会选择一行,然后执行使用所选行数据的操作。或者,用户选择多行,这会产生与他们的选择相关的新数据。使用 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()函数以相同的方式工作——它们只是分别操作lowhigh字段。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>
);

StartButtonStopButton组件非常相似。这两个组件在表格的每一行中都被渲染。有一个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);
};

StartButtonStopButton 组件都使用相同的处理函数——它在 status 值之间切换 runningoff。最后,让我们看看 TableCell 组件,这些 row 动作在这里被渲染:

<TableCell>
  <StartButton
    row={row}
    onClick={toggleStatus(row.id)}
  />
  <StopButton
    row={row}
    onClick={toggleStatus(row.id)}
  />
</TableCell>

行数据作为 row 属性传递。toggleStatus() 函数接受一个 row id 参数,并返回一个作用于该行的新处理函数。

参见

第八章:卡片 - 显示详细信息

在本章中,您将学习以下关于卡片的内容:

  • 主要内容

  • 卡片标题

  • 执行操作

  • 展示媒体

  • 可展开卡片

简介

卡片是用于在给定主题上显示特定信息的 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 组件处理三个头部组件的对齐——头像、标题和副标题。

相关内容

执行操作

卡片用于显示关于主题的特定操作。通常,用户会对主题执行操作,例如向联系人发送消息或删除联系人。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 组件显示的主题图片。你可以使用 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;

这是最终卡片的样式:

相关内容

可展开卡片

有时,你可能无法将所有想要的内容都放入卡片中。为了适应,你可以使你的卡片可展开,这意味着用户可以点击 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 组件,以确保一旦显示额外内容,其样式与卡片内容的其余部分保持一致。

参见

第九章: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 是在响应某些事件时显示的。例如,如果你的应用程序中创建了一个新的资源,那么使用 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 的值传递给了 Snackbaropen 属性。当用户点击“显示 Snackbar”按钮时,showSnackbar() 函数将 open 状态设置为 true。结果,true 值被传递给了 Snackbaropen 属性。

还有更多...

一旦显示了一个 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()。这个函数被传递给了 SnackbaronClose 属性。autoHideDuration 组件是你希望 Snackbar 保持可见的毫秒数。在这个例子中,五秒后,Snackbar 组件将调用传递给其 onClose 属性的函数。这会将 open 状态设置为 false,然后这个值被传递给了 Snackbaropen 属性。

参见

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);

firstsecondthirdfourth 状态对应于它们自己的 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 抽象使你指定过渡和过渡参数变得稍微简单一些。

参见

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 组件。

相关内容

错误边界和错误 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>;
};

现在你可以渲染一个带有指定标签的按钮。如果throwErrortrue,则由于错误,没有任何内容渲染。接下来,让我们更改示例的标记,以包含多个按钮和多个error边界:

export default () => (
  <Fragment>
    <ErrorBoundary>
      <MyButton label="First Button" />
    </ErrorBoundary>
    <ErrorBoundary>
      <MyButton label="Second Button" throwError />
    </ErrorBoundary>
  </Fragment>
);

第一个按钮渲染时没有任何问题。然而,如果错误边界像之前那样是全包容性的,那么这个按钮就不会显示。第二个按钮抛出错误,因为throwError属性为真。因为这个按钮有自己的错误边界,所以它不会阻止其他工作正常的 UI 部分渲染。现在当你运行示例时,你会看到以下内容:

参见

带有操作的 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:

图片

现在您已经为用户提供了一个轻松导航到资源的途径,而不会打断他们当前正在做的事情。

参见

队列 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字符串放入队列。如果消息是队列中唯一的项,那么openmessage状态会立即更新。

接下来,让我们看看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 将被打开。此时,项目也将从队列中移除。

参见