Files
talk/client/coral-framework/hocs/withSlotElements.js
T
2018-03-20 19:00:19 +01:00

203 lines
6.5 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import isEqual from 'lodash/isEqual';
import { getShallowChanges } from 'coral-framework/utils';
import { compose } from 'recompose';
import hoistStatics from 'recompose/hoistStatics';
import isFunction from 'lodash/isFunction';
function resolvePrimitiveOrFunction(primitiveOrFunction, props) {
if (isFunction(primitiveOrFunction)) {
return primitiveOrFunction(props);
}
return primitiveOrFunction;
}
const createHOC = ({
slot,
defaultComponent = null,
passthroughPropName = 'passthrough',
size = null,
propName = 'slotElements',
}) =>
hoistStatics(WrappedComponent => {
return class withSlotElements extends React.Component {
static contextTypes = {
plugins: PropTypes.object,
};
static propTypes = {
reduxState: PropTypes.object,
};
getSlots(props = this.props) {
const tmp = resolvePrimitiveOrFunction(slot, props);
if (Array.isArray(tmp)) {
return tmp;
}
return [tmp];
}
getDefaultComponents(props = this.props, fill = 1) {
const tmp = resolvePrimitiveOrFunction(defaultComponent, props);
if (Array.isArray(tmp)) {
return tmp;
}
return new Array(fill).fill(tmp);
}
getSizes(props = this.props, fill = 1) {
const tmp = resolvePrimitiveOrFunction(size, props);
if (Array.isArray(tmp)) {
return tmp;
}
return new Array(fill).fill(tmp);
}
getPassthrough(props = this.props) {
return passthroughPropName ? props[passthroughPropName] : null;
}
shouldComponentUpdate(next) {
// Prevent Slot from rerendering when only reduxState has changed and
// it does not result in a change of slot children.
const changes = getShallowChanges(this.props, next);
// Handle special `passthrough` props.
if (passthroughPropName) {
const passthroughIndex = changes.indexOf(passthroughPropName);
if (passthroughIndex !== -1) {
if (!this.props[passthroughPropName] || next[passthroughPropName]) {
return true;
}
if (
getShallowChanges(
this.props[passthroughPropName],
next[passthroughPropName]
).lenght === 0
) {
changes.splice(passthroughIndex, 1);
}
}
}
if (changes.length === 1 && changes[0] === 'reduxState') {
// If config changed, we'll have to rerender everything.
// Should only happen during development as this is
// usually static.
if (this.props.reduxState.config !== next.reduxState.config) {
return true;
}
const prevChildrenKeys = this.getSlotElements(this.props).map(
child => child.key
);
const nextChildrenKeys = this.getSlotElements(next).map(
child => child.key
);
return !isEqual(prevChildrenKeys, nextChildrenKeys);
}
// Prevent Slot from rerendering when no props has shallowly changed.
return changes.length !== 0;
}
/**
* Returns slot elements for configured slots. If only one slot is given
* slot elements are returned directly. If more than one slot is specified
* returns an array of slot elements.
*/
getSlotElements(props = this.props) {
const { plugins } = this.context;
const slots = this.getSlots(props);
const sizes = this.getSizes(props, slots.length);
const defaultComponents = this.getDefaultComponents(
props,
slots.length
);
const slotPassthrough = this.getPassthrough(props);
if (process.env.NODE_ENV !== 'production') {
if (slotPassthrough.data && slotPassthrough.data.refetch) {
/* eslint-disable no-console */
console.warn(
'Slots no longer need `data` property.',
'Plugins can use the new HOCs `withRefetch`,',
'`withFetchMore`, `withSubscribeToMore` and `withVariables` instead.',
'Affected slots: ',
slots
);
/* eslint-enable no-console */
}
}
const elements = [];
slots.forEach((s, i) => {
const DefaultComponent = defaultComponents[i];
const size = sizes[i];
const slotElements = plugins.getSlotElements(
s,
props.reduxState,
slotPassthrough,
{ size }
);
if (slotElements.length === 0 && DefaultComponent) {
const p = plugins.getSlotComponentProps(
DefaultComponent,
props.reduxState,
slotPassthrough
);
slotElements.push(<DefaultComponent key="default" {...p} />);
}
elements.push(slotElements);
});
if (elements.length === 1) {
return elements[0];
}
return elements;
}
render() {
const { reduxState: _a, ...rest } = this.props;
const slotElements = this.getSlotElements();
const props = {
[propName]: slotElements,
...rest,
};
return <WrappedComponent {...props} />;
}
};
});
const mapStateToProps = state => ({
reduxState: state,
});
/**
* Exports a HOC that provides a property `slotElements`.
* @param {Object} [options] configuration
* @param {string|array} [options.slot] Slot name or Array of Slot Names
* @param {element|array} [options.defaultComponent] Default Components or Array of such
* @param {number|array} [options.size] Slot size or an Array of slot size
* @param {string} [options.passthroughPropName] The property to find the passthrough prop
* @param {string} [options.propName] New property name, defaults to `slotElements`
*
* @return {func} Returns a HOC that per default provides the property `slotElements` with an
* Array of Slot Elements or in case of multiple slots, an Array of Slot Element
* Arrays.
*
* Example:
* withSlotElements({
* slot: 'awesomeSlot',
* size: 1,
* })(MyComponent);
*/
export default settings => {
return compose(connect(mapStateToProps, null), createHOC(settings));
};