Czołem! W dzisiejszym artykule pokażę, jak skorzystać z guarda o nazwie CanDeactivate.
Scenariusz, który rozpatrzę:
- użytkownik wypełnia widok z formularzem, np. edytuje dane usera
- w przypadku próby zmiany routa (np. kliknięcie w menu) pokaże się modal, w którym user zostanie zapytany, czy chce opuścić widok bez zachowania zmian
- modal pokaże się w przypadku, kiedy formularz jest dirty
- submit button również będzie przekierowywać routa, ale nie będzie nigdy wywoływać modala
Z czego skorzystam:
- modal z biblioteki ng-bootstrap
- klasa canDeactivate
Efekt:
No to wio ;)!
ngBootstrap modal
Najpierw musimy pobrać bibliotekę ngBootstrap. Proces instalacji w linku poniżej:
https://ng-bootstrap.github.io/#/getting-started
Po szybkiej instalacji poprzez NPM, importujemy moduł NgbModule w pliku app.module.ts
Ok, mamy teraz w zanadrzu dostęp do gotowego modala, którego pokażemy użytkownikowi.
CanDeactivate i CanActivate Guard
Warto wiedzieć, jaka jest różnica pomiędzy poniższymi Guardami:
- CanDeactivate Guard – sprawdza, czy mamy pozwolenie na opuszczenie danego routa (route to np. localhost:4200/myNgApp/users/12)
- CanActivate Guard – sprawdza, czy mamy pozwolenie na wejście do danego routa
Brzmi banalnie. Skupmy się na pierwszym.
CanDeactivate Guard jest interfejsem z jedną metodą – canDeactivate, która może zwrócić:
1 2 3 |
boolean Promise<boolean> Observable<boolean> |
Jeśli Guard zwróci true – opuszczamy widok, jeśli false – zostajemy na danym widoku.
Oraz przyjmuje cztery parametry (czwarty opcjonalny):
1 2 3 4 |
component: MyComponent, currentRoute: ActivatedRouteSnapshot, currentState: RouterStateSnapshot, nextState?: RouterStateSnapshot |
Parametr component, wskazuje na klasę komponentu, w którym będziemy będziemy chcieli skorzystać z Guarda. Jeśli chcesz poczytać o pozostałych parametrach, musisz zajrzeć do dokumentacji, nie są tematem artykułu.
Skoro już wiemy, do czego służy interfejs CanDeactivate, to możemy przejść do kodowania.
CanDeactivate Guard – implementacja
Modala pytającego, chcę wywołać w następującym przypadku:
- User form jest „dirty”, czyli user zaczął coś robić z formularzem
- wywołanie zmiany routa nie zostało wywołane kliknięciem buttona submit (użyję do tego flagi)
Zaczniemy od zakodowania klasy Guarda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
@Injectable() export class UserDetailsCanDeactivateGuard implements CanDeactivate<UserDetailsComponent> { private discardSubject : Subject<boolean> = new Subject<boolean>(); discard() : void { this.discardSubject.next(true); } keep() : void { this.discardSubject.next(false); } canDeactivate(component : UserDetailsComponent, currentRoute : ActivatedRouteSnapshot, currentState : RouterStateSnapshot) : Observable<boolean> | boolean { if (component.userForm.dirty && component.discardModalFlag) { component.openDiscardChangesModal(); return this.discardSubject.asObservable().first(); } return true; } } |
Nie będę wdawać się w szczegóły biblioteki RxJS i czym jest Subject, bo jest to bardziej złożony temat. Korzystam z Subjectu, aby móc emitować wartość true lub false i nasłuchiwać na wysłaną wartość:
1 |
private discardSubject : Subject<boolean> = new Subject<boolean>(); |
Następnie przygotowuję dwie metody:
- discard() – zostanie wywołana, jeśli w modalu użytkownik kliknie „discard” button, czyli stwierdzi, że chce opuścić widok bez zachowania zmian. Wyemituję w niej wartość true, aby móc zmienić routa
- keep() – metodę keep chce wywołać w 3 przypadkach:
1. Użytkownik kliknie na tło modala
2. Użytkownik zamknie modal poprzez close button „x”
3. Użytkownik kliknie keep w modalu
Zatem w keep wyemituję false, aby zostać w danym roucie (widoku).
Następnie w metodzie canDeactivate, dodaję warunek po spełnieniu którego, ma się pokazać modal:
1 2 3 4 |
if (component.userForm.dirty && component.discardModalFlag) { component.openDiscardChangesModal(); return this.discardSubject.asObservable().first(); } |
Subject jest jednocześnie Observerem i Observablem. W tym miejscu nasłuchuję na wyemitowaną wartość i ją zwracam. Będzie true – użytkownik opuści widok, jak false – to zostanie w widoku z formularzem.
Teraz musimy zarejestrować naszego guarda w Providers w odpowiednim module i dodać go do wybranej ścieżki w routingu:
1 2 3 4 5 6 7 8 9 10 |
@NgModule({ declarations: [...], imports: [ ... NgbModule.forRoot() ], providers: [UserDetailsCanDeactivateGuard], bootstrap: [AppComponent] }) export class AppModule { } |
App.routing.module.ts:
1 2 3 4 5 6 7 8 |
const routes: Routes = [ ... { path: 'users/:id', component: UserDetailsComponent, canDeactivate: [UserDetailsCanDeactivateGuard] } ]; |
Teraz czas wstrzyknąć guarda do klasy komponentu.
Klasa komponentu z detalami usera
Kod klasy, z pominięciem nieistotnych linii:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
@Component(...) export class UserDetailsComponent implements OnInit { @ViewChild("discardModal") discardModal; user = { name: 'John', surName: 'Doe' }; userForm : FormGroup; discardModalFlag = true; discardModalRef; constructor(private router : Router, private formBuilder : FormBuilder, private userDetailsCanDeactivateGuard : UserDetailsCanDeactivateGuard, private modalService: NgbModal) { } ... openDiscardModal() { this.discardModalRef = this.modalService.open(this.discardModal); this.discardModalRef.result.then( () => this.userDetailsCanDeactivateGuard.keep(), // close modal callback () => this.userDetailsCanDeactivateGuard.keep() // dismiss modal callback ); } discardChanges() : void { this.userDetailsCanDeactivateGuard.discard(); this.discardModalRef.close(); } backToList() : Promise<boolean> { return this.router.navigate(['/users']); } save() : void { this.discardModalFlag = false; this.backToList(); } ... } |
Kroki istotne do wywołania modala:
- Wstrzykujemy klasę z Guardem do konstruktora
- Przygotowuję pole discardModalFlag, które pozwoli nam nie wołać Guarda w przypadku kliknięcia submit buttona, który również zmienia routa
- W metodzie openDiscardModal(), discardModalRef.result zwraca mi promise. Resolve dotyczy akcji closeModal (kliknięcie close button), natomiast Reject dotyczy akcji dismissModal (np. kliknięcie w tło modala, lub guzika ESCAPE). W obu przypadkach chcę zostać na widoku, więc wywołam 2x metodę Keep()
- Metodę discardChanges() przypisuję do buttona „discard” w modalu
Jeśli chcesz poznać szczegóły działania modala z biblioteki ngBootstrap, to zapraszam do dokumentacji, a jeśli nie, to musi starczyć Ci powyższe info;)
Tadam! to wszystko. Kodu HTML nie będę wklejać, możesz go obejrzeć w repo.
Całość kodu w poniższym repozytorium:
https://github.com/angularlove/ng-guards/tree/master/src/app
PODSUMOWANIE
Genialna sprawa ten Guard! nie trzeba ręcznie obserwować wszystkich możliwych elementów, które uruchamiają routing. Guard sam wie, że próba zmiany routa została wywołana. Dodatkowo można to zrobić bardziej generycznie i mieć jednego canDeactivate Guarda do obsługi powtarzających się formularzy (a z modala zrobić reużywalny komponent). Z pewnością istnieje wiele scenariuszy, w których przyda się can Deactivate guard.
Fajnie to wygląda. Coś byś dzisiaj (2020 rok, angular 9-10) zmienił?