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(); } 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 ; } }; }); 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)); };