ReactRouter v4 Prompt - override default alert

前端 未结 3 949
抹茶落季
抹茶落季 2020-12-20 15:09

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

相关标签:
3条回答
  • 2020-12-20 15:38

    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

    0 讨论(0)
  • 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>
    
    0 讨论(0)
  • 2020-12-20 15:54

    Here's a component using hooks to achieve block functionality, the <Prompt.../> component didn't work for me because I wanted to ignore the search on the location.

    import { useEffect, useRef } from 'react';
    import { useHistory } from 'react-router-dom';
    
    interface IProps {
        when: boolean;
        message: string;
    }
    
    export default function RouteLeavingGuard({ when, message }: IProps) {
    
        const history = useHistory();
        const lastPathName = useRef(history.location.pathname);
    
        useEffect(() => {
    
            const unlisten = history.listen(({ pathname }) => lastPathName.current = pathname);
    
            const unblock = history.block(({ pathname }) => {
                if (lastPathName.current !== pathname && when) {
                    return message;
                }
            });
    
            return () => {
                unlisten();
                unblock();
            }
        }, [history, when, message]);
    
        return null;
    
    }
    
    0 讨论(0)
提交回复
热议问题