【问题标题】:Material-UI's Tabs integration with react router 4?Material-UI 的选项卡与反应路由器 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 属性?

提前致谢

【问题讨论】:

标签: reactjs material-ui react-router-v4


【解决方案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://stackoverflow.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>
                );
              }
              

              【讨论】:

                猜你喜欢
                • 2020-08-12
                • 1970-01-01
                • 2018-04-04
                • 2020-11-29
                • 2021-11-26
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2022-01-19
                相关资源
                最近更新 更多