diff --git a/client/coral-embed-stream/src/containers/ExtendableTabPanel.js b/client/coral-embed-stream/src/containers/ExtendableTabPanel.js
index 66659d657..678981a8e 100644
--- a/client/coral-embed-stream/src/containers/ExtendableTabPanel.js
+++ b/client/coral-embed-stream/src/containers/ExtendableTabPanel.js
@@ -1,17 +1,12 @@
import React from 'react';
import ExtendableTabPanel from '../components/ExtendableTabPanel';
-import { connect } from 'react-redux';
import { TabPane } from 'coral-ui';
import ExtendableTab from '../components/ExtendableTab';
-import { getShallowChanges } from 'coral-framework/utils';
-import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
+import { withSlotElements } from 'coral-framework/hocs';
+import { compose } from 'recompose';
class ExtendableTabPanelContainer extends React.Component {
- static contextTypes = {
- plugins: PropTypes.object,
- };
-
componentDidMount() {
this.handleFallback();
}
@@ -20,24 +15,6 @@ class ExtendableTabPanelContainer extends React.Component {
this.handleFallback(next);
}
- 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);
- if (changes.length === 1 && changes[0] === 'reduxState') {
- const prevKeys = this.getSlotElements(this.props.tabSlot, this.props).map(
- el => el.key
- );
- const nextKeys = this.getSlotElements(next.tabSlot, next).map(
- el => el.key
- );
- return !isEqual(prevKeys, nextKeys);
- }
-
- // Prevent Slot from rerendering when no props has shallowly changed.
- return changes.length !== 0;
- }
-
handleFallback(props = this.props) {
if (this.getTabNames(props).indexOf(props.activeTab) === -1) {
props.setActiveTab(props.fallbackTab);
@@ -48,37 +25,26 @@ class ExtendableTabPanelContainer extends React.Component {
return this.getTabElements(props).map(el => el.props.tabId);
}
- getSlotElements(slot, props = this.props) {
- const { plugins } = this.context;
- return plugins.getSlotElements(
- slot,
- props.reduxState,
- props.slotPassthrough
- );
- }
-
getPluginTabElements(props = this.props) {
- return this.getSlotTabElements(props.tabSlot);
+ return props.slotElements[0].map(this.createPluginTabFactory(props));
}
getPluginTabElementsPrepend(props = this.props) {
- return this.getSlotTabElements(props.tabSlotPrepend);
+ return props.slotElements[1].map(this.createPluginTabFactory(props));
}
- getSlotTabElements(slot) {
- return this.getSlotElements(slot).map(el => {
- return (
-
- {React.cloneElement(el, {
- active: this.props.activeTab === el.type.talkPluginName,
- })}
-
- );
- });
- }
+ createPluginTabFactory = (props = this.props) => el => {
+ return (
+
+ {React.cloneElement(el, {
+ active: props.activeTab === el.type.talkPluginName,
+ })}
+
+ );
+ };
getTabElements(props = this.props) {
const elements = [...this.getPluginTabElementsPrepend(props)];
@@ -91,14 +57,16 @@ class ExtendableTabPanelContainer extends React.Component {
return elements;
}
+ createPluginTabPane(el) {
+ return (
+
+ {el}
+
+ );
+ }
+
getPluginTabPaneElements(props = this.props) {
- return this.getSlotElements(props.tabPaneSlot).map(el => {
- return (
-
- {el}
-
- );
- });
+ return props.slotElements[2].map(this.createPluginTabPane);
}
render() {
@@ -137,8 +105,9 @@ ExtendableTabPanelContainer.propTypes = {
loading: PropTypes.bool,
};
-const mapStateToProps = state => ({
- reduxState: state,
-});
-
-export default connect(mapStateToProps, null)(ExtendableTabPanelContainer);
+export default compose(
+ withSlotElements({
+ slot: props => [props.tabSlot, props.tabSlotPrepend, props.tabPaneSlot],
+ passthroughPropName: 'slotPassthrough',
+ })
+)(ExtendableTabPanelContainer);
diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js
index fa8f2524d..599b21401 100644
--- a/client/coral-framework/components/IfSlotIsEmpty.js
+++ b/client/coral-framework/components/IfSlotIsEmpty.js
@@ -1,93 +1,14 @@
import React, { Children } from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import { getShallowChanges } from 'coral-framework/utils';
-import omit from 'lodash/omit';
+import { withSlotElements, withCombatPassthrough } from '../hocs';
+import { compose } from 'recompose';
class IfSlotIsEmpty extends React.Component {
- static contextTypes = {
- plugins: PropTypes.object,
- };
-
- shouldComponentUpdate(next) {
- const changes = getShallowChanges(this.props, next);
-
- // Handle special `passthrough` props.
- const passthroughIndex = changes.indexOf('passthrough');
- if (passthroughIndex !== -1) {
- if (!this.props.passthrough || next.passthrough) {
- return true;
- }
- if (
- getShallowChanges(this.props.passthrough, next.passthrough).lenght === 0
- ) {
- changes.splice(passthroughIndex, 1);
- }
- }
-
- // Prevent Slot from rerendering when only reduxState has changed and
- // it does not result in a change.
- if (changes.length === 1 && changes[0] === 'reduxState') {
- return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next);
- }
-
- // Prevent Slot from rerendering when no props has shallowly changed.
- return changes.length !== 0;
- }
-
- getPassthrough(props = this.props) {
- const slotProps = omit(props, [
- 'slot',
- 'children',
- 'reduxState',
- 'passthrough',
- 'dispatch',
- ]);
-
- // @Deprecated
- if (process.env.NODE_ENV !== 'production') {
- if (Object.keys(slotProps).length) {
- /* eslint-disable no-console */
- console.warn(
- `IfSlotIsEmpty '${
- props.fill
- }' passing through unknown props is deprecated, please use 'passthrough' instead`,
- slotProps
- );
- /* eslint-enable no-console */
- }
- }
-
- if (props.passthrough) {
- return props.passthrough;
- }
-
- if (props.queryData) {
- if (process.env.NODE_ENV !== 'production') {
- /* eslint-disable no-console */
- console.warn(
- `Slot '${
- props.fill
- }' property 'queryData' is deprecated, please use 'passthrough' instead`
- );
- /* eslint-enable no-console */
- }
- return {
- ...props.queryData,
- ...slotProps,
- };
- }
-
- return slotProps;
- }
-
isSlotEmpty(props = this.props) {
- const { slot, reduxState } = props;
- const passthrough = this.getPassthrough(props);
- const slots = Array.isArray(slot) ? slot : [slot];
- return slots.every(slot =>
- this.context.plugins.isSlotEmpty(slot, reduxState, passthrough)
- );
+ const { slotElements } = props;
+ return slotElements.length === 0
+ ? false
+ : slotElements.every(elements => elements.length === 0);
}
render() {
@@ -99,11 +20,14 @@ class IfSlotIsEmpty extends React.Component {
IfSlotIsEmpty.propTypes = {
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
children: PropTypes.node.isRequired,
- passthrough: PropTypes.object,
+ passthrough: PropTypes.object.isRequired,
};
-const mapStateToProps = state => ({
- reduxState: state,
-});
+const omitProps = ['slot', 'children'];
-export default connect(mapStateToProps, null)(IfSlotIsEmpty);
+export default compose(
+ withCombatPassthrough(omitProps),
+ withSlotElements({
+ slot: props => props.slot,
+ })
+)(IfSlotIsEmpty);
diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js
index 024877fbc..be67d4e4f 100644
--- a/client/coral-framework/components/IfSlotIsNotEmpty.js
+++ b/client/coral-framework/components/IfSlotIsNotEmpty.js
@@ -1,93 +1,14 @@
import React, { Children } from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
-import { getShallowChanges } from 'coral-framework/utils';
-import omit from 'lodash/omit';
+import { withSlotElements, withCombatPassthrough } from '../hocs';
+import { compose } from 'recompose';
class IfSlotIsNotEmpty extends React.Component {
- static contextTypes = {
- plugins: PropTypes.object,
- };
-
- shouldComponentUpdate(next) {
- const changes = getShallowChanges(this.props, next);
-
- // Handle special `passthrough` props.
- const passthroughIndex = changes.indexOf('passthrough');
- if (passthroughIndex !== -1) {
- if (!this.props.passthrough || next.passthrough) {
- return true;
- }
- if (
- getShallowChanges(this.props.passthrough, next.passthrough).lenght === 0
- ) {
- changes.splice(passthroughIndex, 1);
- }
- }
-
- // Prevent Slot from rerendering when only reduxState has changed and
- // it does not result in a change.
- if (changes.length === 1 && changes[0] === 'reduxState') {
- return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next);
- }
-
- // Prevent Slot from rerendering when no props has shallowly changed.
- return changes.length !== 0;
- }
-
- getPassthrough(props = this.props) {
- const slotProps = omit(props, [
- 'slot',
- 'children',
- 'reduxState',
- 'passthrough',
- 'dispatch',
- ]);
-
- // @Deprecated
- if (process.env.NODE_ENV !== 'production') {
- if (Object.keys(slotProps).length) {
- /* eslint-disable no-console */
- console.warn(
- `IfSlotIsEmpty '${
- props.fill
- }' passing through unknown props is deprecated, please use 'passthrough' instead`,
- slotProps
- );
- /* eslint-enable no-console */
- }
- }
-
- if (props.passthrough) {
- return props.passthrough;
- }
-
- if (props.queryData) {
- if (process.env.NODE_ENV !== 'production') {
- /* eslint-disable no-console */
- console.warn(
- `Slot '${
- props.fill
- }' property 'queryData' is deprecated, please use 'passthrough' instead`
- );
- /* eslint-enable no-console */
- }
- return {
- ...props.queryData,
- ...slotProps,
- };
- }
-
- return slotProps;
- }
-
isSlotEmpty(props = this.props) {
- const { slot, reduxState } = props;
- const passthrough = this.getPassthrough(props);
- const slots = Array.isArray(slot) ? slot : [slot];
- return slots.every(slot =>
- this.context.plugins.isSlotEmpty(slot, reduxState, passthrough)
- );
+ const { slotElements } = props;
+ return slotElements.length === 0
+ ? false
+ : slotElements.every(elements => elements.length === 0);
}
render() {
@@ -99,11 +20,14 @@ class IfSlotIsNotEmpty extends React.Component {
IfSlotIsNotEmpty.propTypes = {
slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
children: PropTypes.node.isRequired,
- passthrough: PropTypes.object,
+ passthrough: PropTypes.object.isRequired,
};
-const mapStateToProps = state => ({
- reduxState: state,
-});
+const omitProps = ['slot', 'children'];
-export default connect(mapStateToProps, null)(IfSlotIsNotEmpty);
+export default compose(
+ withCombatPassthrough(omitProps),
+ withSlotElements({
+ slot: props => props.slot,
+ })
+)(IfSlotIsNotEmpty);
diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js
index 02f182dc3..a70b9ccf9 100644
--- a/client/coral-framework/components/Slot.js
+++ b/client/coral-framework/components/Slot.js
@@ -4,135 +4,21 @@ import styles from './Slot.css';
import { connect } from 'react-redux';
import kebabCase from 'lodash/kebabCase';
import PropTypes from 'prop-types';
-import isEqual from 'lodash/isEqual';
import get from 'lodash/get';
-import { getShallowChanges } from 'coral-framework/utils';
-import omit from 'lodash/omit';
-
-const emptyConfig = {};
+import { withSlotElements, withCombatPassthrough } from '../hocs';
+import { compose } from 'recompose';
class Slot extends React.Component {
- static contextTypes = {
- plugins: PropTypes.object,
- };
-
- 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.
- const passthroughIndex = changes.indexOf('passthrough');
- if (passthroughIndex !== -1) {
- if (!this.props.passthrough || next.passthrough) {
- return true;
- }
- if (
- getShallowChanges(this.props.passthrough, next.passthrough).lenght === 0
- ) {
- changes.splice(passthroughIndex, 1);
- }
- }
-
- if (changes.length === 1 && changes[0] === 'reduxState') {
- const prevChildrenKeys = this.getChildren(this.props).map(
- child => child.key
- );
- const nextChildrenKeys = this.getChildren(next).map(child => child.key);
- return !isEqual(prevChildrenKeys, nextChildrenKeys);
- }
-
- // Prevent Slot from rerendering when no props has shallowly changed.
- return changes.length !== 0;
- }
-
- getPassthrough(props = this.props) {
- const slotProps = omit(props, [
- 'fill',
- 'inline',
- 'className',
- 'reduxState',
- 'size',
- 'defaultComponent',
- 'queryData',
- 'childFactory',
- 'component',
- 'passthrough',
- 'dispatch',
- ]);
-
- // @Deprecated
- if (process.env.NODE_ENV !== 'production') {
- if (Object.keys(slotProps).length) {
- /* eslint-disable no-console */
- console.warn(
- `Slot '${
- props.fill
- }' passing through unknown props is deprecated, please use 'passthrough' instead`,
- slotProps
- );
- /* eslint-enable no-console */
- }
- }
-
- if (props.passthrough) {
- return props.passthrough;
- }
-
- if (props.queryData) {
- if (process.env.NODE_ENV !== 'production') {
- /* eslint-disable no-console */
- console.warn(
- `Slot '${
- props.fill
- }' property 'queryData' is deprecated, please use 'passthrough' instead`
- );
- /* eslint-enable no-console */
- }
- return {
- ...props.queryData,
- ...slotProps,
- };
- }
-
- return slotProps;
- }
-
- getChildren(props = this.props) {
- const { size = 0 } = props;
- const { plugins } = this.context;
-
- return plugins.getSlotElements(
- props.fill,
- props.reduxState,
- this.getPassthrough(props),
- { size }
- );
- }
-
render() {
const {
inline = false,
className,
- reduxState,
+ debug,
component: Component,
childFactory,
- defaultComponent: DefaultComponent,
fill,
} = this.props;
- const { plugins } = this.context;
- let children = this.getChildren();
- const pluginConfig =
- get(reduxState, 'config.plugins_config') || emptyConfig;
- if (children.length === 0 && DefaultComponent) {
- const props = plugins.getSlotComponentProps(
- DefaultComponent,
- reduxState,
- this.getPassthrough()
- );
- children = ;
- }
-
+ let children = this.props.slotElements;
if (childFactory) {
children = children.map(childFactory);
}
@@ -140,7 +26,7 @@ class Slot extends React.Component {
return (
({
- reduxState: state,
+ debug: get(state, 'config.plugins_config.debug'),
});
-export default connect(mapStateToProps, null)(Slot);
+export default compose(
+ withCombatPassthrough(omitProps),
+ withSlotElements({
+ slot: props => props.fill,
+ size: props => props.size,
+ defaultComponent: props => props.defaultComponent,
+ }),
+ connect(mapStateToProps, null)
+)(Slot);
diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index 7ed114b73..a0f0a3e9c 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -12,6 +12,8 @@ export { default as withForgotPassword } from './withForgotPassword';
export { default as withSetUsername } from './withSetUsername';
export { default as withPopupAuthHandler } from './withPopupAuthHandler';
export { default as withEnumValues } from './withEnumValues';
+export { default as withCombatPassthrough } from './withCombatPassthrough';
+export { default as withSlotElements } from './withSlotElements';
export {
default as withResendEmailConfirmation,
} from './withResendEmailConfirmation';
diff --git a/client/coral-framework/hocs/withCombatPassthrough.js b/client/coral-framework/hocs/withCombatPassthrough.js
new file mode 100644
index 000000000..84dbdb492
--- /dev/null
+++ b/client/coral-framework/hocs/withCombatPassthrough.js
@@ -0,0 +1,53 @@
+import withProps from 'recompose/withProps';
+import omit from 'lodash/omit';
+
+function getPassthrough(props, omitProps) {
+ const slotProps = omit(props, [...omitProps, 'passthrough']);
+
+ // @Deprecated
+ if (process.env.NODE_ENV !== 'production') {
+ if (Object.keys(slotProps).length) {
+ /* eslint-disable no-console */
+ console.warn(
+ `Slot '${
+ props.fill
+ }' passing through unknown props is deprecated, please use 'passthrough' instead`,
+ slotProps
+ );
+ /* eslint-enable no-console */
+ }
+ }
+
+ if (props.passthrough) {
+ return props.passthrough;
+ }
+
+ if (props.queryData) {
+ if (process.env.NODE_ENV !== 'production') {
+ /* eslint-disable no-console */
+ console.warn(
+ `Slot '${
+ props.fill
+ }' property 'queryData' is deprecated, please use 'passthrough' instead`
+ );
+ /* eslint-enable no-console */
+ }
+ return {
+ ...props.queryData,
+ ...slotProps,
+ };
+ }
+
+ return slotProps;
+}
+
+/**
+ * @Deprecated
+ * withCombatPassthrough is a compatibility HOC that supports our old
+ * API which puts unknown props and `queryData` to `passhtrough` to be
+ * used with HOC `withSlotElements`.
+ */
+export default omitProps =>
+ withProps(props => ({
+ passthrough: getPassthrough(props, omitProps),
+ }));
diff --git a/client/coral-framework/hocs/withSlotElements.js b/client/coral-framework/hocs/withSlotElements.js
new file mode 100644
index 000000000..c7aa9fe29
--- /dev/null
+++ b/client/coral-framework/hocs/withSlotElements.js
@@ -0,0 +1,174 @@
+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,
+}) =>
+ 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') {
+ 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.getSizes(props, slots.length);
+
+ const elements = [];
+ slots.forEach((s, i) => {
+ const DefaultComponent = defaultComponents[i];
+ const size = sizes[i];
+ const slotElements = plugins.getSlotElements(
+ s,
+ props.reduxState,
+ this.getPassthrough(props),
+ { size }
+ );
+
+ if (slotElements.length === 0 && DefaultComponent) {
+ const p = plugins.getSlotComponentProps(
+ DefaultComponent,
+ props.reduxState,
+ this.getPassthrough(props)
+ );
+ 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 = {
+ 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
+ *
+ * @return {func} Returns a HOC that 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));
+};