mirror of
https://github.com/wassname/talk.git
synced 2026-07-13 17:45:56 +08:00
Refactor Slot Feature
This commit is contained in:
@@ -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 (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
{React.cloneElement(el, {
|
||||
active: this.props.activeTab === el.type.talkPluginName,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
});
|
||||
}
|
||||
createPluginTabFactory = (props = this.props) => el => {
|
||||
return (
|
||||
<ExtendableTab
|
||||
tabId={el.type.talkPluginName}
|
||||
key={el.type.talkPluginName}
|
||||
>
|
||||
{React.cloneElement(el, {
|
||||
active: props.activeTab === el.type.talkPluginName,
|
||||
})}
|
||||
</ExtendableTab>
|
||||
);
|
||||
};
|
||||
|
||||
getTabElements(props = this.props) {
|
||||
const elements = [...this.getPluginTabElementsPrepend(props)];
|
||||
@@ -91,14 +57,16 @@ class ExtendableTabPanelContainer extends React.Component {
|
||||
return elements;
|
||||
}
|
||||
|
||||
createPluginTabPane(el) {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
}
|
||||
|
||||
getPluginTabPaneElements(props = this.props) {
|
||||
return this.getSlotElements(props.tabPaneSlot).map(el => {
|
||||
return (
|
||||
<TabPane tabId={el.type.talkPluginName} key={el.type.talkPluginName}>
|
||||
{el}
|
||||
</TabPane>
|
||||
);
|
||||
});
|
||||
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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = <DefaultComponent {...props} />;
|
||||
}
|
||||
|
||||
let children = this.props.slotElements;
|
||||
if (childFactory) {
|
||||
children = children.map(childFactory);
|
||||
}
|
||||
@@ -140,7 +26,7 @@ class Slot extends React.Component {
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
{ [styles.inline]: inline, [styles.debug]: pluginConfig.debug },
|
||||
{ [styles.inline]: inline, [styles.debug]: debug },
|
||||
className,
|
||||
`talk-slot-${kebabCase(fill)}`
|
||||
)}
|
||||
@@ -159,7 +45,8 @@ Slot.propTypes = {
|
||||
fill: PropTypes.string.isRequired,
|
||||
inline: PropTypes.bool,
|
||||
className: PropTypes.string,
|
||||
reduxState: PropTypes.object,
|
||||
debug: PropTypes.bool,
|
||||
slotElements: PropTypes.arrayOf(PropTypes.element).isRequired,
|
||||
defaultComponent: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
|
||||
|
||||
/**
|
||||
@@ -193,8 +80,27 @@ Slot.propTypes = {
|
||||
childFactory: PropTypes.func,
|
||||
};
|
||||
|
||||
const omitProps = [
|
||||
'fill',
|
||||
'inline',
|
||||
'className',
|
||||
'size',
|
||||
'defaultComponent',
|
||||
'queryData',
|
||||
'childFactory',
|
||||
'component',
|
||||
];
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
@@ -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(<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 = {
|
||||
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
|
||||
*
|
||||
* @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));
|
||||
};
|
||||
Reference in New Issue
Block a user