I have a page that has a form that checks if the user has unsaved changes before navigating away from it.
The problem is that even with a preventDefault() and return false, the user is still able to click away from the component.
Is there a way to prevent the ngOnDestroy or click event from happening?
Note: User is not going to a different route, just another tab from the same component.
ngOnDestroy() {
if (this.myForm.dirty) {
let save = confirm('You are about to leave the page with unsaved changes. Do you want to continue?');
if (!save) {
window.event.preventDefault();
return false;
}
}
}
You would wanna use CanDeactivate guard.
Here is an example:
1. Create a guard service/provider.
import { Injectable } from '#angular/core';
import { CanDeactivate } from '#angular/router';
import { Observable } from 'rxjs/Observable';
export interface CanComponentDeactivate {
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
#Injectable()
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
canDeactivate(component: CanComponentDeactivate) {
return component.canDeactivate ? component.canDeactivate() : true;
}
}
2. Add your guard service (CanDeactivateGuard) in your app.module providers
providers: [
CanDeactivateGuard,
]
3. Update your routing, to something like this:
{
path: "pipeline/:id",
component: OnePipelineComponent,
canDeactivate: [CanDeactivateGuard],
},
4. Implement canDeactivate method in your component where you want to prevent ngOnDestroy. In my case, it was OnePipelineComponent as mentioned in the route above.
canDeactivate() {
console.log('i am navigating away');
// you logic goes here, whatever that may be
// and it must return either True or False
if (this.user.name !== this.editName) {
return window.confirm('Discard changes?');
}
return true;
}
Note: Follow steps 1 & 2 only once, obviously, & just repeat steps 3 & 4 for every other component where you want the same behaviour, meaning, to
prevent ngOnDestroy or to do something before a
component can be destroyed).
Check out these articles for code sample & an explanation for the code written above. CanDeactivate & CanDeactivate Guard Example
You are mixing two concepts - navigating away means to a new route. The correct angular solution to this is implementing a CanDeactivateGuard. The docs on that are here.
A stack overflow question with answers is here.
In your situation the user is not navigating to a new page (ie. the route does not change). They are simply clicking on a new tab.
Without seeing more code, it's hard to know if both tabs are part of the same form or in two different forms.
But regardless, you need a click handler on the other tab button and in THAT click handler you need to check if the data for the current tab is unsaved (ie. if that tab is one single form, is that form dirty?).
So basically move the code you posted from ngOnDestroy and into that click handler.
Related
I have started learning state management using NGXS. So far everything is fine but have few questions regarding some scenarios like -
If a Mat Dialog box is open (or any div - here I've both the scenarios in my project) and from inside it an API is called, how can I close the dialog only if API returns success?
Suppose a user logs out, how can I reset the states to default values?
For the first case below is my code for the state, action & dispatcher:
abc.action.ts
export class AddExamCategory {
static readonly type = '[ExamCategory] Add';
constructor(public payload: ExamCategory) {}
}
abc.state.ts
export interface ExamCategoryStateModel {
examCategoryList: ExamCategory[];
}
#State<ExamCategoryStateModel>({
name: 'examCategory',
defaults: {
examCategoryList: []
}
})
#Injectable()
export class ExamCategoryState {
constructor(private _adminService: AdminService) {}
#Action(AddExamCategory)
addExamCategory({ getState, patchState }: StateContext<ExamCategoryStateModel>, { payload }: AddExamCategory) {
return this._adminService.createExamCategory(payload).pipe(tap(response => {
patchState({ examCategoryList: [] }); // Want to close the modal/div after this part. If API returns ERROR do not close.
}));
}
}
abc.component.ts
this.store.dispatch(new AddAdminUser({ ...this.adminRegistrationForm.value, password: this.password.value }))
.subscribe(response => {
this.store.dispatch(new GetAdminUsers());
this.dialogRef.close(true)
});
Currently it's like this but it closes no matter what's the status of API.
For the second case, in the service where I have written the logic for logout() I have written like this: this.store.reset({}). Though it's resetting the state but not with the default values in it. I have multiple states to reset on this single logout method.
How to work on these scenarios?
You can add extra property on your state to track the requesting state of your application ('requesting','idle') [you can create extra states as needed to track 'success' and 'error' response from the server]
when dispatch GetAdminUsers set the value of the newely added state to requesting and at GetAdminUsersComplete set the value to idle
subscribe to a selector that's read the state on your ngOnInit and call dialogRef.clse(true) inside of it. like following:
this.store
.pipe(
select(selectors.selectRequestState),
skip(1) //only start tracking after request created
)
.subscribe(result => {
if (result == 'idle')
this.dialogRef.close()
});
example: https://stackblitz.com/edit/angular-ivy-4ofc3q?file=src/app/app.component.html
Reset State
I don't think there is a simple way to reset the state with the store. You need to move through all your state features and implement reset action that set the state to initial state.
the simplest solution is to refresh the browser page after user logout using location.reload();
if you keep the store inside localstorage you need to remove it first then do the reload
the easiest way to accomplish that is with an effect, after you dispatch your action you should be able to trigger other action like "UsserAddedSuccessfully" and read it after that close that modal.
read this question for more detail.
What do I want to achieve
I want to update my current view based on an Id. So say that I have side navigation with the following tabs:
Customer A
Customer B
Customer C
What I want is that the user can click on Customer A and that the current customer view gets updated based on the Customer Id.
What is my problem achieving this
I thought the best way to solve this issue was to navigate to the page and provide the Id directly as follows:
router.navigateToRoute("customer", { currentCustomerId });
Then on the Customer page I am receiving the Id in the activate method as following:
public activate(params) {
this.currrentCustomerId = params.currentCustomerId;
}
Actually, this is working the first time you navigate to a customer. But when I am clicking on another Customer page, the view does not get updated because the activate method does not get triggered for a second time. It is only working if I navigate to another page (not customer page) and go back or simply refresh the whole page.
So what can I use to achieve what I want? I reckon that I have to use something else than activate()?
I appreciate it if someone could give me some insight into this issue.
Regards.
This is due to the default activation strategy wherein, if the URL only changes in terms of a parameter value, the component is reused and hooks are not invoked.
To obtain the desired behavior, you can customize the this behavior at the component level or the route level.
At the component level:
import {activationStrategy} from 'aurelia-router';
export class CustomerComponent {
determineActivationStrategy() {
return activationStrategy.replace;
}
activate(params: {currrentCustomerId: string}) {
this.currentCustomerId = params.currentCustomerId;
}
}
At the route level:
import {Router, RouterConfiguration} from 'aurelia-router';
export class App {
configureRouter(config: RouterConfiguration, router: Router) {
config.map([{
name: 'customer',
moduleId: './customer',
route: 'customer/:currentCustomerId',
activationStrategy: 'replace'
}]);
this.router = router;
}
}
Im searching the best way to show/hide components on my angular application using the current location path like, homepage, loginpage, logoutpage, etc.
I'm suscribed to the router events that gives me the current path, so If Im in the login page I should hide the "navbar-component" and if I'm in the home page I should show it.
This approach should work with different components in different current pages. So I was thinking in this method inside of a *ngIf:
app.component.html
<nav *ngIf="myService.isComponentPartOfTheCurrentPage('navbar')">
...some navigation buttons here
</nav>
myService.ts
isComponentPartOfTheCurrentPage(componentName: string): boolean {
const url = getCurrentPath(); // This works fine
return currenPathContainsThisComponent(componentName, url); // This is gonna return true or false.
}
The main problem with this approach is that the angular cycling is calling this function a lot of times. Also I've read some blogs that not recommend this king of things.
Is there a better way to accomplish this?
In the subscription to your route events update a behavior subject on the service with the current
section$ = new BehaviorSubject(null);
in your route sub
sections$.next(section);
then in your component you listen to the behaviour subject with the async pipe
section$ = this.service.section$;
and in the template
<nav *ngIf="section$ | async as section">
<div *ngIf="section === 'navbar'">Something</div>
</nav>
The section variable will imagically update each time the behavior subject emits.
You could watch for router events, specifically NavigationEnd, assign the current route to an observable and then do the checks in the components for what route we are currently on.
Service:
import { Router, NavigationEnd } from "#angular/router";
import { filter, map } from "rxjs/operators";
currentRoute$: Observable<string>;
constructor(private router: Router) {
this.currentRoute$ = router.events.pipe(
filter(e => e instanceof NavigationEnd),
map((e: NavigationEnd) => e.url)
);
}
Then listen to the observable using async pipe and do the checks
*ngIf="(myService.currentRoute$ | async) === '/login'"
You can also perhaps use includes if you need to check that if that url fragment is included in url, you can then do:
*ngIf="(myService.currentRoute$ | async).includes('login')
I'm making a pop-up component that I want to use in several of my other components, so I made a popup.service that enable the component to be loaded through *ngIf inside other components. This is creating a problem for me since the PopupComponent is a separate entity and I'm unsure how to pass data from the child component(PopupComponent) to its respective parents.
Atm the loading looks like this in ParentComponent.ts:
public openPopup(order_id: string, invoice_id: string): void{
this.load_popup=this.popupService.openPopup(order_id, "selected_order", invoice_id, "selected_invoice");
}
And ParentComponent.html:
<app-popup *ngIf="load_popup"></app-popup>
And it loads like a charm, the problem is in closing it. The close button is located on the PopupComponent, is there an efficient way to have the Child Component (PopupComponent) to affect a variable in the Parent Component ie. ParentComponent.load_popup=false?
My other thought was dynamically loading the component, however I have no idea on how to do that. I was fidgeting around with using the PopupService and putting something like this in it:
import { Injectable, ComponentRef } from '#angular/core';
import {PopupComponent} from '../popup/popup.component';
#Injectable({
providedIn: 'root'
})
export class PopupService {
popup_ref: ComponentRef<PopupComponent>
constructor(
) { }
//Implemented in orderoverviewcomponent, invoicecomponent, and placeordercomponent
public openPopup(id1:string, storage_label1:string, id2:string, storage_label2:string): Boolean{
if (id1){
localStorage.setItem(storage_label1, JSON.stringify(id1));
}
if (id2){
localStorage.setItem(storage_label2, JSON.stringify(id2));
}
this.popup_ref.initiate(); //this line is a made up example of loading the component
return true;
}
public closePopup(storage_label1: string, storage_label2:string): Boolean{
if(storage_label1){
localStorage.removeItem(storage_label1);
}
if(storage_label2){
localStorage.removeItem(storage_label2);
}
this.popup_ref.destroy();
return false;
}
}
Where this.popup_ref.destroy(); would ideally destroy PopupComponent, but when I did that I got a "cannot read property of undefined" on the popup_ref, I'm having trouble declaring it, the syntax seems a bit tricky.
The problem also remains that i need a function to load the component, the opposite of .destroy(), if this is possible I would much prefer it over loading and destroying with *ngIf.
Edit: Partially solved it by just using a boolean in the service as the trigger for *ngIf, is there a way to do a function load and destroy on a component still?
You can bind an EventEmitter() to your component to invoke a function in the parent component.
<app-popup [onClose]="load_popup = false" *ngIf="load_popup"></app-popup>
Then inside of your app-popup component:
#Output onClose = new EventEmitter();
public closePopup(/* code emitted for brevity */) {
/* code emitted for brevity */
this.onClose.emit(); //Call the parent function (in this case: 'load_popup = false')
}
It's important to know that you can pass entire functions to the bound function, and you can even pass variables back to the parent from the child:
[onClose]="myFunction($event)"
this.onClose.emit(DATA HERE);
As an aside, since you're using Angular; I would suggest looking into using Modals for popup dialogue boxes. You can see a good example here:
https://ng-bootstrap.github.io/#/components/modal/examples
I have a situation in Angular 4.0.3 where I have two <router-outlet>'s on a page.
<router-outlet></router-outlet>
<router-outlet name="nav"></router-outlet>
The first outlet will accept routes for content, and the second will accept routes for navigation. I achieve the navigation using this;
router.navigate(['', {outlets: { nav: [route] } }],{skipLocationChange: true });
This changes the outlet's contents without updating the URL - since I don't want any url that look like .. (nav:user).
The problem is the remaining outlet. I do want the URL to update when those are clicked, for instance ...
.../user/profile
Functionally, I can get both outlets to have the proper content, but it keeps appending the nav outlet's route to the url, like this ...
.../user/profile(nav:user)
Is there any way I can stop it from adding the (nav:user) part?
Unless there is some trick I'm not aware of ... I don't think you can. The address bar is what maintains the route state. So without the secondary outlet information in the address bar, the router won't know how to keep the correct routed component in the secondary outlet.
You could try overriding the navigateByUrl method as shown here: http://plnkr.co/edit/78Hp5OcEzN1jj2N20XHT?p=preview
export class AppModule { constructor(router: Router) {
const navigateByUrl = router.navigateByUrl;
router.navigateByUrl = function(url: string|UrlTree, extras: NavigationExtras = {skipLocationChange: false}): Promise<boolean> {
return navigateByUrl.call(router, url, { ...extras, skipLocationChange: true });
} } }
You could potentially add logic here then to check which routes you need to do this on.