1
0
mirror of https://github.com/ascribe/onion.git synced 2024-12-23 18:13:49 +01:00
onion/js/components/context/with_context.js

139 lines
5.0 KiB
JavaScript

import React from 'react';
import { currentUserShape, locationShape, routerShape, whitelabelShape } from '../prop_types';
import { selectFromObject } from '../../utils/general';
import { getDisplayName } from '../../utils/react';
/**
* ContextPropDefinitions
* ======================
* contextType definitions for `contextProp`s.
*
* Each definition is of the form:
*
* contextProp = {
* 'contextTypes': {
* 'contextType': contextType used by the contextProp,
* ...
* },
* transformToProps: (optional) function that will receive the contextTypes from the component's
* context and should return an object containing the props to inject.
* }
*
* In the common case where your `contextProp` maps directly to the `contextType` used
* (eg. using `currentUser` as a `contextProp` to get the `currentUser` from context), you can omit
* the object definition and simply assign the `contextType` to the `contextProp`:
*
* contextProp = contextType
*
* as opposed to:
*
* contextProp = {
* 'contextTypes': {
* contextProp: contextType
* }
* }
*/
const ContextPropDefinitions = {
currentUser: currentUserShape.isRequired,
isLoggedIn: {
contextTypes: {
currentUser: currentUserShape.isRequired
},
transformToProps: ({ currentUser }) => ({ isLoggedIn: !!currentUser.email })
},
location: locationShape.isRequired,
router: routerShape.isRequired,
whitelabel: whitelabelShape.isRequired
};
// Expand any shortform definitions in ContextPropDefinitions into a normalized form that's
// easier for withContext to work with
Object.entries(ContextPropDefinitions).forEach(([prop, def]) => {
if (!def.hasOwnProperty('contextTypes')) {
ContextPropDefinitions[prop] = {
contextTypes: {
[prop]: def
}
};
}
});
/**
* Generalized version of react-router's `withRouter`.
* This injects the given `contextProp`s from the WrappedComponent's context into the component as
* a prop.
*
* Given `contextProp`s must have a matching definition in `ContextPropDefinitions`.
*
* @param {Component} WrappedComponent Component to inject context into
* @param {...string} contextProps Argument list of `contextProp`s (that must be registered in
* `ContextPropDefinitions`) to be injected into component as
* props
* @return {Component} Wrapped component
*/
export default function withContext(WrappedComponent, ...contextProps) {
const wrappedComponentName = getDisplayName(WrappedComponent);
if (!contextProps.length) {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(`Used withContext on ${wrappedComponentName} without supplying any ` +
"items to inject from the component's context. Ignoring...");
}
return WrappedComponent;
}
const contextTypes = contextProps.reduce((types, contextProp) => {
const contextDef = ContextPropDefinitions[contextProp];
if (contextDef) {
Object.assign(types, contextDef.contextTypes);
} else if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.warn(`No context types were found for '${contextProp}' when adding context ` +
`to ${wrappedComponentName}. Make sure to add the context information ` +
'with_context.js.');
}
return types;
}, {});
const WithContext = (props, context) => {
if (process.env.NODE_ENV !== 'production') {
// Check if the expected context was available
Object.keys(contextTypes).forEach((contextType) => {
if (!context[contextType]) {
// eslint-disable-next-line no-console
console.warn(`Expected '${contextType}' did not exist in ` +
`${wrappedComponentName}'s context during mounting`);
}
});
}
const injectedProps = Object.assign({}, ...contextProps.map((contextProp) => {
const contextDef = ContextPropDefinitions[contextProp];
if (contextDef) {
const contextForProp = selectFromObject(context, Object.keys(contextDef.contextTypes));
return typeof contextDef.transformToProps === 'function'
? contextDef.transformToProps(contextForProp)
: contextForProp;
} else {
// Will be ignored by Object.assign()
return undefined;
}
}));
return (
<WrappedComponent {...props} {...injectedProps} />
);
};
WithContext.displayName = `WithContext(${wrappedComponentName}): [${contextProps.join(', ')}]`;
WithContext.contextTypes = contextTypes;
return WithContext;
}