Material-UI 的选项卡与反应路由器 4 集成?

Posted

技术标签:

【中文标题】Material-UI 的选项卡与反应路由器 4 集成?【英文标题】:Material-UI's Tabs integration with react router 4? 【发布时间】:2017-05-29 01:35:17 【问题描述】:

新的 react-router 语法使用 Link 组件来移动路由。但是这怎么能和material-ui结合呢?

在我的例子中,我使用标签作为主要导航系统,所以理论上我应该有这样的东西:

const TabLink = ( onClick, href, isActive, label ) => 
  <Tab
    label=label
    onActive=onClick
  />



export default class NavBar extends React.Component 
  render () 
    return (
      <Tabs>
        <Link to="/">params => <TabLink label="Home" ...params/></Link>
        <Link to="/shop">params => <TabLink label="shop" ...params/></Link>
        <Link to="/gallery">params => <TabLink label="gallery" ...params/></Link>
      </Tabs>
    )
  

但是在渲染的时候,material-ui 会抛出一个错误,Tabs 的子元素必须是Tab 组件。有什么方法可以继续?如何管理标签的 isActive 属性?

提前致谢

【问题讨论】:

在这里完整回答 - ***.com/a/47396910/4180797 【参考方案1】:

另一种解决方案 (https://codesandbox.io/s/l4yo482pll) 没有处理程序或 HOC,只有纯 react-router 和 material-ui 组件:

import React,  Fragment  from "react";
import ReactDOM from "react-dom";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import  Switch, Route, Link, BrowserRouter, Redirect  from "react-router-dom";

function App() 
  const allTabs = ['/', '/tab2', '/tab3'];

  return (
    <BrowserRouter>
      <div className="App">
        <Route
          path="/"
          render=( location ) => (
            <Fragment>
              <Tabs value=location.pathname>
                <Tab label="Item One" value="/" component=Link to=allTabs[0] />
                <Tab label="Item Two" value="/tab2" component=Link to=allTabs[1] />
                <Tab
                  value="/tab3"
                  label="Item Three"
                  component=Link
                  to=allTabs[2]
                />
              </Tabs>
              <Switch>
                <Route path=allTabs[1] render=() => <div>Tab 2</div> />
                <Route path=allTabs[2] render=() => <div>Tab 3</div> />
                <Route path=allTabs[0] render=() => <div>Tab 1</div> />
              </Switch>
            </Fragment>
          )
        />
      </div>
    </BrowserRouter>
  );


const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

【讨论】:

很好的解决方案! 这是最好的答案! 这是最好的解决方案! 简洁明了。我挣扎了一段时间,直到我找到了这个解决方案。我必须承认,我发现 Material UI 页面上的示例很难理解。我最初使用的是 Bootstrap,我很快就实现了这一目标。 代码沙箱已死js'【参考方案2】:

我的导师帮助我使用 React Router 4.0 的 withRouter 来包装 Tabs 组件以启用这样的历史记录方法:

import React, Component from "react";
import Tabs, Tab from 'material-ui';
import  withRouter  from "react-router-dom";

import Home from "./Home";
import Portfolio from "./Portfolio";

class NavTabs extends Component 

 handleCallToRouter = (value) => 
   this.props.history.push(value);
 

  render () 
     return (
      <Tabs
        value=this.props.history.location.pathname
        onChange=this.handleCallToRouter
        >
        <Tab
          label="Home"
          value="/"
        >
        <div>
           <Home />
        </div>
        </Tab>
        <Tab
          label="Portfolio"
          value="/portfolio"
            >
          <div>
            <Portfolio />
          </div>
        </Tab>
      </Tabs>           
    )
  


export default withRouter(NavTabs)  

只需将 BrowserRouter 添加到 index.js 即可。

【讨论】:

这很好,但由于value=this.props.history.location.pathname,如果您将路由添加到特定元素或选项卡中未定义的任何路由,则会导致错误:Warning: Material-UI: the value provided `/portfolio/123` is invalid 我无法突出显示我的标签【参考方案3】:

您从 material-ui 看到的错误是因为它希望将 &lt;Tab&gt; 组件呈现为 &lt;Tabs&gt; 组件的直接子级。

现在,我发现了一种将链接集成到 &lt;Tabs&gt; 组件中而不会丢失样式的方法:

import React, Component from 'react';
import Tabs, Tab from 'material-ui/Tabs';
import Link from 'react-router-dom';

export default class MyComponent extends Component 
    render() 
        const location = this.props;
        const pathname = location;

        return (
            <Tabs value=pathname>
                <Tab label="First tab" containerElement=<Link to="/my-firs-tab-view" /> value="/my-firs-tab-view">
                    /* insert your component to be rendered inside the tab here */
                </Tab>
                <Tab label="Second tab" containerElement=<Link to="/my-second-tab-view" /> value="/my-second-tab-view">
                    /* insert your component to be rendered inside the tab here */
                </Tab>
            </Tabs>
        );
    

要管理选项卡的“活动”属性,您可以使用&lt;Tabs&gt; 组件中的value 属性,并且您还需要为每个选项卡设置value 属性,因此当两个属性匹配时,它会将活动样式应用于该选项卡。

【讨论】:

文档中的containerElement 在哪里?它是如何使用的?【参考方案4】:

这是另一个解决方案,使用 Material 1.0 的测试版并添加浏览器 Back/Forward 组合:

import React from 'react';
import PropTypes from 'prop-types';
import  withStyles  from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import Tabs,  Tab  from 'material-ui/Tabs';
import  withRouter  from "react-router-dom";
import Home from "./Home";
import Portfolio from "./Portfolio";

function TabContainer(props) 
  return <div style= padding: 20 >props.children</div>;


const styles = theme => (
  root: 
    flexGrow: 1,
    width: '100%',
    marginTop: theme.spacing.unit * 3,
    backgroundColor: theme.palette.background.paper,
  ,
);

class NavTabs extends React.Component 
  state = 
    value: "/",
  ;

  componentDidMount() 
    window.onpopstate = ()=> 
      this.setState(
        value: this.props.history.location.pathname
      );
  


  handleChange = (event, value) => 
    this.setState( value );
    this.props.history.push(value);
  ;

  render() 
    const  classes  = this.props;
    const  value  = this.state;

    return (
      <div className=classes.root>
        <AppBar position="static" color="default">
          <Tabs
            value=value
            onChange=this.handleChange
            scrollable
            scrollButtons="on"
            indicatorColor="primary"
            textColor="primary"
          >
            <Tab label="Home" value = "/" />
            <Tab label="Portfolio" value = "/portfolio"/>
          </Tabs>
        </AppBar>
        value === "/" && <TabContainer><Home /></TabContainer>
        value === "/portfolio" && <TabContainer><Portfolio /></TabContainer>
      </div>
    );
  


NavTabs.propTypes = 
  classes: PropTypes.object.isRequired,
;

export default withRouter(withStyles(styles)(NavTabs));

【讨论】:

【参考方案5】:

你可以使用 browserHistory 代替 React-Router Link 组件

import  browserHistory  from 'react-router'

// Go to /some/path.
onClick(label) 
  browserHistory.push('/$label');


// Example for Go back
//browserHistory.goBack()

<Tabs>
  <Tab
    label=label
    onActive=() => onClick(label)
  />
</Tabs>

如您所见,您可以简单地将push() 您的目标发送到browserHistory

【讨论】:

v4 中没有browserHistory,看看他们的index.js。 传递的道具props.push(url, state)有类似的东西【参考方案6】:

正如@gkatchmar 所说,您可以使用withRouter 高阶组件,但您也可以使用context API。由于@gkatchmar 已经显示了withRouter,我将只显示context API。请记住,这是一个实验性 API。

https://***.com/a/42716055/3850405

import React, Component from "react";
import Tabs, Tab from 'material-ui';
import * as PropTypes from "prop-types";

export class NavTabs extends Component 
constructor(props) 
 super(props);


static contextTypes = 
    router: PropTypes.object


handleChange = (event: any , value: any) => 
    this.context.router.history.push(value);
;

  render () 
     return (
      <Tabs
        value=this.context.router.history.location.pathname
        onChange=this.handleChange
        >
        <Tab
          label="Home"
          value="/"
        >
        <div>
           <Home />
        </div>
        </Tab>
        <Tab
          label="Portfolio"
          value="/portfolio"
            >
          <div>
            <Portfolio />
          </div>
        </Tab>
      </Tabs>           
    )
  

【讨论】:

【参考方案7】:

带有 Tab 高亮显示的解决方案,基于 Typescript 并且与 react-route v5 配合使用: 解释:&lt;Tab/&gt; 这里作为 React 路由器的链接。 &lt;Tab/&gt; to='/all-event'value='/all-event' 中的值应该相同才能突出显示

import  Container, makeStyles, Tab, Tabs  from '@material-ui/core';
import React from 'react';
import 
  Link,
  Route,
  Switch,
  useLocation,
  Redirect,
 from 'react-router-dom';
import AllEvents from './components/AllEvents';
import UserEventsDataTable from './components/UserEventsDataTable';

const useStyles = makeStyles(() => (
  container: 
    display: 'flex',
    justifyContent: 'center',
  ,
));

function App() 
  const classes = useStyles();
  const location = useLocation();

  return (
    <>
      <Container className=classes.container>
        <Tabs value=location.pathname>
          <Tab
            label='All Event'
            component=Link
            to=`/all-event`
            value=`/all-event`
          />
          <Tab
            label='User Event'
            component=Link
            to=`/user-event`
            value=`/user-event`
          />
        </Tabs>

      </Container>
      <Switch>
        <Route path=`/all-event`>
          <AllEvents />
        </Route>
        <Route path=`/user-event`>
          <UserEventsDataTable />
        </Route>
        <Route path=`/`>
          <Redirect from='/' to='/all-event' />
        </Route>
      </Switch>
    </>
  );


export default App;

【讨论】:

【参考方案8】:
 <BrowserRouter>
<div className=classes.root>
  <AppBar position="static" color="default">
    <Tabs
      value=this.state.value
      onChange=this.handleChange
      indicatorColor="primary"
      textColor="primary"
      fullWidth
    >
      <Tab label="Item One" component=Link to="/one" />
      <Tab label="Item Two" component=Link to="/two" />
    </Tabs>
  </AppBar>

  <Switch>
    <Route path="/one" component=PageShell(ItemOne) />
    <Route path="/two" component=PageShell(ItemTwo) />
  </Switch>
</div>

【讨论】:

您能否添加几句话来解释您的代码以及它如何回答 OP 的问题?【参考方案9】:

这是一个使用useLocation 挂钩的简单解决方案。不需要状态。不过 React 路由器 v5。

import  Tab, Tabs  from '@material-ui/core';
import  matchPath, NavLink, useLocation  from 'react-router-dom';

const navItems = [
  
    id: 'one',
    path: '/one',
    text: 'One',
  ,
  
    id: 'two',
    path: '/two',
    text: 'Two',
  ,
  
    id: 'three',
    path: '/three',
    text: 'Three',
  ,
];

export default function Navigation() 
  const  pathname  = useLocation();
  const activeItem = navItems.find((item) => !!matchPath(pathname,  path: item.path ));
  return (
    <Tabs value=activeItem?.id>
      navItems.map((item) => (
        <Tab key=item.id value=item.id label=item.text component=NavLink to=item.path />
      ))
    </Tabs>
  );

【讨论】:

【参考方案10】:

我创建了这个钩子来帮助控制选项卡并生成从位置 URL 捕获的默认值。

const useTabValue = (array, mainPath = "/") => 
  const history = useHistory();
  const  pathname  = useLocation();
  const [value, setValue] = useState(0);
  const pathArray = pathname.split("/");

  function handleChange(_, nextEvent) 
    setValue(nextEvent);
    history.push(`$mainPath/$array[nextEvent]`);
  

  const findDefaultValue = useCallback(() => 
    return array.forEach((el) => 
      if (pathArray.indexOf(el) > 0) 
        setValue(array.indexOf(el));
        return;
      
    );
  , [pathArray, array]);

  useEffect(() => 
    findDefaultValue();
  , [findDefaultValue]);
  return 
    handleChange,
    value,
  ;
;

然后我就这样使用它:

const NavigationBar = () => 
  const classes = useStyles();
  const allTabs = useMemo(() => ["home", "search"]);
  const  handleChange, value  = useTabValue(allTabs, "/dashboard");
  return (
    <div className=classes.navBarContainer>
      <Tabs
        centered
        value=value
        variant="fullWidth"
        onChange=handleChange
        className=classes.navBar
      >
        <Tab color="textPrimary" icon=<HomeIcon /> />
        <Tab color="textPrimary" icon=<ExploreIcon /> />
      </Tabs>
    </div>
  );
;

【讨论】:

【参考方案11】:

我让它在我的应用程序中以这种方式工作:

import React, useEffect, useRef from 'react';
import PropTypes from 'prop-types';
import makeStyles 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';
import Box from '@material-ui/core/Box';
import Container from "@material-ui/core/Container";
import Link from "react-router-dom";
import MenuIcon from "@material-ui/icons/Menu";
import VideoCallIcon from "@material-ui/icons/VideoCall";

const docStyles = makeStyles(theme => (
    root: 
        display: 'flex',
        '& > * + *': 
            marginLeft: theme.spacing(2),
        ,
    ,
    appBarRoot: 
        flexGrow: 1,
    ,
    headline: 
        marginTop: theme.spacing(2),
    ,
    bodyCopy: 
        marginTop: theme.spacing(1),
        fontSize: '1.2rem',
    ,
    tabContents: 
        margin: theme.spacing(3),
    ,
));

function TabPanel(props) 
    const children, value, index, classes, ...other = props;

    return (
        <div
            role="tabpanel"
            hidden=value !== index
            id=`simple-tabpanel-$index`
            aria-labelledby=`simple-tab-$index`
            ...other
        >
            value === index && (
                <Container>
                    <Box className=classes.tabContents>
                        children
                    </Box>
                </Container>
            )
        </div>
    );


function a11yProps(index) 
    return 
        id: `simple-tab-$index`,
        'aria-controls': `simple-tabpanel-$index`,
    ;


function TabOneContents(props) 
    const classes = props;
    return (
        <>
            <Typography variant="h4" component='h1' className=classes.headline>
                Headline 1
            </Typography>

            <Typography variant="body1" className=classes.bodyCopy>
                Body Copy 1
            </Typography>
        </>
    )


function TabTwoContents(props) 
    const classes = props;
    const nurseOnboardingPath = '/navigator/onboarding/' + Meteor.userId() + '/1';

    return (
        <>
            <Typography variant="h4" component='h1' className=classes.headline>
                Headline 2
            </Typography>

            <Typography variant="body1" className=classes.bodyCopy>
                Body Copy 2
            </Typography>
        </>
    )


export default function MUITabPlusReactRouterDemo(props) 
    const history, match = props;
    const propsForDynamicClasses = ;
    const classes = docStyles(propsForDynamicClasses);
    const [value, setValue] = React.useState(history.location.pathname.includes('/tab_2') ? 1 : 0);

    const handleChange = (event, newValue) => 
        setValue(newValue);
        const pathName = '/' + (value == 0 ? 'tab_1' : 'tab_2');
        history.push(pathName);
    ;


    return (
        <div className=classes.appBarRoot>
            <AppBar position="static" color="transparent">
                <Tabs value=value onChange=handleChange aria-label="How It Works" textColor="primary">
                    <Tab label="Tab 1" ...a11yProps(0) />
                    <Tab label="Tab 2" ...a11yProps(1) />
                </Tabs>
            </AppBar>
            <TabPanel value=value index=0 classes=classes>
                <TabOneContents classes=classes/>
            </TabPanel>
            <TabPanel value=value index=1 classes=classes>
                <TabTwoContents classes=classes/>
            </TabPanel>
        </div>
    );

...在 React Router 中:

[.....]
<Route exact path="/tab_1"
       render=(routeProps) =>
           <MUITabPlusReactRouterDemo history=routeProps.history
           />
       />

<Route exact path="/tab_2"
       render=(routeProps) =>
           <MUITabPlusReactRouterDemo history=routeProps.history                           />
       />
[.....]

【讨论】:

【参考方案12】:
import React,  useContext, useEffect  from "react";
import PropTypes from "prop-types";
import Drawer from "@material-ui/core/Drawer";
import IconButton from "@material-ui/core/IconButton";
import MenuIcon from "@material-ui/icons/Menu";
import Typography from "@material-ui/core/Typography";
import useStyles from "./Styles";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Box from "@material-ui/core/Box";
import  __t  from "core/translation/translation";
import BrowserData from "core/helper/BrowserData";
import profileMenuItems from "./MenuItems";
import LayoutContext from "components/layout/core/LayoutContext";
import  useHistory, useParams  from "react-router-dom";

function TabPanel(props) 
  const  children, value, index, ...other  = props;
  return (
    <div
      role="tabpanel"
      hidden=value !== index
      id=`vertical-tabpanel-$index`
      aria-labelledby=`vertical-tab-$index`
      ...other
    >
      value === index && (
        <Box p=3>
          <Typography>children</Typography>
        </Box>
      )
    </div>
  );


TabPanel.propTypes = 
  children: PropTypes.node,
  index: PropTypes.any.isRequired,
  value: PropTypes.any.isRequired,
;

export default function UserProfile(props) 
  const  window  = props;
  const classes = useStyles();
  const history = useHistory();
  const  page  = useParams();
  const  isDesktop  = useContext(LayoutContext);
  const [open, setOpen] = React.useState(false);
  const [value, setValue] = React.useState(0);

  const handleChange = (event, newValue) => 
    setValue(newValue);
    history.push("/yourPath/" + newValue);
  ;

  useEffect(() => 
    if (!!page) 
      setValue(eval(page));
    
  , [page]);


  const getContent = () => 
    const  component: Component  = MenuItems[value];
    return <Component />;
  ;

  const handleDrawerToggle = () => 
    setOpen((prevState) => !prevState);
  ;

  const Menu = (
    <div>
      <Tabs
        orientation="vertical"
        variant="scrollable"
        value=value
        onChange=handleChange
        className=classes.tabs
      >
        MenuItems.map(
          ( label, iconPath, iconClassName = "" , index) => (
            <Tab
              label=label
              id=`vertical-tab-$index`
              aria-controls=`vertical-tabpanel-$index`
              className=classes.tab
              icon=
                <img className=iconClassName src=iconPath alt=label />
              
            />
          )
        )
      </Tabs>
    </div>
  );

  return (
    <div className=classes.root>
      <IconButton
        color="inherit"
        aria-label="open drawer"
        edge="start"
        onClick=handleDrawerToggle
        className=classes.drawerToggleButton
      >
        <MenuIcon />
      </IconButton>

      <nav className=classes.drawer>
        <Drawer
          anchor="left"
          open=isDesktop ? true : open
          onClose=handleDrawerToggle
          variant=isDesktop ? "permanent" : "temporary"
          classes=
            paper: classes.drawerPaper,
          
          ModalProps=
            keepMounted: true,
          
        >
          Menu
        </Drawer>
      </nav>

      <main className=classes.content>
        <TabPanel
          value=value
          key=value
          index=value
          className=classes.tabPanel
        >
          getContent()
        </TabPanel>
      </main>
    </div>
  );

【讨论】:

以上是关于Material-UI 的选项卡与反应路由器 4 集成?的主要内容,如果未能解决你的问题,请参考以下文章

为啥选择的选项隐藏在反应中?

将 Twitter Bootstrap 导航选项卡与 Jade 一起使用

iOS - 如何将选项卡与情节提要中的特定视图控制器链接?

我如何将 Material-UI 托管样式应用于非 Material-ui、非反应元素?

与 Material-UI 反应找不到模块

如何为每个 Material-UI TableRow 添加 <Link> react-router?