UNPKG

react-url-query

Version:

A library for managing state through query parameters in the URL in React. Works well with or without Redux and React Router.

271 lines (228 loc) 9.25 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { parse as parseQueryString } from 'query-string'; import urlQueryDecoder from '../urlQueryDecoder'; import urlQueryConfig from '../urlQueryConfig'; import { updateUrlQuerySingle, updateUrlQueryMulti } from '../updateUrlQuery'; import { encode } from '../serialize'; import UrlUpdateTypes from '../UrlUpdateTypes'; /** * Higher order component (HOC) that injects URL query parameters as props. * * @param {Function} mapUrlToProps `function(url, props) -> {Object}` returns props to inject * @return {React.Component} */ export default function addUrlProps(options = {}) { const { mapUrlToProps = d => d, mapUrlChangeHandlersToProps, urlPropsQueryConfig, addRouterParams, addUrlChangeHandlers, } = options; let { changeHandlerName } = options; return function addPropsWrapper(WrappedComponent) { // caching to prevent unnecessary generation of new onChange functions let cachedHandlers; let decodeQuery; // initialize decode query (with cache) if a config is provided if (urlPropsQueryConfig) { decodeQuery = urlQueryDecoder(urlPropsQueryConfig); } /** * Parse the URL query into an object. If a urlPropsQueryConfig is provided * the values are decoded based on type. */ function getUrlObject(props) { let location; // check in history if (urlQueryConfig.history.location) { location = urlQueryConfig.history.location; // react-router provides it as a prop } else if ( props.location && (props.location.query || props.location.search != null) ) { location = props.location; // not found. just use location from window } else { location = window.location; } const currentQuery = location.query || parseQueryString(location.search) || {}; let result; // if a url query decoder is provided, decode the query then return that. if (decodeQuery) { result = decodeQuery(currentQuery); } else { result = currentQuery; } // add in react-router params if requested if ( addRouterParams || (addRouterParams !== false && urlQueryConfig.addRouterParams) ) { Object.assign(result, props.params, props.match && props.match.params); } return result; } const displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component'; class AddUrlProps extends Component { static displayName = `AddUrlProps(${displayName})`; static WrappedComponent = WrappedComponent; static propTypes = { location: PropTypes.any, // eslint-disable-line react/forbid-prop-types }; /** * Create URL change handlers based on props, the urlPropsQueryConfig (if provided), * and mapUrlChangeHandlersToProps (if provided). * As a member function so we can read `this.props` in generated handlers dynamically. */ getUrlChangeHandlerProps(propsWithUrl) { let handlers; if (urlPropsQueryConfig) { // if we have a props->query config, generate the change handler props unless // addUrlChangeHandlers is false if ( addUrlChangeHandlers || (addUrlChangeHandlers == null && urlQueryConfig.addUrlChangeHandlers) ) { // use cache if available. Have to do this since urlQueryConfig can change between // renders (although that is unusual). if (cachedHandlers) { handlers = cachedHandlers; } else { // read in function from options for how to generate a name from a prop if (!changeHandlerName) { changeHandlerName = urlQueryConfig.changeHandlerName; } // for each URL config prop, create a handler handlers = Object.keys(urlPropsQueryConfig).reduce( (handlersAccum, propName) => { const { updateType, queryParam = propName, type, } = urlPropsQueryConfig[propName]; // name handler for `foo` => `onChangeFoo` const handlerName = changeHandlerName(propName); // handler encodes the value and updates the URL with the encoded value // based on the `updateType` in the config. Default is `replaceIn` handlersAccum[ handlerName ] = function generatedUrlChangeHandler(value) { let { location } = urlQueryConfig.history; // for backwards compatibility if (!location) { location = this.props.location; } const encodedValue = encode(type, value); // add a simple check when we have props.location.query to see if // we even need to update. if ( location && location.query && location.query[queryParam] === encodedValue ) { return undefined; // skip updating } return updateUrlQuerySingle( updateType, queryParam, encodedValue, location ); }.bind(this); // bind this so we can access props dynamically return handlersAccum; }, {} ); // add in a batch change handler const batchHandlerName = changeHandlerName('urlQueryParams'); handlers[ batchHandlerName ] = function generatedBatchUrlChangeHandler( queryValues, updateType = UrlUpdateTypes.replaceIn ) { let { location } = urlQueryConfig.history; // for backwards compatibility if (!location) { location = this.props.location; } let allEncodedValuesUnchanged = true; // encode each value const queryReplacements = Object.keys(queryValues).reduce( (accum, propName) => { const { queryParam = propName, type } = urlPropsQueryConfig[ propName ]; const value = queryValues[propName]; const encodedValue = encode(type, value); accum[queryParam] = encodedValue; // add a simple check when we have props.location.query to see if // we even need to update. if ( location && location.query && location.query[queryParam] !== encodedValue ) { allEncodedValuesUnchanged = false; } return accum; }, {} ); if (location && location.query && allEncodedValuesUnchanged) { return undefined; // skip updating if no encoded values changed } return updateUrlQueryMulti( updateType, queryReplacements, location ); }.bind(this); // cache these so we don't regenerate new functions every render cachedHandlers = handlers; } } } // if a manual mapping function is provided, use it, passing in the auto-generated // handlers as an optional secondary argument. if (mapUrlChangeHandlersToProps) { handlers = mapUrlChangeHandlersToProps.call( this, propsWithUrl, handlers ); } return handlers; } render() { // get the url query parameters as an object mapping name to value. // if a config is provided, these are decoded based on their `type` and their // name will match the prop name. // if no config is provided, they are not decoded and their names are whatever // they were in the URL. const url = getUrlObject(this.props); // pass to mapUrlToProps for further decoding if provided this.propsWithUrl = Object.assign( {}, this.props, mapUrlToProps(url, this.props) ); // add in the URL change handlers - either auto-generated based on config // or from mapUrlChangeHandlersToProps. Object.assign( this.propsWithUrl, this.getUrlChangeHandlerProps(this.propsWithUrl) ); // render the wrapped component with the URL props added in. return <WrappedComponent {...this.propsWithUrl} />; } } return AddUrlProps; }; }