Fluentui: Using react-router Link with Nav Component

Created on 31 Jan 2017  路  24Comments  路  Source: microsoft/fluentui

How can I use the react-router Link component with the fabric Nav Component? There is a issue #733 that may be related to this, but I am not sure.

I cannot use renderLink to render the Link component as that results in a nested hyperlink.

Type

Most helpful comment

FWIW - Here is how I got this working:

import React from 'react'
import { withRouter } from 'react-router-dom';
import { Nav } from 'office-ui-fabric-react/lib/Nav'

const FabricNav = withRouter(({ history }) => (
  <Nav
    onLinkClick={(event, element) => {
      event.preventDefault();
      history.push(element.url);
    }}
    groups={[
      {
        links: [
          {
            name: 'Home',
            url: '/',
            key: 'home',
          },
          {
            name: 'Foo',
            url: '/foo',
            key: 'foo',
          },
          {
            name: 'Bar',
            url: '/bar',
            key: 'bar',
          },
        ]
      }
    ]}
  />
));

export class LeftNav extends React.Component {
  render() {
    return (
      <div className="left-nav">
        <FabricNav />
      </div>
    )
  }
}

All 24 comments

Here's a customized example of the Nav:

http://codepen.io/dzearing/pen/ZBGNQg?editors=0010

Take a look at onRenderLink usage. It doesn't perfectly mix into Link but it should get you going. You can totally override the content within each link and render whatever you want.

Actually while this works to render custom content, it does not replace the anchor element, even though it's called onRenderLink. How confusing.

I think this is a bug. It should be rendering the NavLink by default, but should allow you to override that.

Given that react-router is pretty much essential for any SPA with react, I'd also like to see official support for this in office ui fabric. Until then, here's how I am using it with the Nav...

Step One: Modify the groups object
For every link or group inside your 'groups' remove all href tags and add a new custom tag containing the destination you want to send to react-router:

{ name: 'Supervisors', key: 'key4', link: '/administration/supervisors' }

Step Two: Add an event to the Nav tag
In the definition add a handler to the Nav's onLinkClick property:

            <Nav 
              groups={groups} 
              onLinkClick={this.onClick.bind(this)}
            />

Step Three: Add the event handler to your component
Add your custom event handler to the react component (as a sibling/peer to the render method), using the function name from Step Two and the property name from Step One - onClick and link were what I used, but feel free to use whatever you want.

Import the browserHistory component:

import { browserHistory } from 'react-router';

Then add the event handler function:

  onClick(event, element) {
    browserHistory.push(element.link);
  }

I found the programmatic way to navigate from this SO question:

http://stackoverflow.com/questions/31079081/programmatically-navigate-using-react-router

@graham-sportsmgmt you saved my day!

Actually I suggest to standardize all Nav items into button instead of a. Because all of this can be simplified using:

const groups = [
      {
        links:
        [
          { name: 'Dashboard', onClick: () => browserHistory.push('/dashboard'), key: '/dashboard' },
        ]
      }
    ];

BUT the onClick itself considered as buggy, since when we applied onClick, it render the link into button instead of a AND that makes styling went wrong.

image

That will happend when we use onClick. I don't know is it general bug or just happened on me.

I had this issue today as well, except with the ContextualMenu and Breadcrumb components.

ContextualMenu has IContextualMenuItem.onRender, similar to Nav.onRenderLink which was mentioned earlier in this thread. Breadcrumb has no way to customize the rendering of the links.

I haven't tested if the one from ContextualMenu has the same bug as Nav.onRenderLink, but regardless: I think it's not going to get us fully there yet.

If I would replace the default link markup with the Link component from React-Router, I will lose the Fabric styling on the link. So I think we need a way to combine the two.

I noticed that Semantic-UI-React has the following convention for rendering their components as a certain element or component: <ComponentName as="customElement"> or <ComponentName as={CustomComponent}>. (For example, have a look at: http://react.semantic-ui.com/elements/button)

Based on the use cases that I'm seeing here, I'd suggest that we need two separate solutions:

  • a solution to render a Link from Fabric as a custom component (perhaps with <Link as={CustomComponent}>). If all the props get transferred to the component that is rendered, we could easily wire together the Fabric Link and React-Router Link.
  • a separate solution for rendering all internal links in a component in that customized way (or, perhaps, even a way to render _all_ fabric links in the customized way). Basically some generic or scoped form of dependency injection. Other React libraries such as Redux and React-i18next do this by using a <SomethingProvider something={myCustomizedThing}> component, that is placed either at the root of the app or a more specific scope within the app.

As for a quick fix, mine was the following:

import {routerShape} from 'react-router';
import {Breadcrumb} from 'office-ui-fabric-react/lib/Breadcrumb';

class ExampleComponent extends Component {
  render() {
    <Breadcrumb
      items={[
        {
          key: 'foo', 
          text: 'Foo', 
          href: router.createHref('/bar'), 
          onClick: (e) => {e.preventDefault(); router.push('/bar')}
        }
      ]}
    />
  }
}
ExampleComponent.contextTypes = {
  router: routerShape
};

Note that I am setting both a href and onClick. The href is there to make sure that, in addition to instructing the router, it can also still behave like a regular link (e.g. open it in a new tab or window).

@graham-sportsmgmt Why not just add event.preventDefault() ?

onClick(event, element) {
    event.preventDefault();
    browserHistory.push(element.url);
  }

Instead of changing url to link props ?

Guys (specially from Microsoft and other maintainers of Fabric), did anyone got a solution for that?

The workarounds provided here brings a lot of side effects specially if you are using react-router v4.

Thanks!

No solution ever found, and I agree that the issues start to pile up with any workaround.

We figured that MS isn't putting a high priority on this project so we moved our project to a different UI framework.

@graham-sportsmgmt thanks for the reply. May I ask you which framework did you moved to?

We moved to Angular 4 and Bootstrap 4. We also looked at Zurb Foundation, Ionic, and the new Google Material, but Bootstrap had the widest set of components and the best Angular support.

I'm generating navigation this way:

import { Link } from 'react-router-dom'

const menuItems = [
  {
    key: 'Dashboard',
    name: <Link to="/">Home</Link>,
    icon: 'ViewDashboard'
  }

@jlanio Works but unfortunately it messes up the CSS and the ability to click the button, you just can click the text (default a tag behavior), still not the perfect work around

With react-router v4 and SSR, I make a Nav this way:

import { withRouter } from 'react-router-dom';
export class SideNav extends React.Component<ISideNavProps, any> {

    navWithRouter = withRouter(({ history }) => {
        return (
            <Nav
                groups={...}
                onLinkClick={
                    (ev?: React.MouseEvent<HTMLElement>, item?: INavLink) => {
                        if (item && item.key) {
                            history.push(item.key);
                        }
                    }}
                selectedKey={history.location.pathname}
            ></Nav>);
    });


    render() {
        return (
            <div>
                <this.navWithRouter />
            </div>
        );
    }
}

It works well

@ZeekoZhu I think you'll still have issues when trying to open the link in a new tab for example since it's not a tag.

Could a new component be written for this purpose ? or is it easier to adopt this component to use Links?

Here is my Solution, Altho this feels like a short sided way of doing things, it works, Ideally We could have a nav component that wraps a bunch of Links and lets the Links handle the logic ..

Also How can I change the SelectedKey?

 |  2 import * as React from 'react';                                                                                                                                                                                                                                                                                                                                                                                                     
 |  1 import { Nav, INavProps } from 'office-ui-fabric-react/lib/Nav';                                                                                                                                                                                                                                                                                                                                                                    
+|3   import { withRouter } from 'react-router-dom'                                                                                                                                                                                                                                                                                                                                                                                       
 |  1                                                                                                                                                                                                                                                                                                                                                                                                                                     
 |  2 class RightNavStub extends React.Component<any, any> {                                                                                                                                                                                                                                                                                                                                                                              
 |  3   constructor(props: INavProps) {                                                                                                                                                                                                                                                                                                                                                                                                   
 |  4     super(props);                                                                                                                                                                                                                                                                                                                                                                                                                   
 |  5     this._onClickHandler = this._onClickHandler.bind(this);                                                                                                                                                                                                                                                                                                                                                                         
 |  6   }                                                                                                                                                                                                                                                                                                                                                                                                                                 
 |  7                                                                                                                                                                                                                                                                                                                                                                                                                                     
 |  8   render() {                                                                                                                                                                                                                                                                                                                                                                                                                        
 |  9     return (                                                                                                                                                                                                                                                                                                                                                                                                                        
 | 10       <div className='nav'>                                                                                                                                                                                                                                                                                                                                                                                                         
 | 11         <Nav                                                                                                                                                                                                                                                                                                                                                                                                                        
 | 12           groups={                                                                                                                                                                                                                                                                                                                                                                                                                  
+| 13             [                                                                                                                                                                                                                                                                                                                                                                                                                       
 | 14               {                                                                                                                                                                                                                                                                                                                                                                                                                     
 | 15                 links:                                                                                                                                                                                                                                                                                                                                                                                                              
 | 16                 [                                                                                                                                                                                                                                                                                                                                                                                                                   
 | 17                   { name: 'Home', url: '/', key: 'key1', onClick: this._onClickHandler},                                                                                                                                                                                                                                                                                                                                            
 | 18                   { name: 'Login', url: '/login', key: 'key2', onClick: this._onClickHandler},                                                                                                                                                                                                                                                                                                                                      
 | 19                   { name: 'Logout', url: '/logout', key: 'key3', onClick: this._onClickHandler},                                                                                                                                                                                                                                                                                                                                    
 | 20                   { name: 'Wishlist', url: '/wishlist', key: 'key4', onClick: this._onClickHandler},                                                                                                                                                                                                                                                                                                                                
 | 21                   { name: 'Products', url: '/products/1', key: 'key5', onClick: this._onClickHandler},                                                                                                                                                                                                                                                                                                                              
 | 22                 ]                                                                                                                                                                                                                                                                                                                                                                                                                   
 | 23               }                                                                                                                                                                                                                                                                                                                                                                                                                     
~| 24             ]                                                                                                                                                                                                                                                                                                                                                                                                                       
~| 25           }                                                                                                                                                                                                                                                                                                                                                                                                                         
~| 26           expandedStateText={ 'expanded' }                                                                                                                                                                                                                                                                                                                                                                                          
 | 27           collapsedStateText={ 'collapsed' }                                                                                                                                                                                                                                                                                                                                                                                        
~| 28           selectedKey={ 'key1' }                                                                                                                                                                                                                                                                                                                                                                                                    
 | 29         />                                                                                                                                                                                                                                                                                                                                                                                                                          
 | 30       </div>                                                                                                                                                                                                                                                                                                                                                                                                                        
 | 31     );                                                                                                                                                                                                                                                                                                                                                                                                                              
 | 32   }                                                                                                                                                                                                                                                                                                                                                                                                                                 
 | 33                                                                                                                                                                                                                                                                                                                                                                                                                                     
 | 34   _onClickHandler(e: React.MouseEvent<HTMLElement>) {                                                                                                                                                                                                                                                                                                                                                                               
 | 35     e.preventDefault()                                                                                                                                                                                                                                                                                                                                                                                                              
 | 36     console.log(e.currentTarget.pathname)                                                                                                                                                                                                                                                                                                                                                                                           
 | 37     this.props.history.push(e.currentTarget.pathname);                                                                                                                                                                                                                                                                                                                                                                              
 | 38     return false;                                                                                                                                                                                                                                                                                                                                                                                                                   
 | 39   }                                                                                                                                                                                                                                                                                                                                                                                                                                 
 | 40 }                                                                                                                                                                                                                                                                                                                                                                                                                                   
 | 41                                                                                                                                                                                                                                                                                                                                                                                                                                     
 | 42 export const RightNav = withRouter(RightNavStub)  

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions to Fabric React!

I would like to update this thread, terribly sorry about the delay, but we are dedicating resources on cleaning up things like this, and will refine the set of components to address customization like this.

We are planning on apply an "as" pattern across all Fabric components :

To replace rendering a component with another implementation, you would could pass in "renderAs" value, which can be a component (functional or class based), or a string representing the new primitive root type. (Note that we tried to just use "as" but turns out it's already an html attribute type, so for components which mix in html attributes into the root element, that would conflict.)

For example:

<Link renderAs={ReactRouterLink} ... />

or:

<Link renderAs={ linkProps => (<SomethingDifferent url={ linkProps.href } />) } />

When a component renders multiple types of children, we use the "as" pattern but apply the part name for the parameter. For example "fooAs" would render a give type for the "foo" child.

So, we are planning some updates here:

Breadcrumb: will have items array, but also itemAs for rendering the crumbs and dividerAs for rendering the dividers.

Nav: will have items and itemAs.

ContextualMenu will have items and itemsAs.

We would like things to be as consistent as possible.

The first related PR which updates the Link component is already submitted and linked above. (#4639)

For Nav specifically, we really need to refactor it. We have this refactor going in an experiment currently and will graduate it to the main package in major release. (7.0 targeted currently.)

@dzearing whoops sorry, didn't read your last paragraph right.

FWIW - Here is how I got this working:

import React from 'react'
import { withRouter } from 'react-router-dom';
import { Nav } from 'office-ui-fabric-react/lib/Nav'

const FabricNav = withRouter(({ history }) => (
  <Nav
    onLinkClick={(event, element) => {
      event.preventDefault();
      history.push(element.url);
    }}
    groups={[
      {
        links: [
          {
            name: 'Home',
            url: '/',
            key: 'home',
          },
          {
            name: 'Foo',
            url: '/foo',
            key: 'foo',
          },
          {
            name: 'Bar',
            url: '/bar',
            key: 'bar',
          },
        ]
      }
    ]}
  />
));

export class LeftNav extends React.Component {
  render() {
    return (
      <div className="left-nav">
        <FabricNav />
      </div>
    )
  }
}

@mj1856 It tells me You should not use <Route> outside a <Router>. How do I solve this?
I use connected-react-router.

index.js

import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
...
ReactDOM.render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>,
  rootElement
);

App.js

export default () => (
  <Layout>
    <Switch>
      <Route exact path="/" component={Home} />
      <Route path="/page" component={Page} />
    </Switch>
  </Layout>
);

Layout.js

export default function Layout(props) {
  return (
    <div class="ms-Fabric">
      <div class="ms-Grid-row">
        <div class="ms-Grid-col ms-lg2">
          <Navigation />
        </div>
        <div class="ms-Grid-col ms-lg10">{props.children}</div>
      </div>
    </div>
  );
}

So I don't know why this issue is closed.

The linkAs works very well as you can see in this example.

However it does not render the NavLink with the correct css styling that the FabricUI Nav uses, so it looks terrible. Font is different and indenting is non-existent. We still need to see an actual example where Fabric UI Nav renders traditional react NavLinks which work with the Router, and looks identical. Currently if you don't override linkAs it just redirects to different urls which is not how SPA websites work with react.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

luisrudge picture luisrudge  路  3Comments

VincentBailly picture VincentBailly  路  3Comments

prashkan picture prashkan  路  3Comments

nekoya picture nekoya  路  3Comments

prashkan picture prashkan  路  3Comments