模仿Mantine Tabs
阅读本文需要的前提知识。
如果你具备React的基本知识和TypeScript的基本知识,将能够更好地理解并从中受益。
此外,熟悉React组件结构和props概念将使你更轻松地理解这篇文章。
曼泰牌标签
-
- mantine Tabs
- src
Mantine tabs 有什么特别之处?
大多数的UI组件仅由一个类构成。
一方面,mantine tabs 设想了由多个类组成的结构。Tabs 在内部还具有提供者,并且数据在 Tabs 组内进行完整构造。
Tabs という塊の親
Tabs の上部に位置する navigation
List の各アイテム
各 Tab に紐づくコンテンツ
<Tabs defaultValue="gallery">
<Tabs.List>
<Tabs.Tab value="gallery" leftSection={<IconPhoto style={iconStyle} />}>
Gallery
</Tabs.Tab>
<Tabs.Tab value="messages" leftSection={<IconMessageCircle style={iconStyle} />}>
Messages
</Tabs.Tab>
<Tabs.Tab value="settings" leftSection={<IconSettings style={iconStyle} />}>
Settings
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="gallery">
Gallery tab content
</Tabs.Panel>
<Tabs.Panel value="messages">
Messages tab content
</Tabs.Panel>
<Tabs.Panel value="settings">
Settings tab content
</Tabs.Panel>
</Tabs>
引用
https://mantine.dev/core/tabs/
尝试创建一个类似 Tabs.* 的组件。
思维方式
- 如果我们使用 React.FC 来定义 Tabs,那么如果我们想要使 Tabs.Tab 成为可能,我们可以先继承 React.FC,不是吗?
实施
import React, {ReactElement, ReactNode, forwardRef} from 'react';
interface TabsProps {
children: ReactNode | ReactNode[];
}
// POINT: Tabs extends React.FC
interface TabsComponent extends React.FC<TabsProps> {
Tab: typeof Tab;
}
interface TabProps {
children: ReactNode | ReactNode[];
}
/** -------- Components -------- */
const Tab: React.FC<TabProps> = props => {
return (
<div style={{padding: 8, margin: 8, border: '1px solid black'}}>
{props.children}
</div>
);
};
const Tabs: TabsComponent = props => {
return <div style={{display: 'flex'}}>{props.children}</div>;
};
Tabs.Tab = Tab;
export default Tabs;
<Tabs>
<Tabs.Tab>1</Tabs.Tab>
<Tabs.Tab>2</Tabs.Tab>
<Tabs.Tab>3</Tabs.Tab>
</Tabs>
这样的事情竟然发生了… 笑
尝试创建TabsProvider
首先,关于是否需要提供者的问题,
Tabs の children が必ず Tabs.Tab になる
如果会始终遵守这个规则,我认为是不需要的。
原因是,从Tabs传递到Tabs.Tab将需要特定的props。
例如,假设Tabs.Tab需要isActive: boolean。
<div>
{React.Children.map(props.children, (child, idx) => {
return React.cloneElement(child, { isActive: /** TODO */});
})}
</div>
然而,事实上
<Tabs>
<p>I shoudn't be here!</p>
</Tabs>
这种实现方式太强大了。这个组件没有isActive这个props。结果会导致typescript错误。
所以 (suo yi)
- children の props バケツリレーで値を渡す
说到这一点
- children の に provider で値を渡す
避免它。
实施
-import React, {ReactElement, ReactNode, forwardRef} from 'react';
+import React, {
+ ReactNode,
+ createContext,
+ useContext,
+ useState,
+} from 'react';
interface TabsProps {
children: ReactNode | ReactNode[];
+ value: string;
}
// POINT: Tabs extends React.FC
interface TabProps {
children: ReactNode | ReactNode[];
+ value: string;
}
+/** Provider */
+
+const TapsProvider = createContext({
+ value: '',
+ onClickTab: (value: string) => {},
+});
+
/** -------- Components -------- */
const Tab: React.FC<TabProps> = props => {
+ const {value, onClickTab} = useContext(TapsProvider);
+ const isActive = value === props.value;
+
+ const handleClick = () => {
+ onClickTab(props.value);
+ };
return (
- <div style={{padding: 8, margin: 8, border: '1px solid black'}}>
+ <div
+ onClick={handleClick}
+ style={{
+ padding: 8,
+ margin: 8,
+ border: '1px solid black',
+ backgroundColor: isActive ? 'salmon' : 'initial',
+ }}
+ >
{props.children}
</div>
);
};
const Tabs: TabsComponent = props => {
- return <div style={{display: 'flex'}}>{props.children}</div>;
+ const [value, setValue] = useState(props.value);
+ return (
+ <TapsProvider.Provider value={{value, onClick: setValue}}>
+ <div style={{display: 'flex'}}>{props.children}</div>
+ </TapsProvider.Provider>
+ );
};
Tabs.Tab = Tab;
<Tabs value="1">
<Tabs.Tab value="1">1</Tabs.Tab>
<Tabs.Tab value="2">2</Tabs.Tab>
<Tabs.Tab value="3">3</Tabs.Tab>
</Tabs>
追加各种要素后变成最终这样
Tabs.List 追加
Tabs.Panel 追加
import React, {ReactNode, createContext, useContext, useState} from 'react';
interface TabsProps {
children: ReactNode | ReactNode[];
defaultValue: string;
}
// POINT: Tabs extends React.FC
interface TabsComponent extends React.FC<TabsProps> {
Tab: typeof Tab;
List: typeof List;
Panel: typeof Panel;
}
interface ListProps {
children: ReactNode | ReactNode[];
orientation?: 'horizontal' | 'vertical';
}
interface TabProps {
children: ReactNode | ReactNode[];
value: string;
}
interface PanelProps {
children: ReactNode | ReactNode[];
value: string;
}
/** Provider */
const TapsProvider = createContext({
value: '',
onClick: (value: string) => {},
});
/** -------- Components -------- */
const List: React.FC<ListProps> = ({orientation = 'horizontal', ...props}) => {
return (
<div
style={{
display: 'flex',
flexDirection: orientation == 'horizontal' ? 'row' : 'column',
}}
>
{props.children}
</div>
);
};
const Tab: React.FC<TabProps> = props => {
const {value, onClick} = useContext(TapsProvider);
const isActive = value === props.value;
const handleClick = () => {
onClick(props.value);
};
return (
<div
onClick={handleClick}
style={{
padding: 8,
margin: 8,
border: '1px solid black',
backgroundColor: isActive ? 'salmon' : 'initial',
}}
>
{props.children}
</div>
);
};
const Panel: React.FC<PanelProps> = props => {
const {value} = useContext(TapsProvider);
const isActive = value === props.value;
return (
<div style={{display: isActive ? 'initial' : 'none'}}>{props.children}</div>
);
};
const Tabs: TabsComponent = props => {
const [value, setValue] = useState(props.defaultValue);
return (
<TapsProvider.Provider value={{value, onClick: setValue}}>
{props.children}
</TapsProvider.Provider>
);
};
Tabs.Tab = Tab;
Tabs.List = List;
Tabs.Panel = Panel;
export default Tabs;
<Tabs defaultValue="1">
<Tabs.List orientation="horizontal">
<Tabs.Tab value="1">1</Tabs.Tab>
<Tabs.Tab value="2">2</Tabs.Tab>
<Tabs.Tab value="3">3</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="1">contents of 1</Tabs.Panel>
<Tabs.Panel value="2">contents of 2</Tabs.Panel>
<Tabs.Panel value="3">contents of 3</Tabs.Panel>
</Tabs>
最后
这次我们直截了当地继承了 React.FC,但 mantine 将此过程进行了泛化,并定义了所谓的工厂。
我对这个地方的完全理解还为时尚早。
请将以下内容在中文中进行同义转述,仅提供一种选项:
https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/core/factory/factory.ts
替代版本:请访问此链接 https://github.com/mantinedev/mantine/blob/master/src/mantine-core/src/core/factory/factory.ts 来获取所需的内容。
这次的重点有两个,对吧。
-
- 我认为这不是最理想的方式,但通过扩展React.FC来向函数组件添加函数组件。
通过使用Provider来封装Tabs组件。