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;
}
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;
}
}
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 theConfirmationDialogReferenceService
@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;
}
}
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();
}
}
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]);
}
}
Last but not least in order to use the createComponent
function, our component needs to be an entryComponents
in our Module
entryComponents: [
ConfirmationDialogComponent
]
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)
I had to make a small amendment. I accidentally implemented
IDirty
onConfirmationDialogComponent
, but that should only implemented as stated in the beginning on the component that has contains the formDo you have any sample code? I am not sure how to use this idea.
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