【Day18】导航元件 - Breadcrumb

元件介绍

Breadcrumb 是一个导航元件,用於显示当前系统层级结构中的路径位置,并且点击路径能返回之前的页面。在系统有多个层级架构,并且希望能帮助用户清楚知道自己目前层级位置,及希望用户能方便返回上面层级时,能够使用面包屑元件。

「面包屑」这个命名应该是取自格林童话里面的知名童话故事「糖果屋」,讲述汉赛尔与葛丽特兄妹被丢弃在森林中时,希望透过沿路在地上布置面包屑能够帮助他们沿着这些线索找到回家的路。


Detail of 1881 lithograph of Hansel and Gretel by Heinrich Merte from “The Fairy Tales of Brothers Grimm,” edited by Noel Daniel.

参考设计 & 属性分析

从 MUI 以及 Antd 提供的 Breadcrumb 元件来看,他们都是会提供 Breadcrumbs 的 wrapper 元件以及 item 元件,透过这两个元件来组成我们所需要的 Breadcrumb。

Custom separator

从 MUI 以及 Antd 在设计上其实还蛮类似的,在 wrapper 元件上提供一个 separator props 来帮助我们客制化 separator,在 wrapper 上提供的好处是,我们不用每增加一个 item 就自己再手动写一个 separator element,例如下面这样:

<Breadcrumb>
  <Item>Material-UI</Item>
  <Seperator />
  <Item>Core</Item>
  <Seperator />
  <Item>Breadcrumb</Item>
</Breadcrumb>

而是透过 wrapper 直接在里面对 children element 做迭代处理,简化成这样:

<Breadcrumb separator=">">
  <Item>Material-UI</Item>
  <Item>Core</Item>
  <Item>Breadcrumb</Item>
</Breadcrumb>

这样写起来比较简洁,减少许多重复的程序码,而且透过程序自动迭代,免去手动增加会造成的错误,例如自己手残多加一个或是少加一个。

Breadcrumbs with icons

观察一下 MUI 给的范例 Breadcrumbs ,觉得很有趣的是,Breadcrumb 的 item 都是用既有的 MUI 元件来组成的,例如 <Link />, <Chip /> 都可以当作他的 Breadcrumb item,因此如果需要 item 带上 icon,透过这些元件也能够实现,例如:

<Breadcrumbs aria-label="breadcrumb">
  <Link ...props>
    <HomeIcon className={classes.icon} />
    Material-UI
  </Link>
</Breadcrumbs>

或是

<Breadcrumbs aria-label="breadcrumb">
  <Chip
    label="Home"
    icon={<HomeIcon fontSize="small" />}
  />
</Breadcrumbs>

另外,我们来解析一下 Antd 的带 icon Breadcrumb,我们可以发现 <Breadcrumb.Item /> 本质上就是一个 a tag <a href="...">,原生的 a tag 原本就支援我们在其 children 放东西,因此参考他的结构,也是直接放入 svg icon 以及 label text。

maxItems

有时候路径很深很多层的时候, Breadcrumbs 会变得很长,或者 Breadcrumbs item 的文字有时候不小心会很长,所以在处理窄萤幕的时候容易会超出宽度,因此 maxItems 这个 props 可以帮助我们缩短 Breadcrumbs 。

对於 Breadcrumbs 的想法

其他属性我觉得还蛮多元的,例如 Antd Breadcrumbs 也支援在里面放下拉选单,这个我觉得很酷,但我觉得这个属性也是依照需要加入即可,并不是每个网站都很常需要这个属性。

再来就是,我们试想看看,假设我们这个网站很多地方都会需要用到 Breadcrumbs,且在同一个网站上的 Breadcrumbs 会有一致风格的前提之下,若每个地方都用 wrapper 包住 items 这个方式来撰写 Breadcrumbs 的话,我觉得不是一个很好的方法,一方面是这样写真的太冗长,二方面是会需要写很多重复的结构以及样式,所以理想上,在同一个网站中,我会希望 wrapper 包住 items 的结构做一次就好,例如在 project 的 components 资料夹下面,我们就放一个自己为这个网站制作的 CustomBreadcrumbs,然後以後需要 Breadcrumbs 的地方,我们直接传一个物件的结构进来,例如:

const routes = [
  {
    path: '/general',
    label: 'General',
    icon: <General />
  },
  {
    path: '/layout',
    label: 'Layout',
    icon: <Layout />
  },
  {
    path: '/navigation',
    label: 'Navigation',
    icon: <Navigation />
  },
];

<CustomBreadcrumbs
  routes={routes}
/>

所以假设未来某天我们希望更改这个网站所有 Breadcrumbs 的样式,我们就只需要改一个地方就好,因为我们已经统一管理了样式以及结构,其他地方只有传资料进来而已。

介面设计

属性 说明 类型 默认值
to 跳转的路径 string
label 项目名称 string
icon 项目图示 ReactNode
separator 分隔符号 ReactNode, string

元件实作

首先从资料面来看,架设我们希望传进去的 route 设为下面这样:

const routes = [
  {
    to: '/home',
    label: '首页',
  },
  {
    to: '/school',
    label: '学校列表',
  },
  {
    to: '/members',
    label: '会员列表',
  },
  {
    to: '/memberDetail',
    label: '会员资料',
  },
];

我们会希望我们最终可以像这样使用我们的 route 元件:

<Breadcrumb
  routes={routes}
/>

然後就可以产生这样的效果:

因此首先,我们要准备一个 <Breadcrumbs /> 元件,在其中可以迭代我们上面的 routes 结构:

const Breadcrumb = ({ routes }) => (
  <Breadcrumbs>
    {
      routes.map((route) => (
        <BreadcrumbItem
          key={route.label}
          label={route.label}
          to={route.to}
        />
      ))
    }
  </Breadcrumbs>
);

但是从上述程序码当中我们可以发现,我们怎麽只有迭代 routes 的内容出来?那中间的 separator 在哪里呢?相信有读过前面文章的读者应该会想到,我们就是用那一千零一招中的那一招,在 <Breadcrumbs /> 里面使用 React.Children.map 来加工处理,因此我们的 Breadcrumbs 元件会是下面这样,判断他是否为最後一个节点,然後在中间插入 separator:

<StyledBreadcrumbs>
  {
    React.Children.map(children, (child, index) => {
      const isLast = index === React.Children.count(children) - 1;
      return (
        <>
          {child}
          {isLast ? null : <Separator>{separator}</Separator>}
        </>
      );
    })
  }
</StyledBreadcrumbs>

为什麽要这麽麻烦呢?因为我们希望未来可以单独使用 <Breadcrumbs /> 这个元件,所以 route 在迭代的时候,不想把 separator 绑死在上面,这样的话我们就不用限定 Breadcrumbs 每个 node 中的样式,我们甚至可以替换他,变成这样:

const WithCustomNode = (args) => {
  const { routes: withIconRoutes } = args;
  return (
    <Breadcrumbs>
      {
        withIconRoutes.map((route) => (
          <Chip
            key={route.label}
            label={route.label}
            icon={route.icon}
          />
        ))
      }
    </Breadcrumbs>
  );
};

以上述程序码来说,我们甚至可以把 <Breadcrumbs /> 包住我们之前写的 <Chip /> 都没问题。

Custom separator

因为 separator 是在 <Breadcrumbs /> 里面处理的,所以在我们把 separator 参数化之後,当然可以随心所欲的替换 separator,我们的 separtor 可以像这样由外面传入来决定:

<Breadcrumb
  routes={routes}
  separator="/"
/>

Max items

由於我们的 Breadcrumbs 是横向生长的,所以在窄萤幕的时候很容易遇到问题,或是阶层太深的时候也会容易太长,所以我们可以透过 maxItems 这个参数来帮我们决定到底多少节点之後需要折叠起来

<Breadcrumb
  maxItems={2}
  separator="/"
/>

<Breadcrumbs /> 里面,因为我们是拿到上一层已经迭代完的结果,props 是一个 children,所以我们要透过 React.Children.count(children) 这个方法来帮助我们算出到底有几个节点。

当节点数目大於 maxItems 的时候,我们就需要把 Breadcrumbs 折叠起来,我们折叠的方式就是留下头尾,其他都砍了:

const [isCollapse, setIsCollapse] = useState(
  maxItems < React.Children.count(children),
);

if (isCollapse) {
  return (
    <StyledBreadcrumbs>
      {children[0]}
      <Separator>{separator}</Separator>
      <CollapsedContent
        role="presentation"
        onClick={() => setIsCollapse(false)}
      >
        ...
      </CollapsedContent>
      <Separator>{separator}</Separator>
        {children[React.Children.count(children) - 1]}
    </StyledBreadcrumbs>
  );
}

我们也可以设计成,点击中间被折叠起来的部分的时候,就展开成原来的样子:


Breadcrumb 元件原始码:
Source code

Breadcrumbs 元件原始码:
Source code

Storybook:
Breadcrumb


<<:  day16: function programming 是什麽?

>>:  day31 虽然没有写完,但是还是要有summary

Day18 如果你愿意一层一层一层的剥开我的心

Pivot 今天继续来研究PivotTable.js(Gittub)是怎麽写的,我们来研究它所提供...

Day5|【Git】动手建立、初始储存库(Repository)!

这里我们先看一张图,大概了解一下 Git 在发布专案时的流程。 先有个概念,之後会逐一详细解释。 开...

Day28 - Linux 编译 POC/exploit

复习:渗透测试的目的 在合法委托下,确认目标网站或系统有可利用的漏洞,若确认有目标在取得授权下,提升...

IBM Cloud CLI

注册完成後,今天来安装并验证 IBM Cloud CLI 1. 下载并安装 IBM Cloud CL...

Day-21 : devise 安装 part 2

续昨天的part1, 继续纪录专案过程学会的一些小玩具 config/environments/de...