SaltyCrane Blog — Notes on JavaScript and web development

A chained modals example w/ React Router (part 2)

This is an example using React and React Router to create a sequence of chained modals. In part 1, I made a basic example where clicking a "Next" button advanced to the next modal in a list.

This example supports going back and forward using browser navigation and linking to a specific modal's URL. Additionally, each modal has a form input. On clicking the "Next" button, an AJAX request is made to save the data. On failure an error is displayed. On success the next modal is shown. During the request a spinner is shown. The full code is here and a demo is here.

App.js

RoutedApp is the top level component that configures the routes for the app. Each route has an associated component. Like the previous example, the modals are children of ChainedModals which is a child of App. However now each modal is explicitly declared in the element tree and React Router decides which modal to render based on the route. For example, navigating to the /name route renders ModalName as a child of ChainedModals as a child of App. In the App component, children is set to the ChainedModals element. In the ChainedModals component, children is set to either the ModalName or ModalPhone element depending on the route.

Instead of passing modal components to ChainedModals, a list of routes to the modals is passed in. The utility function partial allows me to create a component that is equivalent to ChainedModals with the modalList prop preset.

This uses ES'15 arrow functions, argument destructuring and JSX spread. [App.js on github]

const RoutedApp = () => (
  <Router history={hashHistory}>
    <Route component={App}>
      <Route path="/" component={
        partial(ChainedModals, {modalList: ['/name', '/phone', '/done']})}>
        <Route path="/name" component={ModalName} />
        <Route path="/phone" component={ModalPhone} />
        <IndexRedirect to="/name" />
      </Route>
      <Route path="/done" />
    </Route>
  </Router>
);

const App = ({ children }) => (
  <div>
    <PageBehindModals />
    {children}
  </div>
);

const partial = (Comp, props) => (fprops) => <Comp {...props} {...fprops} />;
ChainedModals.js

ChainedModals is a component that manages its child modal components. Unlike the previous example, the modal to be shown is determined by the current route. Before the component is rendered, the index of the current modal is determined by finding the current route in the modal list. This index is stored in the state. When the modal's "Next" button is clicked, _gotoNext determines the next route to be displayed and changes the route using hashHistory.push(). React.cloneElement is used to pass props to the child modal as shown in the React Router examples. This uses ES'15 nested destructuring and classes, and ES'17 class properties. [ChainedModals.js on github]

class ChainedModals extends Component {
  render() {
    const { children } = this.props;
    const { currIndex } = this.state;

    // Clone the child view element so we can pass props to it.
    const modalElement = children && React.cloneElement(children, {
      step: currIndex + 1,
      gotoNext: this._gotoNext,
      backdrop: false,
      show: true,
    });

    return (
      <div>
        <ModalBackdrop />
        {modalElement}
      </div>
    );
  }

  componentWillMount() {
    this._setIndexFromRoute(this.props);
  }

  componentWillReceiveProps(nextProps) {
    this._setIndexFromRoute(nextProps);
  }

  _setIndexFromRoute(props) {
    const { modalList, location: { pathname } } = props;
    const index = modalList.findIndex(path => path === pathname);
    this.setState({currIndex: index});
  }

  _gotoNext = () => {
    const { modalList } = this.props;
    const { currIndex } = this.state;
    const nextRoute = modalList[currIndex + 1];
    hashHistory.push(nextRoute);
  };
}
ModalName.js

ModalName is one of the modal components in the list. The _handleClickNext method makes a fake AJAX request using the request simulator function. request returns a Promise. On success, it calls gotoNext (which is passed in as a prop) to go to the next modal. The component state is used to show a spinner during the AJAX request (isRequesting) and to show validation errors (errorMsg). This uses ES'15 destructuring, classes, and promises, and ES'17 rest/spread and class properties. [ModalName.js on github]

class ModalName extends Component {
  state = {
    isRequesting: false,
    errorMsg: null
  };

  render() {
    const { step, ...props } = this.props;
    const { isRequesting, errorMsg } = this.state;

    return (
      <Modal {...props}>
        <Modal.Header closeButton>
          <Modal.Title>Step {step} - Name</Modal.Title>
        </Modal.Header>
        <Modal.Body>
          {isRequesting && <p><em>Making fake ajax request...</em></p>}
          {errorMsg && <p><em>{errorMsg}</em></p>}
          <Input
            label="Enter your name"
            type="text"
            bsSize="large"
            {...(errorMsg ? {bsStyle: 'error'} : {})}
            ref={(c) => this._input = c}
          />
        </Modal.Body>
        <Modal.Footer>
          <Button bsStyle="primary" onClick={this._handleClickNext}>Next</Button>
        </Modal.Footer>
      </Modal>
    );
  }

  _handleClickNext = () => {
    const { gotoNext } = this.props;
    const name = this._input.getValue();

    this.setState({isRequesting: true, errorMsg: null});
    request('/api/name', name)
      .then(() => {
        gotoNext();
      })
      .catch((error) => {
        this.setState({isRequesting: false, errorMsg: error});
      });
  };
}

Comments