diff --git a/CHANGELOG.md b/CHANGELOG.md index c3e3fd04..e368b640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,49 @@ ### [@coreui/react](https://coreui.io/) changelog +##### `v2.5.0` +- BREAKING CHANGE release for use with `react-router-dom v5` + - feat(Breadcrumb2): mandatory prop `router` :boom: see: [Breadcrumb](./src/Breadcrumb.md) + - feat(SidebarNav2): mandatory prop `router` :boom: see: [SidebarNav](./src/SidebarNav.md) +- refactor: demo update +- refactor(SidebarNav): rename `options` prop for PerfectScrollbar + +###### dependencies update +- update `react-router-dom` to `^5.0.0` -> moved to `peerDependencies` + +__BREAKING CHANGES:__ :boom: +- removed `react-router-dom` from `dependencies` +- deprecate 'Breadcrumb' in favour of `Breadcrumb2` +- deprecate 'SidebarNav' in favour of `SidebarNav2` + +usage in `DefaultLayout.js`: +```jsx +import * as router from 'react-router-dom'; +import { +AppBreadcrumb2 as AppBreadcrumb +AppSidebarNav2 as AppSidebarNav +} from '@coreui/react'; +// routes config +import routes from '../../routes.js'; +``` + +```html +... +
+ + + + +
+ + ... +
+ ... +
+... +``` + +--- + ##### `v2.1.7` - maintenance release for use with: - react-router `v4.3.x` diff --git a/demo/src/containers/DefaultLayout/DefaultLayout.js b/demo/src/containers/DefaultLayout/DefaultLayout.js index e4bb99df..2fce1fb0 100644 --- a/demo/src/containers/DefaultLayout/DefaultLayout.js +++ b/demo/src/containers/DefaultLayout/DefaultLayout.js @@ -1,11 +1,12 @@ import React, { Component } from 'react'; import { Redirect, Route, Switch } from 'react-router-dom'; +import * as router from 'react-router-dom'; import { Container, Nav, NavItem, NavLink, Badge, DropdownToggle, DropdownMenu } from 'reactstrap'; import { AppAside, AppAsideToggler, - AppBreadcrumb, + AppBreadcrumb2 as AppBreadcrumb, AppFooter, AppHeader, AppHeaderDropdown, @@ -15,7 +16,7 @@ import { AppSidebarForm, AppSidebarHeader, AppSidebarMinimizer, - AppSidebarNav, + AppSidebarNav2 as AppSidebarNav, AppSidebarToggler, } from '../../../../src'; // sidebar nav config @@ -64,13 +65,14 @@ class DefaultLayout extends Component { - + {/**/} +
- - + {/**/} + {routes.map((route, idx) => { diff --git a/package.json b/package.json index ff69cbdc..ea0e0966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coreui/react", - "version": "2.1.7", + "version": "2.5.0", "description": "CoreUI React Bootstrap 4 components", "license": "MIT", "author": { @@ -41,12 +41,12 @@ "prop-types": "^15.7.2", "react-onclickout": "^2.0.8", "react-perfect-scrollbar": "^1.5.2", - "react-router-dom": "~4.3.1", "reactstrap": "^7.1.0" }, "peerDependencies": { "@coreui/coreui": "^2.1.9", - "react": "^16.8.6" + "react": "^16.8.6", + "react-router-dom": "^5.0.0" }, "devDependencies": { "babel-eslint": "^10.0.1", @@ -58,6 +58,7 @@ "nwb": "^0.23.0", "react": "^16.8.6", "react-dom": "^16.8.6", + "react-router-dom": "^5.0.0", "sinon": "^5.1.1", "webpack-dev-server": "~3.3.1" }, diff --git a/src/Breadcrumb.md b/src/Breadcrumb.md index 9a3d39c3..98433a96 100644 --- a/src/Breadcrumb.md +++ b/src/Breadcrumb.md @@ -1,3 +1,14 @@ ### CoreUI `Breadcrumb` component +usage in `DefaultLayout`: +```jsx +import * as router from 'react-router-dom'; +import { AppBreadcrumb2 as AppBreadcrumb } from '@coreui/react'; +// routes config +import routes from '../../routes.js'; +``` + +```html + +``` _todo_ diff --git a/src/Breadcrumb2.js b/src/Breadcrumb2.js new file mode 100644 index 00000000..716f3b65 --- /dev/null +++ b/src/Breadcrumb2.js @@ -0,0 +1,108 @@ +import React, { Component } from 'react'; +import { Breadcrumb, BreadcrumbItem } from 'reactstrap'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +let routes; +let router; + +const getPaths = (pathname) => { + const paths = ['/']; + + if (pathname === '/') return paths; + + pathname.split('/').reduce((prev, curr) => { + const currPath = `${prev}/${curr}`; + paths.push(currPath); + return currPath; + }); + return paths; +}; + +const findRouteName2 = (url) => { + const matchPath = router.matchPath; + const aroute = routes.find(route => matchPath(url, {path: route.path, exact: route.exact})); + return (aroute && aroute.name) ? aroute.name : null +}; + +const BreadcrumbsItem2 = ({ match }) => { + const routeName = findRouteName2(match.url); + const Link = router.Link; + if (routeName) { + return ( + match.isExact ? + {routeName} + : + + + {routeName} + + + ); + } + return null; +}; + +BreadcrumbsItem2.propTypes = { + match: PropTypes.shape({ + url: PropTypes.string + }) +}; + +const Breadcrumbs2 = (args) => { + const Route = router.Route; + const paths = getPaths(args.location.pathname); + const items = paths.map((path, i) => ); + return ( + + {items} + + ); +}; + +const propTypes = { + children: PropTypes.node, + className: PropTypes.string, + appRoutes: PropTypes.any, + tag: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + router: PropTypes.any +}; + +const defaultProps = { + tag: 'div', + className: '', + appRoutes: [{ path: '/', exact: true, name: 'Home', component: null }] +}; + +class AppBreadcrumb2 extends Component { + constructor(props) { + super(props); + + this.state = { routes: props.appRoutes }; + routes = this.state.routes; + router = props.router; + } + + render() { + const { className, tag: Tag, ...attributes } = this.props; + + delete attributes.children + delete attributes.appRoutes + delete attributes.router + + const classes = classNames(className); + + const Route = router.Route; + + return ( + + + + ); + } +} + +AppBreadcrumb2.propTypes = propTypes; +AppBreadcrumb2.defaultProps = defaultProps; + +export default AppBreadcrumb2; diff --git a/src/SidebarNav.js b/src/SidebarNav.js index a8695cb3..549c01b2 100644 --- a/src/SidebarNav.js +++ b/src/SidebarNav.js @@ -185,7 +185,7 @@ class AppSidebarNav extends Component { // sidebar-nav root return ( - + diff --git a/src/SidebarNav.md b/src/SidebarNav.md index c2979239..dfc96e40 100644 --- a/src/SidebarNav.md +++ b/src/SidebarNav.md @@ -1,34 +1,47 @@ ### CoreUI `SidebarNav` subcomponent +usage in `DefaultLayout`: +```jsx +import * as router from 'react-router-dom'; +import { AppSidebarNav2 as AppSidebarNav } from '@coreui/react'; +// sidebar nav config +import navigation from '../../_nav'; +``` +```html + +``` +props: -prop | default ---- | --- -children | `this.navList(navConfig.items)` -className | `sidebar-nav` -navConfig | `{ items: [ { name url icon badge } ] }` -isOpen | `false` -tag | `nav` +prop | default | notes +--- | --- | --- +children | `this.navList(navConfig.items)` | +className | `sidebar-nav` | +navConfig | `{ items: [ { name url icon badge } ] }` | +isOpen | `false` | +tag | `nav` | +router | inject `react-router-dom@5` object | _mandatory for @coreui/react ^2.5.0_ -#### `navConfig` structure +--- +#### `navConfig` shape - title: -````js +```json5 { title: true, name: 'Theme', - class: '' // optional class names space delimited list for title item ex: "text-center" + class: '', // optional class names space delimited list for title item ex: "text-center" wrapper: { // optional wrapper object element: '', // optional* valid HTML5 element tag ( *required when passing attributes) attributes: {} // optional valid JS object with JS API naming ex: { className: "my-class", style: { fontFamily: "Verdana" }, id: "my-id"} }, }, -```` +``` - item: -````js +```json5 { name: 'Dashboard', url: '/dashboard', - icon: `icon-speedometer', + icon: 'icon-speedometer', class: '', // optional variant: 'success', // optional badge: { @@ -38,9 +51,9 @@ tag | `nav` }, attributes: { target: '_blank', rel: "noreferrer noopener", disabled: false, hidden: false }, // (v2.1.0 up) optional valid JS object with JS API naming }, -```` +``` - item with `children` array - works like `nav-dropdown-toggle` with `nav-dropdown-items` -````js +```json5 { name: 'Icons', url: '/icons', @@ -57,16 +70,16 @@ tag | `nav` }, ] } -```` +``` - divider: -````js +```json5 { divider: true }, -```` +``` - order of precedence: -```` +``` title > divider > children > item -```` +``` diff --git a/src/SidebarNav2.js b/src/SidebarNav2.js new file mode 100644 index 00000000..2ab4b7fa --- /dev/null +++ b/src/SidebarNav2.js @@ -0,0 +1,203 @@ +import React, { Component } from 'react'; +import { Badge, Nav, NavItem, NavLink as RsNavLink } from 'reactstrap'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import PerfectScrollbar from 'react-perfect-scrollbar'; +import 'react-perfect-scrollbar/dist/css/styles.css'; + +const propTypes = { + children: PropTypes.node, + className: PropTypes.string, + navConfig: PropTypes.any, + navFunc: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + isOpen: PropTypes.bool, + staticContext: PropTypes.any, + tag: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + router: PropTypes.any +}; + +const defaultProps = { + tag: 'nav', + navConfig: { + items: [ + { + name: 'Dashboard', + url: '/dashboard', + icon: 'icon-speedometer', + badge: { variant: 'info', text: 'NEW' } + }] + }, + isOpen: false, + router: {RsNavLink} +}; + +class AppSidebarNav2 extends Component { + constructor(props) { + super(props); + + this.handleClick = this.handleClick.bind(this); + this.activeRoute = this.activeRoute.bind(this); + this.hideMobile = this.hideMobile.bind(this); + } + + handleClick(e) { + e.preventDefault(); + e.currentTarget.parentElement.classList.toggle('open'); + } + + activeRoute(routeName, props) { + return props.location.pathname.indexOf(routeName) > -1 + ? 'nav-item nav-dropdown open' + : 'nav-item nav-dropdown'; + } + + hideMobile() { + if (document.body.classList.contains('sidebar-show')) { + document.body.classList.toggle('sidebar-show'); + } + } + + // nav list + navList(items) { + return items.map((item, index) => this.navType(item, index)); + } + + // nav type + navType(item, idx) { + return ( + item.title ? this.navTitle(item, idx) + : item.divider ? this.navDivider(item, idx) + : item.label ? this.navLabel(item, idx) + : item.children ? this.navDropdown(item, idx) + : this.navItem(item, idx) + ); + } + + // nav list section title + navTitle(title, key) { + const classes = classNames('nav-title', title.class); + return
  • {this.navWrapper(title)}
  • ; + } + + // simple wrapper for nav-title item + navWrapper(item) { + return item.wrapper && item.wrapper.element ? React.createElement(item.wrapper.element, item.wrapper.attributes, item.name) : item.name; + } + + // nav list divider + navDivider(divider, key) { + const classes = classNames('divider', divider.class); + return
  • ; + } + + // nav label with nav link + navLabel(item, key) { + const classes = { + item: classNames('hidden-cn', item.class), + link: classNames('nav-label', item.class ? item.class : ''), + icon: classNames( + 'nav-icon', + !item.icon ? 'fa fa-circle' : item.icon, + item.label.variant ? `text-${item.label.variant}` : '', + item.label.class ? item.label.class : '', + ) + }; + return ( + this.navLink(item, key, classes) + ); + } + + // nav dropdown + navDropdown(item, key) { + const classIcon = classNames('nav-icon', item.icon); + return ( +
  • + {item.name}{this.navBadge(item.badge)} +
      + {this.navList(item.children)} +
    +
  • ); + } + + // nav item with nav link + navItem(item, key) { + const classes = { + item: classNames(item.class), + link: classNames('nav-link', item.variant ? `nav-link-${item.variant}` : ''), + icon: classNames('nav-icon', item.icon) + }; + return ( + this.navLink(item, key, classes) + ); + } + + // nav link + navLink(item, key, classes) { + const url = item.url || ''; + const itemIcon = + const itemBadge = this.navBadge(item.badge) + const attributes = item.attributes || {} + const NavLink = this.props.router.NavLink || RsNavLink + return ( + + { attributes.disabled ? + + {itemIcon}{item.name}{itemBadge} + + : + this.isExternal(url) || NavLink === RsNavLink ? + + {itemIcon}{item.name}{itemBadge} + : + + {itemIcon}{item.name}{itemBadge} + + } + + ); + } + + // badge addon to NavItem + navBadge(badge) { + if (badge) { + const classes = classNames(badge.class); + return ( + {badge.text} + ); + } + return null; + } + + isExternal(url) { + const link = url ? url.substring(0, 4) : ''; + return link === 'http'; + } + + render() { + const { className, children, navConfig, ...attributes } = this.props; + + delete attributes.isOpen + delete attributes.staticContext + delete attributes.Tag + delete attributes.router + + const navClasses = classNames(className, 'sidebar-nav'); + + // ToDo: find better rtl fix + const isRtl = getComputedStyle(document.querySelector('html')).direction === 'rtl' + + // sidebar-nav root + return ( + + + + ); + } +} + +AppSidebarNav2.propTypes = propTypes; +AppSidebarNav2.defaultProps = defaultProps; + +export default AppSidebarNav2; diff --git a/src/index.js b/src/index.js index 93bad494..b32539fa 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ export AppAside from './Aside'; export AppAsideToggler from './AsideToggler'; export AppBreadcrumb from './Breadcrumb'; +export AppBreadcrumb2 from './Breadcrumb2'; export AppFooter from './Footer'; export AppHeader from './Header'; export AppHeaderDropdown from './HeaderDropdown'; @@ -11,5 +12,6 @@ export AppSidebarForm from './SidebarForm'; export AppSidebarHeader from './SidebarHeader'; export AppSidebarMinimizer from './SidebarMinimizer'; export AppSidebarNav from './SidebarNav'; +export AppSidebarNav2 from './SidebarNav2'; export AppSidebarToggler from './SidebarToggler'; export AppSwitch from './Switch';