Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ReactRouter v4 Prompt - override default alert

The React Router v4 <Prompt></Prompt> component is perfect for the use case of protecting navigation away from a partially filled out form.

But what if we want to supply our own logic in place of the default browser alert() that this component uses? React is intended for creating UIs, so it seems like a pretty reasonable use case. Digging through the issues on Prompt in the github I did not find anyone asking about this.

Does anyone know of a solution for providing custom behavior for the alert?

like image 890
Daniel Thompson Avatar asked Sep 21 '18 05:09

Daniel Thompson


Video Answer


2 Answers

Although you can make use of a custom Modal component while preventing navigating between pages through Links, you can't show a custom modal while trying to close browser or reload it.

However if thats fine with you, you can make use of history.listen to and block navigation. I wrote a generic HOC for it which solves this use case.

In the below code whitelisted pathnames are the pathnames that you would want the other person to navigate to without showing the prompt

import React from 'react';
import { withRouter } from 'react-router';
import _ from 'lodash';

const navigationPromptFactory = ({ Prompt }) => {
    const initialState = {
        currentLocation: null,
        targetLocation: null,
        isOpen: false
    };

    class NavigationPrompt extends React.Component {
        static defaultProps = {
            when: true
        };

        state = initialState;

        componentDidMount() {
            this.block(this.props);
            window.addEventListener('beforeunload', this.onBeforeUnload);
        }

        componentWillReceiveProps(nextProps) {
            const {
                when: nextWhen,
                history: nextHistory,
                whiteListedPathnames: nextWhiteListedPaths
            } = nextProps;
            const { when, history, whiteListedPathnames } = this.props;
            if (
                when !== nextWhen ||
                !_.isEqual(nextHistory.location, history.location) ||
                !_.isEqual(whiteListedPathnames, nextWhiteListedPaths)
            ) {
                this.unblock();
                this.block(nextProps);
            }
        }

        componentWillUnmount() {
            this.unblock();
            window.removeEventListener('beforeunload', this.onBeforeUnload);
        }

        onBeforeUnload = e => {
            const { when } = this.props;

            // we can't override an onBeforeUnload dialog
            // eslint-disable-next-line
            // https://stackoverflow.com/questions/276660/how-can-i-override-the-onbeforeunload-dialog-and-replace-it-with-my-own

            if (when) {
                // support for custom message is no longer there
                // https://www.chromestatus.com/feature/5349061406228480
                // eslint-disable-next-line
                // https://stackoverflow.com/questions/38879742/is-it-possible-to-display-a-custom-message-in-the-beforeunload-popup

                // setting e.returnValue = "false" to show prompt, reference below
                //https://github.com/electron/electron/issues/2481
                e.returnValue = 'false';
            }
        };

        block = props => {
            const {
                history,
                when,
                whiteListedPathnames = [],
                searchQueryCheck = false
            } = props;
            this.unblock = history.block(targetLocation => {
                const hasPathnameChanged =
                    history.location.pathname !== targetLocation.pathname;
                const hasSearchQueryChanged =
                    history.location.search !== targetLocation.search;
                const hasUrlChanged = searchQueryCheck
                    ? hasPathnameChanged || hasSearchQueryChanged
                    : hasPathnameChanged;
                const isTargetWhiteListed = whiteListedPathnames.includes(
                    targetLocation.pathname
                );
                const hasChanged =
                    when && hasUrlChanged && !isTargetWhiteListed;
                if (hasChanged) {
                    this.setState({
                        currentLocation: history.location,
                        targetLocation,
                        isOpen: true
                    });
                }
                return !hasChanged;
            });
        };

        onConfirm = () => {
            const { history } = this.props;
            const { currentLocation, targetLocation } = this.state;
            this.unblock();
            // replacing current location and then pushing navigates to the target otherwise not
            // this is needed when the user tries to change the url manually
            history.replace(currentLocation);
            history.push(targetLocation);
            this.setState(initialState);
        };

        onCancel = () => {
            const { currentLocation } = this.state;
            this.setState(initialState);
            // Replacing the current location in case the user tried to change the url manually
            this.unblock();
            this.props.history.replace(currentLocation);
            this.block(this.props);
        };

        render() {
            return (
                <Prompt
                    {...this.props}
                    isOpen={this.state.isOpen}
                    onCancel={this.onCancel}
                    onConfirm={this.onConfirm}
                />
            );
        }
    }

    return withRouter(NavigationPrompt);
};

export { navigationPromptFactory };

In order to use the above, you can simply provide your custom Prompt Modal like

      const NavigationPrompt = navigationPromptFactory({
           Prompt: AlertDialog
      });
      const whiteListedPathnames = [`${match.url}/abc`, match.url];

       <NavigationPrompt
                when={isEditingPlan}
                cancelLabel={'Stay'}
                confirmLabel={'Leave'}
                whiteListedPathnames={whiteListedPathnames}
                title={'Leave This Page'}
            >
                <span>
                    Unsaved Changes may not be saved
                </span>
      </NavigationPrompt>
like image 109
Shubham Khatri Avatar answered Oct 18 '22 06:10

Shubham Khatri


The prompt component by default doesn't allow overriding the use of window.alert().

Here's a link to a conversation that matches your needs fairly similarly:

https://github.com/ReactTraining/react-router/issues/4635

There's a few key points in there that you can refer to, mostly just that instead of using prompt you can just make your own modal to be triggered on specific user actions. :)

Hope this helps

like image 27
Tom Rowe Avatar answered Oct 18 '22 05:10

Tom Rowe