Search code examples
htmlangularjsgoogle-chromeangular-ui-routeronbeforeunload

What causes the 'beforeunload' event to be triggered in Chrome?


I am adding an event listener 'beforeunload' to check if a form is dirty or not and provide a message to my users that navigating away from the page might cause them to lose unsaved data. Since I am on Chrome, I understand that I am not allowed to set a custom message, but basically that I have to add the event listener to get the default message to appear. The message is indeed appearing when my form is dirty, but then I'm not sure how to reset it other than to reload the page. My question is: what is causing the 'beforeunload' event to be triggered? For me it only fires when my form is dirty, but since I didn't set that up, I'm not sure how to turn it off once the event is triggered.

I have tried removing the event listener when the user confirms they'd like to leave the page, but when I click back to the page and try to reload, the event still fires, even though the form is not dirty and even pristine is set to true.

This is the relevant code in the controller.

        var unloadEvent = function (e) {
            e.returnValue = '';
        };
        window.addEventListener("beforeunload", unloadEvent);

        // For when a user hits the back button 
        $transitions.onBefore({}, function ($transition)
        {
            console.log("on before", vm.ticketForm); 
            if (vm.ticketForm && vm.ticketForm.$dirty == true) {
                var answer = confirm("Are you sure you want to leave this page? Changes you made may not be saved.")
                if (!answer) {
                    // User would like to cancel the click and remain on the page
                    $window.history.forward(); 
                    $transition.abort();
                    return false;
                }
                else {
                    // Form is dirty but user would like to leave the page
                    // Set the form to pristine
                    window.removeEventListener("beforeunload", unloadEvent);
                    vm.ticketForm.$setPristine(); 
                    vm.ticketForm.$setUntouched(); 
                }
            }
        });

        $scope.$on('$destroy', function() {
            window.removeEventListener("beforeunload", unloadEvent); 
        });

This is a portion of the html

 <form name="detailVM.ticketForm">
        <table class="table table-condensed table-striped table-center">
            <tbody>
                <tr>
                    <th>Username</th>
                    <th>Worker Number</th>
                    <th></th>
                    <th>Start Time</th>
                    <th>End Time</th>
                    <th>Adjusted Start Time</th>
                    <th>Adjusted End Time</th>
                    <th></th>
                </tr>

                <tr ng-repeat="worker in detailVM.currentTicket.crew_assignment.workers" ng-if="worker.revised_start_timestamp !== null && worker.revised_end_timestamp !== null">
                    <td>{{::worker.worker_username}}</td>
                    <td>{{::worker.worker_number}}</td>
                    <td><a href="#">Edit Shift</a></td>
                    <td>{{ ::detailVM.formatTime(worker.current_start_timestamp, 'MM/DD/YYYY, h:mm:ss a') }}</td>
                    <td>{{ ::detailVM.formatTime(worker.current_end_timestamp, 'MM/DD/YYYY, h:mm:ss a') }}</td>
                    <td><input type="datetime-local" ng-model="worker.revised_start_timestamp" ng-change="detailVM.changeWorkerStartTime(worker, worker.start_timestamp)"></td>
                    <td><input type="datetime-local" ng-model="worker.revised_end_timestamp" ng-change="detailVM.changeWorkerEndTime(worker, worker.end_timestmap)"></td>
                    <td>
                        <button ng-click="detailVM.deleteWorker(worker)">
                            <span class="fa fa-trash"></span>
                        </button>
                    </td>
                </tr>
                <tr ng-if="detailVM.newWorkerFormShown">
                    <td>
                        <input type="string" ng-model="detailVM.newWorkerName"/>
                    </td>
                    <td>
                        <input type="string" ng-model="detailVM.newWorkerNumber"/>
                    </td>
                    <td>
                        <input type="datetime-local" ng-model="detailVM.newWorkerStartTimestamp"/>
                    </td>
                    <td>
                        <input type="datetime-local" ng-model="detailVM.newWorkerEndTimestamp"/>
                    </td>
                    <td>
                        <button ng-click="detailVM.addWorker()">
                            <span class="fa fa-plus addButton"></span>
                        </button>
                    </td>
                </tr>
            </tbody>
        </table>
</form>

I am unclear on what is causing the event to fire, since it only fires sometimes. As far as I can tell, it only fires after the form is touched, and then continues to fire even when the event listener is removed and then re-added.


Solution

  • I am adding an event listener 'beforeunload' to check if a form is dirty or not and provide a message to my users that navigating away from the page might cause them to l ose unsaved data.

    One way to do that is to create a custom directive to the form directive:

    app.directive("confirmDirtyUnload", function($window) {
        return {
            require: "ngForm", 
            link: postLink,
        };
        function postLink(scope, elem, attrs, ngForm) {
            $window.addEventListener("beforeunload", checkDirty);
            elem.on("$destroy", function (ev) {
                $window.removeEventListener("beforeunload", checkDirty);
            });
            function checkDirty(event) {
                if (ngForm.$dirty) {
                     // Cancel the event as stated by the standard.
                     event.preventDefault();
                     // Chrome requires returnValue to be set.
                     event.returnValue = '';
                };
            }
        }
    });
    

    Usage:

    <form confirm-dirty-unload name="form1">
        <input name="text1" ng.model="data.text1" />
    </form>