react-stepzilla icon indicating copy to clipboard operation
react-stepzilla copied to clipboard

Next Disabled should be disabled until the validation has passed

Open jorgecuesta opened this issue 9 years ago • 8 comments

Hi @newbreedofgeek can you add a way to make that Next Button be disabled before component report a valid state?

jorgecuesta avatar Jan 31 '17 23:01 jorgecuesta

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.

newbreedofgeek avatar Feb 07 '17 00:02 newbreedofgeek

This should be high priority. As it breaks when it's promise based!

newbreedofgeek avatar Jun 05 '17 23:06 newbreedofgeek

@jorgecuesta is the bug fixed?

zeel avatar Dec 19 '17 06:12 zeel

No sorry was a miss click. I reopen it.

jorgecuesta avatar Dec 20 '17 19:12 jorgecuesta

@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.

agent3bood avatar Feb 18 '18 16:02 agent3bood

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.

jorgecuesta avatar Nov 16 '18 23:11 jorgecuesta

Is this feature still a priority? It would definitely be very useful.

patorjk avatar Apr 24 '19 15:04 patorjk

This feature would be nice to have.

sekharsr avatar Jun 13 '22 08:06 sekharsr