'Modal should appear whn user navigates away from notes without saving it - react.js

I've a custom notes and Save notes button as shown in below screen shot: If the user navigates away from notes tab without saving the notes, a confirmation modal should appear.
Screen shot:
notes Code is as given below:

import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { updateActionNotes } from "../../redux/actions/editActionNotes";
import { Modal } from "@zambezi/sdk/modal";
import { Div, Span, Img, Button } from "../styled";
import styles from "../../components/action/action-notes.module.css";
import { useActionEditNotesPut } from "../../hooks/api/actions/useActionEditNotesPut";
import { ActionNotesDiv } from "./action-notes.styled";
import RedAlert from "../../assets/incomplete alert-red.png";

const ActionNotes = (props) => {
  const dispatch = useDispatch();
  const { action_id, notes } = props;
  const [notesValue, setNotesValue] = useState(notes);
  const editingActionDataProposed = useSelector((state) =>
    state.proposedActionItemList.actionItemList?.find(
      (el) => el.action_id === action_id
    )
  );
  const editingActionDataPlanned = useSelector((state) =>
    state.plannedActions.activeActions?.find((el) => el.action_id === action_id)
  );
  const editingAction = editingActionDataProposed
    ? editingActionDataProposed
    : editingActionDataPlanned;
  const editedActionNotes = useSelector((state) =>
    state.editActionNotes?.editedNotesList?.find(
      (el) => el?.action_id === action_id
    )
  );
  // For modal const [alertModalPopup, setAlertModalPopup] = useState(false);
  const handleRandomClick = (event) => {
    event.preventDefault();
    setAlertModalPopup(true);
  };
  const handleModalCancel = () => {
    setAlertModalPopup(false);
    onCancel && onCancel();
    alert("Run");
  };
  const onCancel = () => {};
  const renderAlertModalPopup = (title, titleId, closeButton) => {
    return <>{closeButton}</>;
  };
  const { mutate, isLoading } = useActionEditNotesPut({
    onError: () => {
      // TODO: handle error
      //   dispatch(updateActionNotes(undefined));
    },
    onSuccess: () => {
      dispatch(updateActionNotes({ action_id: action_id, notes: notesValue }));
    },
  });
  const handleChange = (event) => {
    setNotesValue(event.target.value);
  };
  // const handleClose = () => {
  //   setAlertModalPopup(false);
  // };
  return (
    <ActionNotesDiv onClick={handleRandomClick}>
      <section className="notes">
        <div className="title">Notes</div>
        <div className="sub-title">
          Keep any notes you have about this Action here.
        </div>
        <textarea
          defaultValue={
            editedActionNotes ? editedActionNotes.notes : notesValue
          }
          placeholder="Type notes here describing the progress you've made.&#10;What you're going to do next or any other important &#10;information..."
          rows="25"
          className="textareacss"
          onChange={handleChange}
        />
        <div className="actions">
          <button
            type="button"
            className="save"
            onClick={() => {
              mutate({ ...editingAction, notes: notesValue });
            }}
            disabled={isLoading}
          >
            Save notes
          </button>
        </div>
      </section>
      <Modal
        open={alertModalPopup}
        renderHeader={renderAlertModalPopup}
        withSectioning={false}
        className={`${styles.actionNotesPopup}`}
      >
        <Div width="100%">
          <Div id="msgBody" marginTop="18px" alignItems="">
            <div className="modalHeader">
              <Img src={RedAlert} width="24px" height="24px" />
            </div>
            <div className="modalHeader">
              <Span fontSize="18px" lineHeight="24px" fontWeight="700">
                Are you sure you want to leave your notes?
              </Span>
            </div>
            <div className="modalHeader">
              <span>Save the notes you've added before you go so they</span>
            </div>
            <div className="modalHeaderOne">
              <span> aren't lost </span>
            </div>
            <Div flex="flex" display="flex" marginTop="15px">
              <button id="btnVisitResourceCenter"> Leave </button>
              <button id="btnClose" onClick={handleModalCancel}>
                Stay to save notes
              </button>
            </Div>
          </Div>
        </Div>
      </Modal>
    </ActionNotesDiv>
  );
};
export default ActionNotes;

This code deals with Navigation:

import React from "react";
import ActionDetail from "./action-detail";
import ActionConsiderations from "./action-considerations";
import ActionProductsServices from "./action-products-services";
import ActionResourceServices from "./action-resource-services";
import ActionNotes from "./action-notes";

import { Div } from "../styled";
import { ActionMoreInfoDiv } from "./action-more-info.styled";

const ActionMoreInfo = (props) => {
  return (
    <ActionMoreInfoDiv>
      <Div padding="0px 30px">
        <Div id="Details" padding="0px" width="846px">
          <ActionDetail {...props} />
        </Div>
        <Div id="Considerations" display="none" padding="0px">
          <ActionConsiderations {...props} />
        </Div>
        <Div id="Notes" display="none" padding="0px">
          <ActionNotes {...props} />
        </Div>
        <Div id="Resources" display="none" padding="0px">
        <ActionResourceServices action_id={1} resourceIdList={props.item['resourceIdList']}/>
        </Div>
        <Div id="Products and services" display="none" padding="0px">
        <ActionProductsServices action_id={1} productIdList={props.item['productIdList']} />
        </Div>
      </Div>
    </ActionMoreInfoDiv>
  );
};

export default ActionMoreInfo;

How can I do that? Should I use component.unmount or check naviagtion?



Solution 1:[1]

The code seems a bit of a nightmare - not proper structure, formatting. So I will rely on a basic explanation with any examples that are not directly taken for your code. Hopefully you can use it in your case.

Recall: So, you have an areafield and panels that trigger different 'page' layout based on panel clicked. And you wish to open a modal whenever the areafield value has not been saved.

There are multiple ways to do this.

1st the simplest: Check whenever a value of your areafield changes via onChange. Once it changes - set a new state for variable like edited to true const [edited, setEdited] = useState(false). Then, whenever you click on any other panel navigation - do a check if edited. If is true then instead of navigating to another panel - open modal that warns for unsaved changes. Modal contains buttons like, for exmaple cancel - that just closes the modal, and Go or discard unsaved that closes the modal AND navigates the user to new panel.

Its simple and quick to do, but an issue exist: if a user adds 1 letter and then deleted it then the edited will be true hence prompting the modal regardless of the fact that the user removed the new changes (deleted that 1 new letter).

2nd way: Instead of having edited field - on panel navigation click check if the current areafield value is the same as initial value. If it is then navigate. if not - open the modal. This removes the need of edited state. But it relies if the initial value exist and you have to store, if you havent already, a current field value in a state. That could cause more re-renders then worth.

But both have an issue - its hard to scale. Most likely your navigation is in another component and does not re-render when the panel re-renders. While this is a good approach, this also means you can not directly interact with how the navigation works. For that you would need to update the panel navigation component and include some sort of redux state, or a custom hook, that would allow you to block the navigation from any component.

For redux: could have like setBlockNavigation method that in any component, like this, you could set it to true if edited is true (for 1st option) or if values are different (2nd option). Then in navigation component navigation will only be executed when blockNavigation is false like !blockNavigation && navigate('anotherpanel'). BUT this is dirty and relys on constant re-renders, state changes, etc. Could cause issues.

BETTER IMO idea: custom hook. Create a custom hook for navigation that returns a method like onPathChange that calls out whenever you click any navigation link. The method accepts return values true or false (boolean). If it recieves true then it navigates. If false then navigation gets canceled. like onPathChange() && navigate('anotherPanel'). By default the onPathChange always returns true. Then you can use this hook in your Notes panel to like:

onPathChange() {
    return !edited
}

So if edited is true then it gets inverted and returns false hence stopping the navigation. And form on so - you can control the modal from Notes panel component. This is scalable with nay navigation from any component. But adds some complexity.

But sadly without having more of your code nor spending hours trying to untangle your code - cant provide exact code of how to do so with your code base. Hopefully this answer provided of how this can be achieved and you can implement it successfully in your codebase.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Lith