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}); }); }; }
Related posts
- How to use ast-grep with GraphQL — posted 2024-09-24
- Next.js App Router (RSC) projects w/ open source code — posted 2024-07-30
- Next.js Relay GraphQL Pokemon example — posted 2024-05-22
- Example Node.js Passport.js SAML app using OneLogin — posted 2024-05-10
- Aphrodite to CSS Modules codemod — posted 2022-12-09
- Simple codemod example with jscodeshift — posted 2021-05-03