blob: 1c3044340b5367acb9e5e90accf224db23be4fa9 [file] [log] [blame]
/**
* Copyright 2019 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Directive, ElementRef, HostListener, Injectable, QueryList, ViewChildren} from '@angular/core';
import {NgModel} from '@angular/forms';
import {MatInput} from '@angular/material/input';
import {MatSelect} from '@angular/material/select';
import {UrlTree} from '@angular/router';
import {Observable} from 'rxjs';
import {Notifier} from '../services/notifier';
/**
* A class than can track whether a form has dirty input elements.
* If a page want to trigger UnsavedChangeGuard on navigating away from the
* page, it has to extend this class.
*/
@Directive()
export abstract class FormChangeTracker {
/**
* A form should be consisted of MatInputs, MatSelects and other components
* that extends FormChangeTracker.
*
* For example: setting form has inputs, and name_value_pair_form that
* collectively make up the form.
*/
@ViewChildren(MatInput, {read: ElementRef})
formInputs!: QueryList<ElementRef>;
@ViewChildren(MatSelect, {read: ElementRef})
formSelects!: QueryList<ElementRef>;
@ViewChildren(FormChangeTracker) trackers!: QueryList<FormChangeTracker>;
// Every formInputs will have a cooresponding ngModel
@ViewChildren(NgModel) ngmodels!: QueryList<NgModel>;
/**
* Form, on submit, will check whether invalid inputs exist
*/
invalidInputs: ElementRef[] = [];
/**
* A boolean which records users actions. (e.g, on press add button,
* hasContentChanged should be true)
*/
hasContentChanged = false;
/**
* Check whether the form is dirty by examining it's inputs, and recursively
* examining it's subforms (Component that extends FormChangeTracker).
*/
isFormDirty(): boolean {
return this.hasContentChanged ||
!!this.trackers.find(tracker => tracker.isFormDirty()) ||
!!this.formInputs.find(
input => input.nativeElement.classList.contains('ng-dirty'));
}
findInvalidFields(): ElementRef[] {
let invalidFields: ElementRef[] = [];
// Every control (select, input, textarea) will have ngmodel
if (!this.ngmodels) {
return invalidFields;
}
// When validating form, touch form fields to show error messages
for (const ngmodel of this.ngmodels) {
ngmodel.control.markAsTouched();
}
// Find all invalid fields
invalidFields = this.findInvald(this.formSelects);
return invalidFields.concat(this.findInvald(this.formInputs));
}
private findInvald(list: QueryList<ElementRef>): ElementRef[] {
return list.filter(
item => item.nativeElement.classList.contains('ng-invalid'));
}
getInvalidInputs(): ElementRef[] {
this.invalidInputs = this.findInvalidFields();
return this.invalidInputs;
}
/**
* Reset the form's state to not dirty, it will recursively set the forms
* input and subforms (Component that extends FormChangeTracker) to clean
* state
*/
resetForm() {
for (const input of this.formInputs) {
input.nativeElement.classList.remove('ng-dirty');
}
for (const tracker of this.trackers) {
tracker.resetForm();
}
this.hasContentChanged = false;
}
/**
* Listen to page close and refresh event, if there's unsaved data,
* prompt the user to confirm
* @param $event A beforeUnloadEvent
*/
@HostListener('window:beforeunload', ['$event'])
beforeUnload($event: BeforeUnloadEvent) {
if (this.isFormDirty()) {
$event.returnValue = '';
}
}
}
/**
* A guard that if added to a route, it will do a check before navigating away
* form the page. It's used to detect whether the current page has unsaved data.
* If so, it will prompt user to confirm that they want to navigate away.
* The component for that route must implement ChangeTracker.
*/
@Injectable()
export class UnsavedChangeGuard {
constructor(private readonly notifier: Notifier) {}
canDeactivate(tracker: FormChangeTracker):
Observable<boolean|UrlTree>|Promise<boolean|UrlTree>|boolean|UrlTree {
if (tracker.isFormDirty()) {
return this.notifier.confirm(
'You have unsaved data. Are you sure you want to navigate away?',
'Unsaved changes');
}
return true;
}
}