Skip to content

Button & React Router #984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
iddan opened this issue Nov 20, 2016 · 9 comments
Closed

Button & React Router #984

iddan opened this issue Nov 20, 2016 · 9 comments

Comments

@iddan
Copy link

iddan commented Nov 20, 2016

I've seen the Button component has a href prop which you can pass a URI.
Can it interface with React Router as the Link component?

@borela
Copy link
Contributor

borela commented Nov 21, 2016

What I am doing is enveloping the button with the react router's link:

import React from 'react';
import RtButton from 'react-toolbox/lib/button/Button';
import {Link} from 'react-router';

export class Button extends React.Component {
  render() {
    const {href, ...otherProps} = this.props;

    if (href == undefined) {
      return <RtButton {...otherProps}/>;
    }

    return (
      <Link to={href}>
        <RtButton {...otherProps}/>
      </Link>
    );
  }
};

export default Button;

Also you may want to change the import to:

import RtButton from 'react-toolbox/lib/button';

If you are not loading the themes manually like I do.

@borela
Copy link
Contributor

borela commented Nov 25, 2016

As I had some troubles with the previous implementation, I created a package to interface with the v4 router.

@javivelasco
Copy link
Member

javivelasco commented Nov 25, 2016

What about an HOC? Try this:

const withReactRouterLink = Component =>
  class Decorated extends React.Component {
    static propTypes = {
      activeClassName: PropTypes.string,
      className: PropTypes.string,
      to: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.func
      ])
    }

    static contextTypes = {
      router: PropTypes.object
    };

    resolveToLocation = to => {
      const { router } = this.context;
      return typeof to === 'function' ? to(router.location) : to
    }

    handleClick = event => {
      const { to } = this.props;
      const { router } = this.context;
      event.preventDefault();
      router.push(this.resolveToLocation(to));
    }

    render () {
      const { router } = this.context;
      const { activeClassName, className, to, ...rest } = this.props;
      const toLocation = this.resolveToLocation(to);
      const isActive = router.isActive(toLocation);
      const _className = isActive ? `${className} ${activeClassName}` : className;

      return (
        <Component
          {...rest}
          className={_className}
          href={toLocation}
          onClick={this.handleClick}
        />
      );
    }
  };

Then you can create a wrapper around React Toolbox and use it like a Link from react-router:

import { Button } from 'react-toolbox/lib/button';
const ReactToolboxLink = withReactRouterLink(Button);

export const Test = () => 
    <ReactToolboxLink activeClassName="active" to="/location" primary raised />

I don't know if it works with RR4 though.

@borela
Copy link
Contributor

borela commented Nov 25, 2016

I would actually love to have a component that supports it as part of this library, it could have a property to enable support.

The main issue are themes, the only way to have it fully working "out of the box" was to clone the current code for the button, iconbutton, link and make minor changes so that it works as before but now it supports router's link and I don't have to know the internals (it just forwards the props).

In theory that could work with router v1, v2, v3 and v4 because if I am not mistaken, the required props for the link component didn't change since the first version, but I only tested with the v4.

@pmendelski
Copy link

I agree with @borela. No way to setup the typical routing behavior for Single Page Applications is a major drawback.

@devarsh
Copy link

devarsh commented Feb 13, 2017

Have modified the HOC for react router 4

RRHoc.js

import React, {Component, PropTypes } from 'react'
import { withRouter } from 'react-router-dom'

export const withReactRouterLink = Component => {

  class Decorated extends React.Component {
    constructor(props,context) {
      super(props,context)
      this.state = {active: false}
    }
    static propTypes = {
      activeClassName: PropTypes.string,
      className: PropTypes.string,
      target: PropTypes.string,
      to: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.object,
      ]).isRequired,
    };

    resolveToLocation = to => {
      return typeof to === 'object' ? to['pathname'] : to
    }

    isActive = (toLocation, nextProps) => {
      const currProps = nextProps || this.props
      const { location, to } = currProps
      return toLocation == location.pathname
    }
    handleClick = event => {
      event.preventDefault();
      const { to } = this.props;
      this.props.push(to)
      this.setState({active: this.isActive(to)})
    }

    componentWillMount() {
      const { to } = this.props;
      this.setState({active: this.isActive(to)})
    }
    componentWillReceiveProps(nextProps) {
      const { to } = this.props;
      if (this.state.active != this.isActive(to,nextProps)) {
        this.setState({active: this.isActive(to,nextProps)})
      }
    }
    shouldComponentUpdate(nextProps, nextState) {
      const { to } = this.props;
      return this.state.active != nextState.active
    }
    render () {
      const { activeClassName, className, to,  ...rest } = this.props;
      const toLocation = this.resolveToLocation(to);
      const _className = this.state.active ? `${className} ${activeClassName}` : className;
      return (
        <Component
          {...rest}
          className={_className}
          href={toLocation}
          onClick={this.handleClick}
        />
      );
    }
  };
  return withRouter(Decorated)
}

Usage

import { Route, BrowserRouter as Router } from 'react-router-dom'
import { List, ListItem } from 'react-toolbox'

import React, { Component, PropTypes } from 'react';

import {withReactRouterLink} from './RRHoc.js'

const RRListItem = withReactRouterLink(ListItem)

const RrRt = () => (
  <List>
  <RRListItem caption="Home" to="/home" activeClassName="customRouterListActive" className="customRouterList" />
  <RRListItem caption="School" to="/School" activeClassName="customRouterListActive" className="customRouterList" />
  <RRListItem caption="Work" to="/Work" activeClassName="customRouterListActive" className="customRouterList" />
  </List>
)


const Expose = () => (
  <Router>
  <div>
    <RrRt/>
    <Route path="/home" component={()=> <h1> Home </h1>} />
    <Route path="/School" component={()=> <h1> School </h1> } />
     <Route path="/Work" component={()=> <h1> Work </h1> } />
  </div>
  </Router>
)

export default Expose

css

.customRouterList {
  text-decoration: none;
}
.customRouterListActive {
  background-color : rgb(238, 238, 238);
}

@devarsh
Copy link

devarsh commented Feb 15, 2017

@javivelasco I know this has been an old issue, but just want your thoughts.

I've been experimenting with passing Functions as children vs HOC for this particular use case, so we can passing any Element as a child to this component.

RRLinkFunc.js

import React, {Component, PropTypes } from 'react'
import { withRouter } from 'react-router-dom'

class Decorated extends React.Component {
  constructor(props,context) {
    super(props,context)
    this.state = {active: false}
  }
  static propTypes = {
    to: PropTypes.oneOfType([
      PropTypes.string,
      PropTypes.object,
    ]).isRequired,
    children: React.PropTypes.func.isRequired,
  };

  resolveToLocation = to => {
    return typeof to === 'object' ? to['pathname'] : to
  }

  isActive = (toLocation, nextProps) => {
    const currProps = nextProps || this.props
    const { location, to } = currProps
    return toLocation == location.pathname
  }
  handleClick = event => {
    event.preventDefault();
    const { to } = this.props;
    this.props.push(to)
    this.setState({active: this.isActive(to)})
  }

  componentWillMount() {
    const { to } = this.props;
    this.setState({active: this.isActive(to)})
  }
  componentWillReceiveProps(nextProps) {
    const { to } = this.props;
    if (this.state.active != this.isActive(to,nextProps)) {
      this.setState({active: this.isActive(to,nextProps)})
    }
  }
  shouldComponentUpdate(nextProps, nextState) {
    return this.state.active != nextState.active
  }
  render () {
    const { to } = this.props;
    const toLocation = this.resolveToLocation(to);
    return this.props.children(toLocation,this.handleClick,this.state.active)
  }
}

export default withRouter(Decorated)

Usage

import { Route, BrowserRouter as Router } from 'react-router-dom'
import { List, ListItem } from 'react-toolbox'
import {Button} from 'react-toolbox/lib/button';

import React, { Component, PropTypes } from 'react';

import RRLink from './RRLinkFunc.js'

const RrRt = () => (
  <div>
  <List>
    <RRLink key={1} to="/home">
       {
        (location,handler,isActive) => {
          const className = 'customRouterList'
          const activeClassName = 'customRouterListActive'
          const _className = isActive ? `${className} ${activeClassName}` : className;
          return <ListItem caption="Home" onClick={handler} className={_className} />
        }
      }
    </RRLink>
    <RRLink key={2} to="/school">
      {
        (location,handler,isActive) => {
          const className = 'customRouterList'
          const activeClassName = 'customRouterListActive'
          const _className = isActive ? `${className} ${activeClassName}` : className;
          return <ListItem to={location} caption="School" onClick={handler} className={_className} />
        }
      }
    </RRLink>
  </List>
  <RRLink key={3} to="/work">
    {
      (location,handler,isActive) => {
        const className = 'customRouterList'
        const activeClassName = 'customRouterListActive'
        const _className = isActive ? `${className} ${activeClassName}` : className;
        return <Button href={location} label="Work" onClick={handler} />
      }
    }
  </RRLink>
  </div>
)


const Expose = () => (
  <Router>
  <div>
    <RrRt/>
    <Route path="/home" component={()=> <h1> Home </h1>} />
    <Route path="/school" component={()=> <h1> School </h1> } />
     <Route path="/work" component={()=> <h1> Work </h1> } />
  </div>
  </Router>
)

export default Expose

@niksajanjic
Copy link

Neither of the proposed solutions up there works for me with RRv4.2. But either of those examples is overly complicated and can be simplified. If you take a look at the example for the custom link provided at react router documentation:

https://reacttraining.com/react-router/web/example/custom-link

You can just alter a code and make it work with React Toolbox Link:

import React from 'react'
import PropTypes from 'prop-types'
import { Route } from 'react-router-dom'
import Link from 'react-toolbox/lib/link'

const handleClick = (event, history, to) => {
  event.preventDefault()

  history.push(typeof to === 'object' ? to.pathname : to)
}

const RRLink = ({ to, exact, strict, ...rest }) =>
  <Route path={to} exact={exact} strict={strict} children={({ history, match }) => (
    <Link {...rest} active={!!match} onClick={(event) => handleClick(event, history, to)} />
  )} />

RRLink.propTypes = {
  to: PropTypes.oneOfType([
    PropTypes.shape({
      pathname: PropTypes.string.isRequired
    }),
    PropTypes.string
  ]).isRequired,
  exact: PropTypes.bool,
  strict: PropTypes.bool
}

export default RRLink
import RRLink from './RRLink'
...
<RRLink exact to='/' label='Home' icon='home' />
<RRLink strict to='/locations' label='Lokacije' icon='place' />
...

This code simulates react router's Link component and up to some degree NavLink component. You can pass any additional props that React Toolbox accepts including theme and they will be attached using ...rest. If you want to use this component to fully simulate NavLink you also have to add logic for NavLink's properties: activeClassName, activeStyle, isActive and location, but I didn't need them.

@skoob13
Copy link

skoob13 commented Jan 29, 2018

Another solution for Button component:

import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';

export default WrappedComponent => withRouter(
  class ButtonWrapper extends PureComponent {
    static propTypes = {
      href: PropTypes.string,
      onClick: PropTypes.object,
      history: PropTypes.any.isRequired,
      asDefaultLink: PropTypes.bool,
    }

    onClick = (event) => {
      event.preventDefault();

      const { href, onClick, history } = this.props;
      history.push(href);

      if (onClick) {
        onClick(event);
      }
    }

    render() {
      const { asDefaultLink, ...props } = this.props;
      return (
        <WrappedComponent
          {...props}
          onClick={(props.href && !asDefaultLink) ? this.onClick : props.onClick}
        />
      );
    }
  },
);

Usage:

import {
  Button as DefaultButton,
  IconButton as DefaultIconButton,
} from 'react-toolbox/lib/button';
import interceptLink from './interceptLink';

const Button = interceptLink(DefaultButton);
const IconButton = interceptLink(DefaultIconButton);

export {
  Button,
  IconButton,
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants