Next Disabled should be disabled until the validation has passed
Hi @newbreedofgeek can you add a way to make that Next Button be disabled before component report a valid state?
Good idea, I will fit this in at some point soon.
In the meantime, just keen a local state on if you clicked next/save and do not process another click based on this.
This should be high priority. As it breaks when it's promise based!
@jorgecuesta is the bug fixed?
No sorry was a miss click. I reopen it.
@newbreedofgeek I have tried to solve this issue and could not find a way other than making a HOC that wraps every step and on update it will check the validate function. Do you have any other ideas? also if I work on making a HOC for steps will it be mergerd? Because it will require many changes to the code.
I am in mobile now, so I will post some code later.
My team and I get it working using Redux
import React from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import Promise from 'promise';
import isEmpty from 'lodash/isEmpty';
import FlexBox from 'flexbox-react';
import Modal from 'react-bootstrap/lib/Modal';
import Button from 'components/Button';
import Header from './components/Header';
import Body from './components/Body';
import Footer from './components/Footer';
import Tooltip from 'components/Tooltip';
class Wizard extends React.Component {
constructor(props) {
super(props);
this.state = {
showPreviousBtn: false,
nextStepText: props.nextText,
activeStep: this.props.startAtStep,
title: this.props.steps[0].title,
description: this.props.steps[0].description || null,
};
this.applyValidationFlagsToSteps();
}
// a validation method is each step can be sync or async (Promise based),
// this utility abstracts the wrapper stepMoveAllowed to be Promise driven
// regardless of validation return type
abstractStepMoveAllowedToPromise(movingBack) {
return Promise.resolve(this.stepMoveAllowed(movingBack));
}
applyValidationFlagsToSteps() {
this.props.steps.map((i) => {
i.validated = this.props.dontValidate;
return i;
});
}
checkNavState(currentStep) {
const {steps, nextText, prevBtnOnLastStep, nextTextOnFinalActionStep} = this.props;
if (currentStep > 0 && currentStep !== steps.length - 1) {
this.setState({
showPreviousBtn: true,
nextStepText: nextText,
});
} else if (currentStep === 0) {
this.setState({
showPreviousBtn: false,
nextStepText: nextText,
});
} else {
this.setState({
showPreviousBtn: (prevBtnOnLastStep) ? true : false,
nextStepText: isEmpty(nextTextOnFinalActionStep) ? this.getLocale('finish') : nextTextOnFinalActionStep,
});
}
}
// set the nav state
setNavState(next) {
if (next < this.props.steps.length) {
this.setState({
activeStep: next,
title: this.props.steps[next].title,
description: this.props.steps[next].description || null,
});
}
this.checkNavState(next);
}
// move next via next button
next() {
this.abstractStepMoveAllowedToPromise().then((proceed = true) => {
// validation was a success (promise or sync validation). In it was a
// Promise's resolve() then proceed will be undefined, so make it true.
// Or else 'proceed' will carry the true/false value from sync validation
this.updateStepValidationFlag(proceed);
if (proceed) {
this.setNavState(this.state.activeStep + 1);
}
}).catch((e) => {
if (e) {
// CatchRethrowing: as we wrap StepMoveAllowed() to resolve as a
// Promise, the then() is invoked and the next React Component is
// loaded. ... during the render, if there are JS errors thrown (e.g.
// ReferenceError) it gets swallowed by the Promise library and comes
// in here (catch) ... so we need to rethrow it outside the execution
// stack so it behaves like a notmal JS error (i.e. halts and prints to
// console)
setTimeout(function() {
throw e;
});
}
// Promise based validation was a fail (i.e reject())
this.updateStepValidationFlag(false);
});
}
// move behind via previous button
previous() {
if (this.state.activeStep > 0) {
this.setNavState(this.state.activeStep - 1);
}
}
// update step's validation flag
updateStepValidationFlag(val = true, forceUpdate = false) {
const shouldUpdate = val !== this.props.steps[this.state.activeStep].validated;
this.props.steps[this.state.activeStep].validated = val;
// note: if a step component returns 'undefined' then treat as "true".
if (shouldUpdate && forceUpdate) {
return this.forceUpdate();
}
}
// are we allowed to move forward? via the next button or via jumpToStep?
stepMoveAllowed(skipValidationExecution = false, forward) {
let proceed = false;
if (this.props.dontValidate) {
proceed = true;
} else {
// if its a form component, it should have implemeted a public
// isValidated class. If not then continue
if (typeof this.activeComponent.isValidated == 'undefined') {
proceed = true;
} else if (skipValidationExecution) {
// we are moving backwards in steps, in this case dont validate as it
// means the user is not commiting to "save"
proceed = true;
} else {
// user is moving forward in steps, invoke validation as its available
proceed = this.activeComponent.isValidated(forward);
}
}
return proceed;
}
getLocale = (path) => {
const {formatMessage} = this.context.intl;
return formatMessage({id: 'commons.components.Modal.Wizard.' + path});
};
renderWizardBody = () => {
const compToRender = React.cloneElement(
this.props.steps[this.state.activeStep].component, {
ref: (comp) => {
if (!comp) return;
if (this.props.steps[this.state.activeStep].getCompRef) {
return this.activeComponent = this.props.steps[this.state.activeStep].getCompRef(
comp);
}
return this.activeComponent = comp;
},
validateComponent: (forward) => {
const isValid = this.stepMoveAllowed(false, forward);
this.updateStepValidationFlag(isValid, true);
},
});
return (
<FlexBox className="wizard-body-container" flexDirection="column">
{compToRender}
</FlexBox>
);
};
renderWizardFooter = () => {
const {showPreviousBtn, nextStepText, activeStep} = this.state;
const {steps, prevText, onHide} = this.props;
const primaryBtnText = isEmpty(nextStepText) ? this.getLocale('next') : nextStepText;
const prevBtnText = isEmpty(prevText) ? this.getLocale('previous') : prevText;
const primary = {
key: 'primary-step-button',
text: primaryBtnText,
disabled: !steps[activeStep].validated,
onClick: () => {
this.next();
},
};
const prevBtnProps = {
key: 'secondary-step-button',
text: prevBtnText,
onClick: () => {
this.previous();
},
};
const cancelBtnProps = {
key: 'cancel-button',
semStyle: 'secondary',
semSize: 'small',
text: this.getLocale('cancel'),
onClick: onHide,
};
let extraFooter, secondary;
if (showPreviousBtn) {
secondary = prevBtnProps;
extraFooter = <Button {...cancelBtnProps}/>;
} else {
secondary = cancelBtnProps;
}
return (
<Footer
primary={primary}
secondary={secondary}
extraFooter={extraFooter}
/>
);
};
render() {
const {show, onHide, dialogClassName, closeButton, ...rest} = this.props;
const {description} = this.state;
const tooltipCfg = {
id: 'modal-description-tooltip',
body: <div className="wizard-help-tip">{description}</div>,
placement: 'bottom',
};
return (
<Modal
show={show}
dialogClassName={'material-dialog ' + (dialogClassName || '')}
onHide={onHide}
{...rest}
>
<Header closeButton={closeButton}>
<FlexBox className="header-wrapper" alignItems="center">
<Modal.Title>{this.state.title}</Modal.Title>
{
Boolean(description) && <Tooltip {...tooltipCfg}>
<span className="icon icon-help"/>
</Tooltip>
}
</FlexBox>
</Header>
<Body className="is-wizard">
{this.renderWizardBody()}
</Body>
{this.renderWizardFooter()}
</Modal>
);
}
}
Wizard.contextTypes = {
intl: intlShape,
};
Wizard.propTypes = {
...Header.propTypes,
...Footer.propTypes,
steps: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
description: PropTypes.string,
component: PropTypes.element.isRequired,
getCompRef: PropTypes.func,
})
).isRequired,
startAtStep: PropTypes.number,
dontValidate: PropTypes.bool,
prevBtnOnLastStep: PropTypes.bool,
nextText: PropTypes.string,
prevText: PropTypes.string,
nextTextOnFinalActionStep: PropTypes.string,
};
Wizard.defaultProps = {
startAtStep: 0,
dontValidate: false,
prevBtnOnLastStep: true,
nextText: '',
prevText: '',
nextTextOnFinalActionStep: '',
closeButton: false,
};
export default Wizard;
this are own components:
import Button from 'components/Button';
import Header from './components/Header';
import Body from './components/Body';
import Footer from './components/Footer';
import Tooltip from 'components/Tooltip';
But has nothing to be with the logic of Wizard it self.
Is this feature still a priority? It would definitely be very useful.
This feature would be nice to have.