react-render-iff
Version:
A helper component to render react components conditionally.
292 lines (257 loc) • 7.71 kB
JSX
/* eslint-disable no-console */
import React from "react";
import PropTypes from "prop-types";
function isFunc(functionToCheck) {
if (!functionToCheck) return false;
const getType = {};
const toString = getType.toString.call(functionToCheck);
return (
toString === "[object Function]" ||
toString === "[object AsyncFunction]" ||
false
);
}
const evaluateValue = (prop, data) =>
prop && isFunc(prop) ? prop(data) : prop;
/**
* A helper component to render a child component
* conditionally.
*
* @param {{
if: Boolean | (utils) => Boolean,
ifFirst: Array<Number, Array>,
ifLast: Array<Number, Array>,
if2ndToLast: Array<Number, Array>,
elseIf: Boolean | (utils) => Boolean
else: Component | (utils) => Compoent,
children: Component,
render: Component | () => Component,
elseIfRender: Component | () => Component,
as: ?String,
safeEval: ?String|Boolean,
* }} props
*/
const RenderIf = React.forwardRef(function RenderIf(props, refToForward) {
let ifCondition;
const elseIfCondition = props.elseIf;
const consoleWarn = function () {
console.warn.apply(undefined, arguments);
};
const checkIsAtPositionInArray = (check, couple) => {
const checkFirst = check === "first";
const checkLast = check === "last";
const check2ndToLast = check === "2ndToLast";
// Create the if condition from the provided index and array
const indexOfIndex = !Number.isNaN(couple[0])
? 0
: !Number.isNaN(couple[1])
? 1
: undefined;
const index =
indexOfIndex !== undefined
? couple[indexOfIndex === 0 ? 0 : 1]
: undefined;
const arr = couple[indexOfIndex === 0 ? 1 : 0];
if (index === undefined) {
consoleWarn(
"Should always provide an index for ifFirst or ifLast check."
);
}
if (checkFirst && index !== undefined) {
return index === 0;
}
if (!arr) {
consoleWarn("Should always provide an array for position check");
// Default to ifLast=true
return true;
}
if (checkLast) {
return index === arr.length - 1;
}
if (check2ndToLast) {
return index === arr.length - 2;
}
return false;
};
const mutuallyExclusiveIfProps = ["if", "ifFirst", "ifLast", "if2ndToLast"];
const providedMutuallyExclusiveIfProps = mutuallyExclusiveIfProps.filter(
(key) => props[key] !== undefined
);
const hasAnIfConditionProps = mutuallyExclusiveIfProps.some((key) => {
return Object.keys(props).includes(key);
});
if (providedMutuallyExclusiveIfProps.length > 1) {
consoleWarn(
"You've provided more than one of the following top-level conditional props. You should only provide one of these props to avoid unexpected consequences.\n\t",
JSON.stringify(providedMutuallyExclusiveIfProps)
);
}
const getArrayProp = () => props.ifFirst || props.ifLast || props.if2ndToLast;
const cbData = {
checkIsFirst: (couple) =>
checkIsAtPositionInArray("first", couple || getArrayProp()),
checkIsLast: (couple) =>
checkIsAtPositionInArray("last", couple || getArrayProp()),
checkIs2ndToLast: (couple) =>
checkIsAtPositionInArray("2ndToLast", couple || getArrayProp()),
};
const renderResult = (v) => {
if (props.as) {
const wrapperProps = { ...props };
if (refToForward) {
wrapperProps.ref = refToForward;
}
const cssProp = wrapperProps.css;
delete wrapperProps.if;
delete wrapperProps.ifFirst;
delete wrapperProps.ifLast;
delete wrapperProps.if2ndToLast;
delete wrapperProps.else;
delete wrapperProps.elseIf;
delete wrapperProps.elseIfRender;
delete wrapperProps.as;
delete wrapperProps.css;
delete wrapperProps.debugKey;
delete wrapperProps.safeEval;
return (
// The styled-component css prop only works if the prop is defined directly on the element
// instead of being spread.
// I'm not sure why.
<props.as css={cssProp} {...wrapperProps}>
{v}
</props.as>
);
} else {
return v;
}
};
if (
props.ifFirst !== undefined ||
props.ifLast !== undefined ||
props.if2ndToLast
) {
if (props.ifFirst) {
ifCondition = cbData.checkIsFirst(props.ifFirst);
} else if (props.ifLast) {
ifCondition = cbData.checkIsLast(props.ifLast);
} else if (props.if2ndToLast) {
ifCondition = cbData.checkIs2ndToLast(props.if2ndToLast);
}
} else {
ifCondition = props.if;
}
if (ifCondition === undefined && !hasAnIfConditionProps) {
consoleWarn(
`Should provide props.if\n${
props.debugKey
? `Debug key: ${props.debugKey}. ${Object.keys(props)}`
: ""
}`
);
}
if (props.elseIf !== undefined && !props.elseIfRender) {
consoleWarn(
"You provided props.elseIf without a corresponding props.elseIfRender. Nothing will be rendered if this condition evaluates to true"
);
}
// Evaluate if condition
let evaluatedIfCondition;
try {
evaluatedIfCondition = evaluateValue(ifCondition, cbData);
} catch (err) {
if (props.safeEval) {
consoleWarn(
`[safeEval: ${props.safeEval}] Error while evaluating "if" condition:`,
err.stack
);
} else {
throw err;
}
}
if (evaluatedIfCondition) {
if (props.render) {
try {
return renderResult(evaluateValue(props.render));
} catch (err) {
if (props.safeEval) {
consoleWarn(
`[safeEval: ${props.safeEval}] Error while evaluating "render" prop:`,
err.stack
);
} else {
throw err;
}
}
}
return renderResult(props.children);
}
// Evaluate else-if condition
let evaluatedElseIfCondition;
try {
evaluatedElseIfCondition = evaluateValue(elseIfCondition, cbData);
} catch (err) {
if (props.safeEval) {
consoleWarn(
`[safeEval: ${props.safeEval}] Error while evaluating "elseIf" condition:`,
err.stack
);
} else {
throw err;
}
}
if (evaluatedElseIfCondition) {
if (props.elseIfRender) {
try {
return renderResult(evaluateValue(props.elseIfRender));
} catch (err) {
if (props.safeEval) {
consoleWarn(
`[safeEval: ${props.safeEval}] Error while evaluating "elseIfRender" prop:`,
err.stack
);
} else {
throw err;
}
}
}
return null;
}
// Render the else contents
if (props.else !== undefined) {
try {
return renderResult(evaluateValue(props.else, cbData));
} catch (err) {
if (props.safeEval) {
consoleWarn(
`[safeEval: ${props.safeEval}] Error while evaluating "else" prop:`,
err.stack
);
} else {
throw err;
}
}
}
return null;
});
RenderIf.propTypes = {
if: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
ifFirst: PropTypes.array,
ifLast: PropTypes.array,
if2ndToLast: PropTypes.array,
elseIf: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
else: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
children: PropTypes.node,
render: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
elseIfRender: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
as: PropTypes.string,
// Support styled-components css prop
css: PropTypes.any,
safeEval: PropTypes.oneOfType([PropTypes.bool, PropTypes.string]),
};
RenderIf.defaultProps = {
if: false,
safeEval: false,
as: undefined,
css: undefined,
};
export default RenderIf;