Add support for activeClassName for the NamedLink component

This commit is contained in:
Kimmo Puputti 2017-02-16 17:15:27 +02:00
parent 48d6b3a3bd
commit 4045146364
13 changed files with 137 additions and 47 deletions

View file

@ -3,13 +3,13 @@ exports[`FilterPanel matches snapshot 1`] = `
<h1>
Filters
</h1>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="SearchListingsPage">
See studios
</withFlattenedRoutes(NamedLink)>
<withFlattenedRoutes(NamedLink)
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
name="SearchListingsPage">
X
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</div>
`;

View file

@ -9,7 +9,7 @@ exports[`ListingCard matches snapshot 1`] = `
</div>
<div>
<div>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="ListingPage"
params={
Object {
@ -18,7 +18,7 @@ exports[`ListingCard matches snapshot 1`] = `
}
}>
Banyan Studios
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
<div>
55€ / day
</div>

View file

@ -8,7 +8,7 @@ exports[`ListingCardSmall matches snapshot 1`] = `
</div>
</div>
<div>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="ListingPage"
params={
Object {
@ -17,7 +17,7 @@ exports[`ListingCardSmall matches snapshot 1`] = `
}
}>
Banyan Studios
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
<div>
(
<span>

View file

@ -4,13 +4,13 @@ exports[`MapPanel matches snapshot 1`] = `
Map
</div>
<div />
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="SearchFiltersPage">
Filters
</withFlattenedRoutes(NamedLink)>
<withFlattenedRoutes(NamedLink)
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
name="SearchListingsPage">
X
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</div>
`;

View file

@ -1,30 +1,80 @@
/**
* This component wraps React-Router's Link by providing name-based routing.
* (Helps to narrow down the scope of possible format changes to routes.)
*
* The `name` prop should match a route in the flattened
* routesConfiguration passed in context by the RoutesProvider
* component. The `params` props is the route params for the route
* path of the given route name.
*
* The `to` prop is an object with the same shape as Link requires,
* but without `pathname` that will be generated from the given route
* name.
*
* Some additional props can be passed for the <a> element like
* `className` and `style`.
*
* The component can also be given the `activeClassName` prop that
* will be added to the element className if the current URL matches
* the one in the generated pathname of the link.
*/
import React, { PropTypes } from 'react';
import { Link } from 'react-router-dom';
import { Link, withRouter } from 'react-router-dom';
import classNames from 'classnames';
import { pathByRouteName, withFlattenedRoutes } from '../../util/routes';
import * as propTypes from '../../util/propTypes';
const NamedLink = props => {
const { name, search, hash, state, params, flattenedRoutes, ...rest } = props;
export const NamedLinkComponent = props => {
const { name, params, flattenedRoutes } = props;
// Link props
const { to, children } = props;
const pathname = pathByRouteName(name, flattenedRoutes, params);
const locationDescriptor = { pathname, search, hash, state };
return <Link to={locationDescriptor} {...rest} />;
const { match } = props;
const active = match.url && match.url === pathname;
// <a> element props
const { className, style, activeClassName } = props;
const aElemProps = { className: classNames(className, { [activeClassName]: active }), style };
return <Link to={{ pathname, ...to }} {...aElemProps}>{children}</Link>;
};
const { arrayOf, object, string } = PropTypes;
const { arrayOf, object, string, shape, any } = PropTypes;
NamedLink.defaultProps = { search: '', hash: '', state: {}, params: {} };
NamedLinkComponent.defaultProps = {
params: {},
to: {},
children: null,
className: '',
style: {},
activeClassName: 'NamedLink_active',
match: {},
};
NamedLink.propTypes = {
// This ensures a nice display name in snapshots etc.
NamedLinkComponent.displayName = 'NamedLink';
NamedLinkComponent.propTypes = {
// name of the route in routesConfiguration
name: string.isRequired,
search: string,
hash: string,
state: object,
// params object for the named route
params: object,
// Link component props
to: shape({ search: string, hash: string, state: object }),
children: any,
// Note: The Link `replace` prop conflicts with the replace function
// from withRouter. If the replace boolean is required for the Link
// component, add it here with another name.
// generic props for the underlying <a> element
className: string,
style: object,
activeClassName: string,
// from withFlattenedRoutes
flattenedRoutes: arrayOf(propTypes.route).isRequired,
// from withRouter
match: object,
};
export default withFlattenedRoutes(NamedLink);
export default withFlattenedRoutes(withRouter(NamedLinkComponent));

View file

@ -1,7 +1,41 @@
import React from 'react';
import { RoutesProvider } from '../index';
import { renderDeep } from '../../util/test-helpers';
import NamedLink from './NamedLink';
import { renderShallow, renderDeep } from '../../util/test-helpers';
import NamedLink, { NamedLinkComponent } from './NamedLink';
describe('NamedLinkComponent', () => {
it('should mark the link as active if the current URL matches', () => {
const routesConf = [
{ path: '/a', name: 'APage', component: () => null },
{ path: '/b', name: 'BPage', component: () => null },
];
const activeClassName = 'my-active-class';
const aProps = {
name: 'APage',
activeClassName,
flattenedRoutes: routesConf,
match: { url: '/a' },
};
const bProps = {
name: 'BPage',
activeClassName,
flattenedRoutes: routesConf,
match: { url: '/a' },
};
const tree = renderDeep(
<div>
<NamedLinkComponent {...aProps}>link to a</NamedLinkComponent>
<NamedLinkComponent {...bProps}>link to b</NamedLinkComponent>
</div>,
);
const aLink = tree.children[0];
const bLink = tree.children[1];
expect(aLink.type).toEqual('a');
expect(bLink.type).toEqual('a');
expect(aLink.props.className).toEqual(activeClassName);
expect(bLink.props.className).toEqual('');
});
});
describe('NamedLink', () => {
it('should contain correct link', () => {

View file

@ -28,7 +28,7 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
<p>
Cancel booking
</p>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDiscussionPage"
params={
Object {
@ -36,6 +36,6 @@ exports[`OrderDetailsPanel matches snapshot 1`] = `
}
}>
You have a new message!
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</div>
`;

View file

@ -2,14 +2,14 @@ exports[`SearchResultsPanel matches snapshot 1`] = `
<div>
<Menu />
<div>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="SearchFiltersPage">
Filters
</withFlattenedRoutes(NamedLink)>
<withFlattenedRoutes(NamedLink)
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
name="SearchMapPage">
Map
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</div>
</div>
`;

View file

@ -19,7 +19,7 @@ exports[`CheckoutPage matches snapshot 1`] = `
<p>
By confirming I accept the booking terms and conditions.
</p>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDetailsPage"
params={
Object {
@ -27,6 +27,6 @@ exports[`CheckoutPage matches snapshot 1`] = `
}
}>
Confirm & Pay
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</Connect(withRouter(PageLayout))>
`;

View file

@ -123,7 +123,7 @@ exports[`ListingPage matches snapshot 1`] = `
<div>
Map
</div>
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDetailsPage"
params={
Object {
@ -131,6 +131,6 @@ exports[`ListingPage matches snapshot 1`] = `
}
}>
Book Banyan Studios
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</Connect(withRouter(PageLayout))>
`;

View file

@ -1,9 +1,9 @@
exports[`NotFoundPage matches snapshot 1`] = `
<Connect(withRouter(PageLayout))
title="Page not found">
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="LandingPage">
Home
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
</Connect(withRouter(PageLayout))>
`;

View file

@ -1,7 +1,7 @@
exports[`OrderPage matches snapshot 1`] = `
<Connect(withRouter(PageLayout))
title="Your Banyan Studios booking is confirmed!">
<withFlattenedRoutes(NamedLink)
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDetailsPage"
params={
Object {
@ -14,8 +14,8 @@ exports[`OrderPage matches snapshot 1`] = `
}
}>
Booking details
</withFlattenedRoutes(NamedLink)>
<withFlattenedRoutes(NamedLink)
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
name="OrderDiscussionPage"
params={
Object {
@ -23,7 +23,7 @@ exports[`OrderPage matches snapshot 1`] = `
}
}>
Discussion
</withFlattenedRoutes(NamedLink)>
</withFlattenedRoutes(withRouter(NamedLink))>
<OrderDetailsPanel
className="undefined"
confirmationCode="X2587X"

View file

@ -1,13 +1,17 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link, withRouter } from 'react-router-dom';
import { withRouter } from 'react-router-dom';
import { logout } from '../../ducks/Auth.ducks';
import { NamedLink } from '../../components';
import css from './Topbar.css';
/* eslint-disable react/no-danger */
const House = () => <span dangerouslySetInnerHTML={{ __html: '&#127968;' }} />;
/* eslint-enable react/no-danger */
const Topbar = props => {
const { isAuthenticated, onLogout, user, push: historyPush } = props;
const house = { dangerouslySetInnerHTML: { __html: '&#127968;' } };
const hamburger = { dangerouslySetInnerHTML: { __html: '&#127828;' } };
const handleChange = e => {
@ -24,7 +28,9 @@ const Topbar = props => {
return (
<div className={css.container}>
<div>
<Link className={css.home} to="/" {...house} />
<NamedLink className={css.home} name="LandingPage">
<House />
</NamedLink>
<select value="default" className={css.navDropDown} onChange={handleChange}>
<option value="default" {...hamburger} />
<option value="/s">Search</option>
@ -51,7 +57,7 @@ const Topbar = props => {
{user.email}
<button className={css.logoutButton} onClick={handleLogout}>Logout</button>
</div>
: <Link to="/login">Login</Link>}
: <NamedLink name="LogInPage">Login</NamedLink>}
</div>
</div>
);