iT邦幫忙

2021 iThome 鐵人賽

DAY 18
0

元件介紹

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


上一篇
【Day17】數據展示元件 - Infinite scroll
下一篇
【Day19】導航元件 - Dropdown
系列文
30 天擁有一套自己手刻的 React UI 元件庫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言