From b61bf31199b2b3b2ac0ad69995dee3d07f5d939f Mon Sep 17 00:00:00 2001 From: kaesonho Date: Thu, 23 Jul 2015 09:32:09 -0700 Subject: [PATCH] navlink performance improvement, remove handleRoute and provide createNavLinkComponent for users to customize it --- docs/api/createNavLinkComponent.md | 25 +++++ index.js | 1 + lib/NavLink.js | 129 +-------------------- lib/createNavLinkComponent.js | 174 +++++++++++++++++++++++++++++ tests/unit/lib/NavLink-test.js | 4 +- 5 files changed, 204 insertions(+), 129 deletions(-) create mode 100644 docs/api/createNavLinkComponent.md create mode 100644 lib/createNavLinkComponent.js diff --git a/docs/api/createNavLinkComponent.md b/docs/api/createNavLinkComponent.md new file mode 100644 index 0000000..a0cd907 --- /dev/null +++ b/docs/api/createNavLinkComponent.md @@ -0,0 +1,25 @@ +# createNavLinkComponent + +`createNavLinkComponent` is a function for you to create a NavLink component, you are able to pass options to overwrite attributes to generate the NavLink. e.g., custom mixin or click handler. + +## Parameters + +| Param Name | Param Type | Description | +|-----------|-----------|-------------| +| overwriteSpec | Object | the spec object taken to overwrite the default spec when we create NavLink using React.createClass | + + +## Example Usage + +```js +var createNavLinkComponent = require('fluxible-router').createNavLinkComponent; + +module.exports = createNavLinkComponent({ + displayName: 'CustomNavLink', + mixins: [someMixin], + clickHandler: function (e) { + // custom click handler + this.dispatchNavAction(e); + } +}); +``` diff --git a/index.js b/index.js index 5b4b0f7..3159d5b 100644 --- a/index.js +++ b/index.js @@ -5,6 +5,7 @@ 'use strict'; module.exports = { + createNavLinkComponent: require('./lib/createNavLinkComponent'), handleHistory: require('./lib/handleHistory'), handleRoute: require('./lib/handleRoute'), History: require('./lib/History'), diff --git a/lib/NavLink.js b/lib/NavLink.js index 5dac755..e0f28d0 100644 --- a/lib/NavLink.js +++ b/lib/NavLink.js @@ -5,131 +5,6 @@ /*global window */ 'use strict'; -var React = require('react'); -var navigateAction = require('./navigateAction'); -var debug = require('debug')('NavLink'); -var objectAssign = require('object-assign'); -var handleRoute = require('./handleRoute'); -var Immutable = require('immutable'); +var createNavLinkComponent = require('./createNavLinkComponent'); -function isLeftClickEvent (e) { - return e.button === 0; -} - -function isModifiedEvent (e) { - return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); -} - -var NavLink = React.createClass({ - displayName: 'NavLink', - contextTypes: { - executeAction: React.PropTypes.func - }, - propTypes: { - currentRoute: React.PropTypes.object, - currentNavigate: React.PropTypes.object, - href: React.PropTypes.string, - isActive: React.PropTypes.func, - makePath: React.PropTypes.func, - stopPropagation: React.PropTypes.bool, - routeName: React.PropTypes.string, - navParams: React.PropTypes.object, - followLink: React.PropTypes.bool, - preserveScrollPosition: React.PropTypes.bool, - replaceState: React.PropTypes.bool - }, - _getHrefFromProps: function (props) { - var href = props.href; - var routeName = props.routeName; - if (!href && routeName) { - href = this.props.makePath(routeName, props.navParams); - } - if (!href) { - throw new Error('NavLink created without href or unresolvable routeName \'' + routeName + '\''); - } - return href; - }, - dispatchNavAction: function (e) { - var navType = this.props.replaceState ? 'replacestate' : 'click'; - debug('dispatchNavAction: action=NAVIGATE', this.props.href, this.props.followLink, this.props.navParams); - - if (this.props.followLink) { - return; - } - - if (isModifiedEvent(e) || !isLeftClickEvent(e)) { - // this is a click with a modifier or not a left-click - // let browser handle it natively - return; - } - - var href = this._getHrefFromProps(this.props); - - if (href[0] === '#') { - // this is a hash link url for page's internal links. - // Do not trigger navigate action. Let browser handle it natively. - return; - } - - if (href[0] !== '/') { - // this is not a relative url. check for external urls. - var location = window.location; - var origin = location.origin || (location.protocol + '//' + location.host); - - if (href.indexOf(origin) !== 0) { - // this is an external url, do not trigger navigate action. - // let browser handle it natively. - return; - } - - href = href.substring(origin.length) || '/'; - } - - e.preventDefault(); - if (this.props.stopPropagation) { - e.stopPropagation(); - } - - var context = this.props.context || this.context; - var onBeforeUnloadText = typeof window.onbeforeunload === 'function' ? window.onbeforeunload() : ''; - var confirmResult = onBeforeUnloadText ? window.confirm(onBeforeUnloadText) : true; - - if (confirmResult) { - // Removes the window.onbeforeunload method so that the next page will not be affected - window.onbeforeunload = null; - - context.executeAction(navigateAction, { - type: navType, - url: href, - preserveScrollPosition: this.props.preserveScrollPosition, - params: this.props.navParams - }); - } - }, - render: function() { - var href = this._getHrefFromProps(this.props); - var isActive = this.props.isActive(href); - - var className = this.props.className; - var style = this.props.style; - if (isActive) { - className = className ? (className + ' ') : ''; - className += this.props.activeClass || 'active'; - style = objectAssign({}, style, this.props.activeStyle); - } - - return React.createElement( - 'a', - objectAssign({}, { - onClick: this.dispatchNavAction - }, this.props, { - href: href, - className: className, - style: style - }), - this.props.children - ); - } -}); - -module.exports = handleRoute(NavLink); +module.exports = createNavLinkComponent(); diff --git a/lib/createNavLinkComponent.js b/lib/createNavLinkComponent.js new file mode 100644 index 0000000..01ae341 --- /dev/null +++ b/lib/createNavLinkComponent.js @@ -0,0 +1,174 @@ +/** + * Copyright 2015, Yahoo! Inc. + * Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. + */ +/*global window */ +'use strict'; +var React = require('react'); +var RouteStore = require('./RouteStore'); +var debug = require('debug')('NavLink'); +var navigateAction = require('./navigateAction'); +var objectAssign = require('object-assign'); + +function isLeftClickEvent (e) { + return e.button === 0; +} + +function isModifiedEvent (e) { + return !!(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); +} + +/** + * create NavLink component with custom options + * @param {Object} overwriteSpec spec to overwrite the default spec to create NavLink + * @returns {React.Component} NavLink component + */ +module.exports = function createNavLinkComponent (overwriteSpec) { + var NavLink = React.createClass(objectAssign({}, { + displayName: 'NavLink', + contextTypes: { + executeAction: React.PropTypes.func, + getStore: React.PropTypes.func + }, + propTypes: { + href: React.PropTypes.string, + stopPropagation: React.PropTypes.bool, + routeName: React.PropTypes.string, + navParams: React.PropTypes.object, + followLink: React.PropTypes.bool, + preserveScrollPosition: React.PropTypes.bool, + replaceState: React.PropTypes.bool + }, + getInitialState: function () { + return this._getState(this.props); + }, + componentDidMount: function () { + var routeStore = this.context.getStore(RouteStore); + routeStore.addChangeListener(this._onRouteStoreChange); + }, + componentWillUnmount: function () { + var routeStore = this.context.getStore(RouteStore); + routeStore.removeChangeListener(this._onRouteStoreChange); + }, + shouldComponentUpdate: function (nextProps, nextState) { + if (this.state.isActive !== nextState.isActive || this.receivedNewProps) { + return true; + } + return false; + }, + componentWillReceiveProps: function (nextProps) { + this.receivedNewProps = true; + this.setState(this._getState(nextProps)); + }, + _onRouteStoreChange: function () { + if (this.isMounted()) { + this.setState(this._getState(this.props)); + } + }, + _getState: function (props) { + var routeStore = this.context.getStore(RouteStore); + var href = this._getHrefFromProps(props); + var className = props.className; + var style = props.style; + var isActive = routeStore.isActive(href); + if (isActive) { + className = className ? (className + ' ') : ''; + className += props.activeClass || 'active'; + style = objectAssign({}, style, props.activeStyle); + } + return { + href: href, + isActive: isActive, + className: className, + style: style + }; + }, + _getHrefFromProps: function (props) { + var href = props.href; + var routeName = props.routeName; + var routeStore = this.context.getStore(RouteStore); + if (!href && routeName) { + href = routeStore.makePath(routeName, props.navParams); + } + if (!href) { + throw new Error('NavLink created without href or unresolvable routeName \'' + routeName + '\''); + } + return href; + }, + dispatchNavAction: function (e) { + var navType = this.props.replaceState ? 'replacestate' : 'click'; + debug('dispatchNavAction: action=NAVIGATE', this.props.href, this.props.followLink, this.props.navParams); + + if (this.props.followLink) { + return; + } + + if (isModifiedEvent(e) || !isLeftClickEvent(e)) { + // this is a click with a modifier or not a left-click + // let browser handle it natively + return; + } + + var href = this._getHrefFromProps(this.props); + + if (href[0] === '#') { + // this is a hash link url for page's internal links. + // Do not trigger navigate action. Let browser handle it natively. + return; + } + + if (href[0] !== '/') { + // this is not a relative url. check for external urls. + var location = window.location; + var origin = location.origin || (location.protocol + '//' + location.host); + + if (href.indexOf(origin) !== 0) { + // this is an external url, do not trigger navigate action. + // let browser handle it natively. + return; + } + + href = href.substring(origin.length) || '/'; + } + + e.preventDefault(); + if (this.props.stopPropagation) { + e.stopPropagation(); + } + + var context = this.props.context || this.context; + var onBeforeUnloadText = typeof window.onbeforeunload === 'function' ? window.onbeforeunload() : ''; + var confirmResult = onBeforeUnloadText ? window.confirm(onBeforeUnloadText) : true; + + if (confirmResult) { + // Removes the window.onbeforeunload method so that the next page will not be affected + window.onbeforeunload = null; + + context.executeAction(navigateAction, { + type: navType, + url: href, + preserveScrollPosition: this.props.preserveScrollPosition, + params: this.props.navParams + }); + } + }, + clickHandler: function (e) { + this.dispatchNavAction(e); + }, + render: function () { + this.receivedNewProps = false; + return React.createElement( + 'a', + objectAssign({}, { + onClick: this.clickHandler + }, this.props, { + href: this.state.href, + className: this.state.className, + style: this.state.style + }), + this.props.children + ); + } + }, overwriteSpec)); + return NavLink; +}; diff --git a/tests/unit/lib/NavLink-test.js b/tests/unit/lib/NavLink-test.js index 4f6113d..9c5bce6 100644 --- a/tests/unit/lib/NavLink-test.js +++ b/tests/unit/lib/NavLink-test.js @@ -392,14 +392,14 @@ describe('NavLink', function () { setTimeout(function () { expect(link.getDOMNode().getAttribute('href')).to.equal('/foo'); expect(link.getDOMNode().textContent).to.equal('bar'); - expect(!link.getDOMNode().getAttribute('class')); + expect(!link.getDOMNode().getAttribute('class')).to.equal(true); done(); }, 50); }); }); describe('componentWillUnmount', function () { - it('should update active state', function () { + it('should remove the change listener', function () { var div = document.createElement('div'); React.render(