Skip to content

Commit a2c1f5b

Browse files
committed
BREAKING CHANGE: feat(WizardPattern): Clean up Wizard, add WizardPattern and StatefulWizardPattern components
1 parent b9d2b6d commit a2c1f5b

31 files changed

+2103
-1490
lines changed

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@
8888
"react": "^16.3.0",
8989
"react-dev-utils": "^5.0.0",
9090
"react-dom": "^16.3.0",
91-
"sass-loader": "^6.0.7",
91+
"react-test-renderer": "^16.2.0",
92+
"sass-loader": "^6.0.6",
9293
"semantic-release": "^12.2.0",
9394
"style-loader": "^0.20.3",
9495
"stylelint": "^8.4.0",
@@ -97,7 +98,7 @@
9798
},
9899
"peerDependencies": {
99100
"prop-types": "^15.6.0",
100-
"react": "^15.6.2 || ^16.2.0",
101+
"react": "^16.3.1",
101102
"react-dom": "^15.6.2 || ^16.2.0"
102103
},
103104
"sassIncludes": {
@@ -143,4 +144,4 @@
143144
"czConfig": {
144145
"path": "node_modules/cz-conventional-changelog"
145146
}
146-
}
147+
}

src/common/controlled.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import { nullValues, bindMethods, selectKeys, filterKeys } from './helpers';
55
/*
66
controlled(stateTypes, defaults)(WrappedComponent)
77
8+
*** NOTE / BEWARE! *******************************************************************************
9+
This is already deprecated, even as new as it is, because we now have getDerivedStateFromProps!
10+
It remains here for now because of its additional "persist" feature, which we should factor out.
11+
**************************************************************************************************
12+
813
This Higher Order Component provides the controlled component pattern on a prop-by-prop basis.
914
It's a nice way for components to implement internal state so they "just work" out of the box,
1015
but also give users the option of lifting some or all of that state up into their application.

src/common/helpers.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import React from 'react';
22

3+
/** Equivalent to calling `this.someMethod = this.someMethod.bind(this)` for every method name in the methods array. */
34
export const bindMethods = (context, methods) => {
45
methods.forEach(method => {
56
context[method] = context[method].bind(context);
67
});
78
};
89

9-
// Implementation of the debounce function
10+
/** Implementation of the debounce function */
1011
export const debounce = (func, wait) => {
1112
let timeout;
1213
function innerFunc(...args) {
@@ -17,33 +18,49 @@ export const debounce = (func, wait) => {
1718
return innerFunc;
1819
};
1920

20-
// Returns a subset of the given object including only the given keys, with values optionally replaced by a fn.
21+
/** Returns true if propName is a non-null, defined property of the props object (can be any object, not just React props). */
22+
export const propExists = (props, propName) =>
23+
props && props.hasOwnProperty(propName) && props[propName] != null;
24+
25+
/** Given two objects (props and state), returns the value of propName from props if present, or from state otherwise. */
26+
export const propOrState = (props, state, propName) =>
27+
propExists(props, propName) ? props[propName] : state[propName];
28+
29+
/** Returns a subset of the given object including only the given keys, with values optionally replaced by a fn. */
2130
export const selectKeys = (obj, keys, fn = val => val) =>
2231
keys.reduce((values, key) => ({ ...values, [key]: fn(obj[key]) }), {});
2332

24-
// Returns a subset of the given object with a validator function applied to its keys.
33+
/** Returns a subset of the given object with a validator function applied to its keys. */
2534
export const filterKeys = (obj, validator) =>
2635
selectKeys(obj, Object.keys(obj).filter(validator));
2736

37+
/** Returns a subset of the given object with the given keys left out. */
38+
export const excludeKeys = (obj, keys) =>
39+
filterKeys(obj, key => !keys.includes(key));
40+
41+
/** Returns the given React children prop as a regular array of React nodes. */
2842
export const childrenToArray = children =>
2943
children &&
3044
React.Children.count(children) > 0 &&
3145
React.Children.toArray(children);
3246

47+
/** Filters the given React children prop with the given validator function. Returns an array of nodes. */
3348
export const filterChildren = (children, validator) => {
3449
const array = childrenToArray(children);
3550
return array && array.filter(validator);
3651
};
3752

53+
/** Given a React children prop, finds the first child node to pass the validator function. */
3854
export const findChild = (children, validator) => {
3955
const array = childrenToArray(children);
4056
return array && array.find(validator);
4157
};
4258

59+
/** Returns true if there is at least one of propNames with a different value in newProps than in oldProps. */
4360
export const propsChanged = (propNames, oldProps, newProps) =>
4461
propNames.some(propName => oldProps[propName] !== newProps[propName]);
4562

46-
// Returns an object with the same keys as the given one, but all null values.
63+
/** Returns an object with the same keys as the given one, but all null values. */
4764
export const nullValues = obj => selectKeys(obj, Object.keys(obj), () => null);
4865

4966
export const noop = Function.prototype;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import WizardPattern from './WizardPattern';
5+
import { wizardStepShape } from './WizardPatternConstants';
6+
import { noop, propOrState, excludeKeys } from '../../../index';
7+
8+
/**
9+
* StatefulWizardPattern - the Stateful Wizard Pattern component.
10+
*/
11+
class StatefulWizardPattern extends React.Component {
12+
static getDerivedStateFromProps(nextProps, prevState) {
13+
return {
14+
activeStepIndex: propOrState(nextProps, prevState, 'activeStepIndex')
15+
};
16+
}
17+
18+
constructor(props) {
19+
super(props);
20+
this.state = { activeStepIndex: 0 };
21+
}
22+
23+
onStepChanged = newStepIndex => {
24+
const { shouldPreventStepChange, steps } = this.props;
25+
const { activeStepIndex } = this.state;
26+
const { shouldPreventExit } = steps[activeStepIndex];
27+
const { shouldPreventEnter } = steps[newStepIndex];
28+
if (
29+
shouldPreventStepChange(activeStepIndex, newStepIndex) ||
30+
(shouldPreventExit && shouldPreventExit(newStepIndex)) ||
31+
(shouldPreventEnter && shouldPreventEnter(activeStepIndex))
32+
) {
33+
return;
34+
}
35+
this.setState({ activeStepIndex: newStepIndex });
36+
};
37+
38+
render() {
39+
const { shouldDisableNextStep, ...otherProps } = this.props;
40+
const { activeStepIndex } = this.state;
41+
// NOTE: the steps array is passed down with ...otherProps, including any shouldPreventEnter or shouldPreventExit functions inside it.
42+
// These functions are for StatefulWizardPattern only and should not be used in WizardPattern despite being passed down here.
43+
return (
44+
<WizardPattern
45+
nextStepDisabled={shouldDisableNextStep(activeStepIndex)}
46+
{...otherProps}
47+
activeStepIndex={activeStepIndex} // Value from state, as set by getDerivedStateFromProps
48+
onStepChanged={this.onStepChanged}
49+
/>
50+
);
51+
}
52+
}
53+
54+
StatefulWizardPattern.propTypes = {
55+
...excludeKeys(WizardPattern.propTypes, ['activeStepIndex']),
56+
steps: PropTypes.arrayOf(
57+
PropTypes.shape({
58+
...wizardStepShape,
59+
shouldPreventEnter: PropTypes.func,
60+
shouldPreventExit: PropTypes.func
61+
})
62+
),
63+
shouldDisableNextStep: PropTypes.func,
64+
shouldPreventStepChange: PropTypes.func
65+
};
66+
67+
StatefulWizardPattern.defaultProps = {
68+
...excludeKeys(WizardPattern.defaultProps, ['activeStepIndex']),
69+
shouldDisableNextStep: noop,
70+
shouldPreventStepChange: noop
71+
};
72+
73+
export default StatefulWizardPattern;
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { noop, Wizard, Icon, Button } from '../../../index';
4+
import WizardPatternBody from './WizardPatternBody';
5+
import { wizardStepShape } from './WizardPatternConstants';
6+
7+
/**
8+
* WizardPattern - the Wizard Pattern Body component.
9+
*/
10+
const WizardPattern = ({
11+
steps,
12+
activeStepIndex,
13+
onStepChanged,
14+
onNext,
15+
onBack,
16+
nextStepDisabled,
17+
title,
18+
loadingTitle,
19+
loadingMessage,
20+
show,
21+
onHide,
22+
onExited,
23+
stepButtonsDisabled,
24+
cancelText,
25+
backText,
26+
nextText,
27+
closeText,
28+
loading,
29+
nextButtonRef,
30+
bodyHeader,
31+
children
32+
}) => {
33+
const onFirstStep = activeStepIndex === 0;
34+
const onFinalStep = activeStepIndex === steps.length - 1;
35+
36+
const onHideClick = () => {
37+
onHide(onFinalStep);
38+
};
39+
40+
const onBackClick = () => {
41+
goToStep(Math.max(activeStepIndex - 1, 0));
42+
};
43+
44+
const onNextClick = () => {
45+
goToStep(Math.min(activeStepIndex + 1, steps.length - 1));
46+
};
47+
48+
const getStep = (index = activeStepIndex) => steps[index];
49+
50+
const getPrevStep = (relativeToIndex = activeStepIndex) =>
51+
relativeToIndex > 0 && steps[relativeToIndex - 1];
52+
53+
const getNextStep = (relativeToIndex = activeStepIndex) =>
54+
relativeToIndex < steps.length - 1 && steps[relativeToIndex + 1];
55+
56+
const activeStep = getStep();
57+
58+
const goToStep = newStepIndex => {
59+
if (shouldPreventGoToStep(newStepIndex)) return;
60+
if (newStepIndex === activeStepIndex + 1) {
61+
const stepOnNextResult = activeStep.onNext && activeStep.onNext();
62+
const propOnNextResult = onNext(newStepIndex);
63+
const stepFailed =
64+
stepOnNextResult === false || propOnNextResult === false;
65+
if (stepFailed) return;
66+
}
67+
if (newStepIndex === activeStepIndex - 1) {
68+
const stepFailed = onBack(newStepIndex) === false;
69+
if (stepFailed) return;
70+
}
71+
if (onStepChanged) onStepChanged(newStepIndex);
72+
};
73+
74+
const shouldPreventGoToStep = newStepIndex => {
75+
const targetStep = getStep(newStepIndex);
76+
const stepBeforeTarget = getPrevStep(newStepIndex);
77+
78+
const preventExitActive = activeStep.preventExit;
79+
const preventEnterTarget =
80+
targetStep.preventEnter ||
81+
(stepBeforeTarget && stepBeforeTarget.isInvalid);
82+
const nextStepClicked = newStepIndex === activeStepIndex + 1;
83+
84+
return (
85+
preventExitActive ||
86+
preventEnterTarget ||
87+
(nextStepClicked && nextStepDisabled)
88+
);
89+
};
90+
91+
const activeStepStr = (activeStepIndex + 1).toString();
92+
93+
const prevStepUnreachable =
94+
onFirstStep || activeStep.preventExit || getPrevStep().preventEnter;
95+
// nextStepUnreachable is still true onFinalStep, because the Next button turns into a Close button
96+
const nextStepUnreachable =
97+
nextStepDisabled ||
98+
activeStep.isInvalid ||
99+
activeStep.preventExit ||
100+
getNextStep().preventEnter;
101+
102+
return (
103+
<Wizard show={show} onHide={onHideClick} onExited={onExited}>
104+
<Wizard.Header onClose={onHideClick} title={title} />
105+
<Wizard.Body>
106+
{bodyHeader}
107+
<WizardPatternBody
108+
loadingTitle={loadingTitle}
109+
loadingMessage={loadingMessage}
110+
loading={loading}
111+
steps={steps}
112+
activeStepIndex={activeStepIndex}
113+
activeStepStr={activeStepStr}
114+
goToStep={goToStep}
115+
nextStepDisabled={nextStepDisabled}
116+
stepButtonsDisabled={stepButtonsDisabled}
117+
/>
118+
</Wizard.Body>
119+
<Wizard.Footer>
120+
<Button bsStyle="default" className="btn-cancel" onClick={onHideClick}>
121+
{cancelText}
122+
</Button>
123+
<Button
124+
bsStyle="default"
125+
onClick={onBackClick}
126+
disabled={prevStepUnreachable}
127+
>
128+
<Icon type="fa" name="angle-left" />
129+
{backText}
130+
</Button>
131+
<Button
132+
bsStyle="primary"
133+
onClick={onFinalStep ? onHideClick : onNextClick}
134+
disabled={nextStepUnreachable}
135+
ref={nextButtonRef}
136+
>
137+
{onFinalStep ? (
138+
closeText
139+
) : (
140+
<React.Fragment>
141+
{nextText}
142+
<Icon type="fa" name="angle-right" />
143+
</React.Fragment>
144+
)}
145+
</Button>
146+
</Wizard.Footer>
147+
{children}
148+
</Wizard>
149+
);
150+
};
151+
152+
WizardPattern.propTypes = {
153+
activeStepIndex: PropTypes.number.isRequired,
154+
show: PropTypes.bool,
155+
title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
156+
onHide: PropTypes.func,
157+
onExited: PropTypes.func,
158+
onBack: PropTypes.func,
159+
onNext: PropTypes.func,
160+
onStepChanged: PropTypes.func,
161+
loadingTitle: PropTypes.string,
162+
loadingMessage: PropTypes.string,
163+
loading: PropTypes.bool,
164+
cancelText: PropTypes.string,
165+
backText: PropTypes.string,
166+
nextText: PropTypes.string,
167+
closeText: PropTypes.string,
168+
steps: PropTypes.arrayOf(PropTypes.shape(wizardStepShape)),
169+
nextStepDisabled: PropTypes.bool,
170+
stepButtonsDisabled: PropTypes.bool,
171+
nextButtonRef: PropTypes.func,
172+
bodyHeader: PropTypes.node,
173+
children: PropTypes.node
174+
};
175+
176+
WizardPattern.defaultProps = {
177+
show: false,
178+
title: '',
179+
onHide: noop,
180+
onExited: noop,
181+
onBack: noop,
182+
onNext: noop,
183+
onStepChanged: noop,
184+
loadingTitle: 'Loading Wizard...',
185+
loadingMessage: 'Loading...',
186+
loading: false,
187+
cancelText: 'Cancel',
188+
backText: 'Back',
189+
nextText: 'Next',
190+
closeText: 'Close',
191+
steps: [],
192+
nextStepDisabled: false,
193+
stepButtonsDisabled: false,
194+
nextButtonRef: noop,
195+
bodyHeader: null,
196+
children: null
197+
};
198+
199+
WizardPattern.displayName = 'WizardPattern';
200+
201+
export default WizardPattern;

0 commit comments

Comments
 (0)