DEV Community

Stefanos Kouroupis
Stefanos Kouroupis

Posted on • Edited on

Angular: display a warning and prevent navigation when model is dirty

Angular is one of my favourite frameworks. I know there are a lot haters out there, as well as many lovers, for good reasons. When it comes to enterprise applications, its where it shines.

Today I decided to write on how you can create a component that gets dynamically inserted into the dom and prevent navigation, if and when your view's model is dirty.

This technique is exceptionally handy, especially when you build an application with a lot of forms.

What we try to achieve is basically, if the form has changes (either through creating or editing a form) and the user accidently tries to navigate away, give him a choice.

  • to go back and save his changes
  • or accept that his changes will be lost and navigate away

This is the interface that our routed component needs to implement

  • getRef() returns the component's container reference
  • isDirty() contains our bespoke comparison logic
interface IDirty {
    isDirty(): boolean;
    getRef(): ViewContainerRef;
}
Enter fullscreen mode Exit fullscreen mode

This is the guard service that implements CanDeactivate. Which basically blocks navigation or not by returning true/false.

@Injectable()
export class ModelDirtyGuardService implements CanDeactivate<IDirty> {
    constructor(
        private confirmationDialogService: ConfirmationDialogService,
        private confirmationDialogReferenceService: ConfirmationDialogReferenceService
    ) { }
    public canDeactivate(
        component: IDirty,
        currentRoute: ActivatedRouteSnapshot,
        currentState: RouterStateSnapshot,
        nextState: RouterStateSnapshot
    ): Observable<boolean> | Promise<boolean> | boolean {
        let canLeave: boolean = component.isDirty();
        if (canLeave === false) {
            canLeave = this.confirmationDialogService.loadComponent(
              component.getRef(),
              nextState.url
            );
            this.confirmationDialogReferenceService.allow = false;
        } else {
            this.confirmationDialogReferenceService.allow = false;
        }

        return canLeave;
    }
}
Enter fullscreen mode Exit fullscreen mode

ConfirmationDialogService is the service that is responsible for constructing and inserting our component to the dom.

ConfirmationDialogReferenceService holds a global state of the component we want to insert.

what is happening is simple

  • we check if the model is dirty by using the isDirty function that our component needs to implement. One of the things that we are not covering here, is how to check if the form/model is dirty. Depends on your application logic.
  • if the model is dirty that means that we need to insert our component (ConfirmationDialogComponent) into the view, through a service (ConfirmationDialogService).
  • set the state allow through the ConfirmationDialogReferenceService
@Injectable()
export class ConfirmationDialogService {
    answer: boolean;
    componentRef: ComponentRef<ConfirmationDialogComponent>;

    constructor(
        private componentFactoryResolver: ComponentFactoryResolver,
        private confirmationDialogReferenceService: ConfirmationDialogReferenceService
    ) { }

    loadComponent(viewContainerRef: ViewContainerRef, nextState) {
        this.confirmationDialogReferenceService.routerState = nextState;
        let componentFactory = this.componentFactoryResolver.resolveComponentFactory(ConfirmationDialogComponent);
        this.componentRef = viewContainerRef.createComponent(componentFactory);
        this.confirmationDialogReferenceService.componentRef = this.componentRef;
        return this.confirmationDialogReferenceService.allow;
    }

}
Enter fullscreen mode Exit fullscreen mode

Next we create our component which implements IDirty and it has two functions

  • closeDialog(), we decide that we need not to navigate away so we need to unload the dynamic component (this is done by destroying it through ConfirmationDialogReferenceService) and stay in the same view.
  • navigateAway() we accept that we agree to navigate away, losing any changes.
export class ConfirmationDialogComponent{

    constructor(
    private confirmationDialogReferenceService: ConfirmationDialogReferenceService) { }

    public closeDialog() {
        this.confirmationDialogReferenceService.unloadComponent();
    }

    public navigateAway() {
        this.confirmationDialogReferenceService.allow = true;
        this.confirmationDialogReferenceService.destroyComponentAndAllowNavigation();
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally we have ConfirmationDialogReferenceService, which keeps the current state of our dynamic ConfirmationDialogComponent component.

The important bits is

  • routerState, which we set the route we need to navigate to
  • unloadComponent, which destroys our component (we stay in the same view)
  • destroyComponentAndAllowNavigation, which destroys our component and let us navigate away
@Injectable()
export class ConfirmationDialogReferenceService {
    private _componentRef: any;
    private _routerState: string;
    private _allow: boolean;

    constructor(
        private router: Router
    ) {

    }

    set componentRef(ref) {
        this._componentRef = ref;
    }

    get componentRef() {
        return this._componentRef;
    }

    set allow(allow) {
        this._allow = allow;
    }

    get allow() {
        return this._allow;
    }

    set routerState(state) {
        this._routerState = state;
    }

    get routerState() {
        return this._routerState;
    }

    public unloadComponent() {
        this.componentRef.destroy();
    }

    public destroyComponentAndAllowNavigation() {
        this.componentRef.destroy();
        this.router.navigate([this.routerState]);
    }

}
Enter fullscreen mode Exit fullscreen mode

Last but not least in order to use the createComponent function, our component needs to be an entryComponents in our Module

    entryComponents: [
        ConfirmationDialogComponent
    ]
Enter fullscreen mode Exit fullscreen mode

Apologies if there are any unused properties left, I tried to clean up. This whole example was lifted from a real world application

Top comments (3)

Collapse
 
elasticrash profile image
Stefanos Kouroupis

I had to make a small amendment. I accidentally implemented IDirty on ConfirmationDialogComponent, but that should only implemented as stated in the beginning on the component that has contains the form

Collapse
 
ericyu67 profile image
ericyu

Do you have any sample code? I am not sure how to use this idea.

Collapse
 
elasticrash profile image
Stefanos Kouroupis

Once you start implementing its quite straight forward. I don't have any sample code, but I have used it in 3 projects so far. Maybe If I find some time I could do a small poc