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

Button & React Router #984

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

Comments

Projects
None yet
7 participants
@iddan

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

This comment has been minimized.

Show comment
Hide comment
@borela

borela Nov 21, 2016

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@borela

borela Nov 25, 2016

Contributor

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

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

This comment has been minimized.

Show comment
Hide comment
@javivelasco

javivelasco Nov 25, 2016

Member

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.

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

This comment has been minimized.

Show comment
Hide comment
@borela

borela Nov 25, 2016

Contributor

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.

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

This comment has been minimized.

Show comment
Hide comment
@pmendelski

pmendelski Dec 16, 2016

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

pmendelski commented Dec 16, 2016

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

@devarsh

This comment has been minimized.

Show comment
Hide comment
@devarsh

devarsh 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 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

This comment has been minimized.

Show comment
Hide comment
@devarsh

devarsh 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

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

This comment has been minimized.

Show comment
Hide comment
@niksajanjic

niksajanjic Dec 1, 2017

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.

niksajanjic commented Dec 1, 2017

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

This comment has been minimized.

Show comment
Hide comment
@skoob13

skoob13 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,
};

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