diff --git a/client/coral-framework/components/IfSlotIsEmpty.js b/client/coral-framework/components/IfSlotIsEmpty.js index 9aebd94c6..fa8f2524d 100644 --- a/client/coral-framework/components/IfSlotIsEmpty.js +++ b/client/coral-framework/components/IfSlotIsEmpty.js @@ -2,6 +2,7 @@ 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'; class IfSlotIsEmpty extends React.Component { static contextTypes = { @@ -9,9 +10,23 @@ class IfSlotIsEmpty extends React.Component { }; 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. - const changes = getShallowChanges(this.props, next); if (changes.length === 1 && changes[0] === 'reduxState') { return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next); } @@ -20,19 +35,58 @@ class IfSlotIsEmpty extends React.Component { 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, - className: _a, - reduxState, - component: _b = 'div', - children: _c, - queryData, - ...rest - } = 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, rest, queryData) + this.context.plugins.isSlotEmpty(slot, reduxState, passthrough) ); } @@ -44,6 +98,8 @@ class IfSlotIsEmpty extends React.Component { IfSlotIsEmpty.propTypes = { slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + children: PropTypes.node.isRequired, + passthrough: PropTypes.object, }; const mapStateToProps = state => ({ diff --git a/client/coral-framework/components/IfSlotIsNotEmpty.js b/client/coral-framework/components/IfSlotIsNotEmpty.js index 03f300858..024877fbc 100644 --- a/client/coral-framework/components/IfSlotIsNotEmpty.js +++ b/client/coral-framework/components/IfSlotIsNotEmpty.js @@ -2,6 +2,7 @@ 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'; class IfSlotIsNotEmpty extends React.Component { static contextTypes = { @@ -9,9 +10,23 @@ class IfSlotIsNotEmpty extends React.Component { }; 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. - const changes = getShallowChanges(this.props, next); if (changes.length === 1 && changes[0] === 'reduxState') { return this.isSlotEmpty(this.props) !== this.isSlotEmpty(next); } @@ -20,19 +35,58 @@ class IfSlotIsNotEmpty extends React.Component { 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, - className: _a, - reduxState, - component: _b = 'div', - children: _c, - queryData, - ...rest - } = 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, rest, queryData) + this.context.plugins.isSlotEmpty(slot, reduxState, passthrough) ); } @@ -44,6 +98,8 @@ class IfSlotIsNotEmpty extends React.Component { IfSlotIsNotEmpty.propTypes = { slot: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + children: PropTypes.node.isRequired, + passthrough: PropTypes.object, }; const mapStateToProps = state => ({ diff --git a/client/coral-framework/components/Slot.js b/client/coral-framework/components/Slot.js index 73f6f1bb0..02f182dc3 100644 --- a/client/coral-framework/components/Slot.js +++ b/client/coral-framework/components/Slot.js @@ -20,6 +20,20 @@ class Slot extends React.Component { // 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 @@ -32,8 +46,8 @@ class Slot extends React.Component { return changes.length !== 0; } - getSlotProps(props = this.props) { - return omit(props, [ + getPassthrough(props = this.props) { + const slotProps = omit(props, [ 'fill', 'inline', 'className', @@ -43,7 +57,45 @@ class Slot extends React.Component { '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) { @@ -53,8 +105,7 @@ class Slot extends React.Component { return plugins.getSlotElements( props.fill, props.reduxState, - this.getSlotProps(props), - props.queryData, + this.getPassthrough(props), { size } ); } @@ -67,7 +118,6 @@ class Slot extends React.Component { component: Component, childFactory, defaultComponent: DefaultComponent, - queryData, fill, } = this.props; const { plugins } = this.context; @@ -78,8 +128,7 @@ class Slot extends React.Component { const props = plugins.getSlotComponentProps( DefaultComponent, reduxState, - this.getSlotProps(this.props), - queryData + this.getPassthrough() ); children = ; } @@ -125,8 +174,12 @@ Slot.propTypes = { component: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), // props coming from graphql must be passed through this property. + // @Deprecated queryData: PropTypes.object, + // props that are passed to all Slot Components + passthrough: PropTypes.object, + /** * You may need to apply reactive updates to a child as it is exiting. * This is generally done by using `cloneElement` however in the case of an exiting diff --git a/client/coral-framework/services/plugins.js b/client/coral-framework/services/plugins.js index d797e0e3e..3879249ac 100644 --- a/client/coral-framework/services/plugins.js +++ b/client/coral-framework/services/plugins.js @@ -67,39 +67,64 @@ function addMetaDataToSlotComponents(plugins) { }); } +/** + * getSlotComponentProps calculate the props we would pass to the slot component. + * query datas are only passed to the component if it is defined in `component.fragments`. + */ +function getSlotComponentProps(component, reduxState, props, queryData) { + const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig; + return { + ...props, + config: pluginConfig, + ...(component.fragments + ? pick(queryData, Object.keys(component.fragments)) + : withWarnings(component, queryData)), + }; +} + +/** + * splitProps detects props coming from the query and + * returns `queryData` and `rest`. + */ +function splitProps(props) { + const rest = { ...props }; + const queryData = {}; + if (props.passthrough) { + Object.keys(props).forEach(k => { + if (props[k].__unsafeFromQuery) { + queryData[k] = props[k]; + delete rest[k]; + } + }); + } + return { queryData, rest }; +} + class PluginsService { constructor(plugins) { this.plugins = plugins; addMetaDataToSlotComponents(plugins); } - isSlotEmpty(slot, reduxState, props = {}, queryData = {}) { - return ( - this.getSlotElements(slot, reduxState, props, queryData).length === 0 - ); + isSlotEmpty(slot, reduxState, props = {}) { + return this.getSlotElements(slot, reduxState, props).length === 0; } /** - * getSlotComponentProps calculate the props we would pass to the slot component. - * query datas are only passed to the component if it is defined in `component.fragments`. + * Returns props that would pass to the given slot component. */ - getSlotComponentProps(component, reduxState, props, queryData) { - const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig; - return { - ...props, - config: pluginConfig, - ...(component.fragments - ? pick(queryData, Object.keys(component.fragments)) - : withWarnings(component, queryData)), - }; + getSlotComponentProps(component, reduxState, props) { + const { queryData, rest } = splitProps(props); + return getSlotComponentProps(component, reduxState, rest, queryData); } /** * Returns React Elements for given slot. */ - getSlotElements(slot, reduxState, props = {}, queryData = {}, options = {}) { + getSlotElements(slot, reduxState, props = {}, options = {}) { const pluginConfig = get(reduxState, 'config.plugin_config') || emptyConfig; const { size = 0 } = options; + const { queryData, rest } = splitProps(props); const isDisabled = component => { if ( @@ -112,10 +137,10 @@ class PluginsService { // Check if component is excluded. if (component.isExcluded) { - let resolvedProps = this.getSlotComponentProps( + let resolvedProps = getSlotComponentProps( component, reduxState, - props, + rest, queryData ); if (component.mapStateToProps) { @@ -154,12 +179,7 @@ class PluginsService { .map(({ component, key }) => React.createElement(component, { key, - ...this.getSlotComponentProps( - component, - reduxState, - props, - queryData - ), + ...getSlotComponentProps(component, reduxState, rest, queryData), }) ); }