Skip to content

Instantly share code, notes, and snippets.

@CalebEverett
Last active January 26, 2017 21:01
Show Gist options
  • Save CalebEverett/712fb3948d2695a95957 to your computer and use it in GitHub Desktop.
Save CalebEverett/712fb3948d2695a95957 to your computer and use it in GitHub Desktop.
Help with material-ui leftNav,react-router and redux

I am having trouble getting this to work. I'm trying to get the material ui leftNav to work with react router and redux with nav going through the store and router. This was close, but didn't get all the way to the menu item.

TypeError: Cannot read property 'isActive' of undefined

I was also referring to these examples: https://github.com/callemall/material-ui/blob/master/docs/src/app/components/app-left-nav.jsx

http://codetheory.in/react-integrating-routing-to-material-uis-left-nav-or-other-components/

Here is the component I'm working on. I started with react-redux-universal-hot-example. This component is going into the App component.

import React, { Component } from 'react';
import LeftNav from 'material-ui/lib/left-nav';
import RaisedButton from 'material-ui/lib/raised-button';

const menuItems = [
  { route: '/widgets', text: 'Widgets' },
  { route: 'survey', text: 'Survey' },
  { route: 'about', text: 'About' }
];

export default class MaterialLeftNav extends Component {
  static propTypes = {
    history: React.PropTypes.object
  }

  static contextTypes = {
    location: React.PropTypes.object,
    history: React.PropTypes.object
  }

  contructor(props) {
    super(props);
  }

  _onLeftNavChange(e, key, payload) {
    this.props.history.pushState(null, payload.route);
  }

  _handleTouchTap() {
    this.refs.leftNav.toggle();
  }

  _getSelectedIndex() {
    let currentItem;

    for (let i = menuItems.length - 1; i >= 0; i--) {
      currentItem = menuItems[i];
      if (currentItem.route && this.props.history.isActive(currentItem.route)) return i;
    }
  }

  render() {
    return (
      <div>
        <LeftNav
          ref="leftNav"
          docked
          menuItems={menuItems}
          selectedIndex={this._getSelectedIndex()}
          onChange={this._onLeftNavChange}
        />
        <RaisedButton label="Toggle Menu" primary onTouchTap={this._handleTouchTap} />
      </div>
    );
  }

}
@CalebEverett
Copy link
Author

Not sure if this is the best way to do this, but it worked, so thought I would leave it in case somebody else got stuck with this.

I ended up getting this to work by moving the specification of the menu items into an array and up to the component that the LeftNav and MenuItems were being rendered in and then binding the index number of the array through the onTouchTap event handler so history.pushState could pull the new route from the menu items.

The other key was to define the history contextType on App component and then make it available in the _handleMenuItemTouchTap function. Here's the complete App component.

MaterialLeftNav.js

/** In this file, we create a React component which incorporates components provided by material-ui */

import React, { Component, PropTypes } from 'react';
import LeftNav from 'material-ui/lib/left-nav';
import MenuItem from 'material-ui/lib/menus/menu-item';
import RaisedButton from 'material-ui/lib/raised-button';

export default class MaterialLeftNav extends Component {
  static propTypes = {
    menuitem: PropTypes.shape({
      primaryText: PropTypes.string.isRequired,
      value: PropTypes.string.isRequired
    }),
    menuitems: PropTypes.array
  }

  static contextTypes = {
    history: PropTypes.object.isRequired
  }

  _handleMenuItemTouchTap(i) {
    const {history} = this.context;
    history.pushState(null, this.props.menuitems[i].value);
    this.refs.leftNavChildren.toggle();
  }

  _handleToggleButtonTouchTap() {
    this.refs.leftNavChildren.toggle();
  }

  render() {
    const styles = require('./MaterialLeftNav.scss');

    return (
      <div>
        <LeftNav ref="leftNavChildren" docked={false}>
          {this.props.menuitems.map(function(menuitem, i) {
            return ( <MenuItem index={i} primaryText={menuitem.primaryText} value={menuitem.value} onTouchTap={::this._handleMenuItemTouchTap.bind(this, i)}/>
              );
          }, this) }
        </LeftNav>
        <div className={styles.menuToggle}>
          <RaisedButton label="Toggle Menu" primary onTouchTap={::this._handleToggleButtonTouchTap} />
        </div>
      </div>
    );
  }
}

App.js

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import DocumentMeta from 'react-document-meta';
import { isLoaded as isInfoLoaded, load as loadInfo } from 'redux/modules/info';
import { isLoaded as isAuthLoaded, load as loadAuth, logout } from 'redux/modules/auth';
import { InfoBar, Test, MaterialLeftNav } from 'components';
import { pushState } from 'redux-router';
import config from '../../config';
const ThemeManager = require('material-ui/lib/styles/theme-manager');
const themeDecorator = require('material-ui/lib/styles/theme-decorator');
const spTheme = require('../../theme/sptheme.js');

const menuitems = [
  {primaryText: 'Survey', value: '/survey'},
  {primaryText: 'Widgets', value: '/widgets'},
  {primaryText: 'Home', value: '/'}
];

import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();

@themeDecorator(ThemeManager.getMuiTheme(spTheme))
@connect(
  state => ({user: state.auth.user}),
  {logout, pushState})
export default class App extends Component {
  static propTypes = {
    children: PropTypes.object.isRequired,
    user: PropTypes.object,
    logout: PropTypes.func.isRequired,
    pushState: PropTypes.func.isRequired
  };

  static contextTypes = {
    store: PropTypes.object.isRequired,
    history: PropTypes.object.isRequired
  };

  componentWillReceiveProps(nextProps) {
    if (!this.props.user && nextProps.user) {
      // login
      this.props.pushState(null, '/loginSuccess');
    } else if (this.props.user && !nextProps.user) {
      // logout
      this.props.pushState(null, '/');
    }
  }

  static fetchData(getState, dispatch) {
    const promises = [];
    if (!isInfoLoaded(getState())) {
      promises.push(dispatch(loadInfo()));
    }
    if (!isAuthLoaded(getState())) {
      promises.push(dispatch(loadAuth()));
    }
    return Promise.all(promises);
  }

  handleLogout(event) {
    event.preventDefault();
    this.props.logout();
  }

  render() {
    const styles = require('./App.scss');
    return (
      <div className={styles.app}>
        <DocumentMeta {...config.app}/>
        <div className={styles.appContent}>
          <MaterialLeftNav menuitems={menuitems}/>
          {this.props.children}
        </div>
        <Test/>
        <InfoBar/>

        <div className="well text-center">
          Have questions? Ask for help <a
          href="https://github.com/erikras/react-redux-universal-hot-example/issues"
          target="_blank">on Github</a> or in the <a
          href="https://discordapp.com/channels/102860784329052160/105739309289623552" target="_blank">#react-redux-universal</a> Discord channel.
        </div>
      </div>
    );
  }
}

@CalebEverett
Copy link
Author

This ended up being a really verbose way to get this done, mostly because I'm learning how to code. There was a callback in the onChange that included the variables needed to specify the path. I still needed access to history in the way described here, but getting the path passed from the menu turned out to be much more straightforward.

There were a few other things in here as well that I'm leaving in for my own future reference and the off chance that some other noob like myself stumbles across this. The first was passing the path down to be able to use in the app bar and the second was the passing down of the theme file to access the color variables. The last one was the passing down of the window size info from the state, which was being used here to dock the left nave on screens bigger than medium.

_handleLeftNavChange(event, selectedIndex, menuItem) {
    const {history} = this.context;
    history.pushState(null, menuItem.route);
    this.refs.leftNav.toggle();
  }

  _toggleLeftNav() {
    this.refs.leftNav.toggle();
  }

  render() {
    const styles = require('./MaterialLeftNav.scss');
    const path = (!this.props.path) ? 'Home' : this.props.path;
    const {palette} = this.context.spTheme;
    const divColor = palette.primary3Color;
    const leftNavHeader = (
      <div style={{backgroundColor: divColor, height: '12em'}}>Niko</div>
    );

    const menuItems = [
      {key: 0, text: 'Survey', route: '/survey'},
      {key: 1, text: 'Widgets', route: '/widgets'},
      {key: 2, text: 'Home', route: '/'}
    ];

    return (
      <div className={styles}>
        <AppBar
          title={path}
          onLeftIconButtonTouchTap={::this._toggleLeftNav}
        />
        <LeftNav
          ref="leftNav"
          menuItems={menuItems}
          onChange={::this._handleLeftNavChange}
          docked={this.props.browser.greaterThan.medium}
          header={leftNavHeader}
        />
      </div>
    );
  }

@loganwedwards
Copy link

Just to add another comment: I'm using React v.14 and React Router 1.0 and found this to make routing work nicely with my application. It looks like the React Router api changed for this stuff from Navigation to the History object. I fought with this for a while and after realizing that I'm using ES6 classes (no mixins allowed) and Router v1.0, this is a way to make it work. Some of this was inspired by @CalebEverett above (so, thank you!). A main difference is I've added the History object to the Navigation (my component, not to be confused with React Router) context.

import React from 'react';
// Link not used anymore
//import { Link } from 'react-router';
import AppBar from 'material-ui/lib/app-bar';
import LeftNav from 'material-ui/lib/left-nav';
import MenuItem from 'material-ui/lib/menus/menu-item';

class Navigation extends React.Component {

    toggleNav() {
        this.refs.leftNav.toggle();
    };

    // since we are not using React Router <Link>, this manually
    // changes the state using the History object
    handleNavChange(event, selectedIndex, menuItem) {
        this.context.history.pushState(null, menuItem.route);
        this.refs.leftNav.toggle();
    }

    render() {
        // TODO: React Router typically uses <Link> components for routing
        // We are hacking around using Material-Ui instead with passing the
        // History object for routing
        // <Link to={item.route}>{item.text}</Link>

        // Our nav links. key is required by Material-UI
        const navLinks = [
            {route: '/dashboard', text: 'Dashboard', key: 0},
            {route: '/settings', text: 'Settings', key: 1},
            {route: '/logout', text: 'Logout', key: 2}
        ];

        return (
            <div>
                <AppBar title="ISI Dashboard" onLeftIconButtonTouchTap={this.toggleNav.bind(this)}></AppBar>
                <LeftNav ref="leftNav" docked={false} menuItems={navLinks} onChange={this.handleNavChange.bind(this)} />
            </div>
        );
    }
}

// Since this is not a <Route> component, we add History to the context
Navigation.contextTypes = {
  history: React.PropTypes.object
};

export default Navigation;

My app looks like:

import React from 'react';
import ReactDOM from 'react-dom';
import Router from 'react-router';
import { Link, Route, IndexRoute } from 'react-router';
import './main.scss';
import App from './components/App/app';
import Dashboard from './components/Dashboard/dashboard';
import Settings from './components/Settings/settings';

/**

This is the entry point for the app.

*/

let injectTapEventPlugin = require("react-tap-event-plugin");

//Needed for onTouchTap
//Can go away when react 1.0 release
//Check this repo:
//https://github.com/zilverline/react-tap-event-plugin
injectTapEventPlugin();

ReactDOM.render((
    <Router>
        <Route path="/" component={App}>
            <IndexRoute component={Dashboard} />
            <Route path="dashboard" component={Dashboard} />
            <Route path="settings" component={Settings} />
        </Route>
    </Router>
), document.getElementById('app'));
import React from 'react';
import Navigation from '../Navigation/navigation';

class App extends React.Component {
    render() {
        return (
            <div>
                <Navigation />
                {this.props.children}
            </div>
        );
    }

}

export default App;

@jruts
Copy link

jruts commented May 23, 2016

  1. You can just initialise the tap event plugin directly:
    require("react-tap-event-plugin")()
  2. If your app has no state you do not need to wrap it in a component. A normal function will do.
    So you can write your App component like this:
import React from 'react';

const App = () => (
  <div>
    <Navigation />
     {this.props.children}
  </div>
)

export default App

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment