Angular CDK Overlay Module: CanDeactivateGuard should launch custom modal

Viewed 229

UPDATE:

I've refactored based on @Blind Despair's answer below. It now uses the Overlay Module from Angular CDK. Here's the code for the Guard:

@Injectable({
  providedIn: 'root',
})
export class PendingChangesGuard
  implements CanDeactivate<ComponentCanDeactivate> {
  content = 'A simple string content modal overlay';
  constructor(private overlayService: OverlayService) {}

  canDeactivate(
    component: ComponentCanDeactivate
  ): boolean | Observable<boolean> {
    return component.canDeactivate()
      ? true
      : this.openConfirmDialog(this.content);
  }

  openConfirmDialog(content: TemplateRef<any> | ComponentType<any> | 
string) {
    const ref = this.overlayService.open(this.content, null);

    return ref.afterClosed$.subscribe((res) => {});
  }
}

The problem is this line is returning a subscription and my guard has to return either a boolean or Observable:

 return ref.afterClosed$.subscribe((res) => {});

Here is the OverlayService class:

@Injectable({
  providedIn: 'root',
})
export class OverlayService {
  constructor(private overlay: Overlay, private injector: Injector) {}

  open<R = any, T = any>(
    content: string | TemplateRef<any> | Type<any>,
    data: T
  ): OverlayRef<R> {
    const configs = new OverlayConfig({
      hasBackdrop: true,
      panelClass: ['modal', 'is-active'],
      backdropClass: 'modal-background',
    });

    const overlayRef = this.overlay.create(configs);

    const myOverlayRef = new OverlayRef<R, T>(overlayRef, content, 
data);

    const injector = this.createInjector(myOverlayRef, this.injector);
    overlayRef.attach(new ComponentPortal(OverlayComponent, null, 
injector));

    return myOverlayRef;
  }

  createInjector(ref: OverlayRef, inj: Injector) {
    const injectorTokens = new WeakMap([[RaOverlayRef, ref]]);
    return new PortalInjector(inj, injectorTokens);
  }
}

And here's the Overlay class:

export interface OverlayCloseEvent<R> {
  type: 'backdropClick' | 'close';
  data: R;
}

// R = Response Data Type, T = Data passed to Modal Type
export class OverlayRef<R = any, T = any> {
  afterClosed$ = new Subject<OverlayCloseEvent<R>>();

  constructor(
    public overlay: OverlayRef,
    public content: string | TemplateRef<any> | Type<any>,
    public data: T // pass data to modal i.e. FormData
  ) {
    overlay.backdropClick().subscribe(() => 
this._close('backdropClick', null));
  }

  close(data?: R) {
    this._close('close', data);
  }

  private _close(type: 'backdropClick' | 'close', data: R) {
    this.overlay.dispose();
    this.afterClosed$.next({
      type,
      data,
    });

    this.afterClosed$.complete();
  }
}

Any tips for how to refactor the openConfirmDialog() to return what I need?

2 Answers

You cannot return a boolean in that guard, because opening modal is not enough to define the outcome it should give. You have to wait for user input, which means that your guard canDeactivate should either return a Promise or an Observable. Unfortunately your current modal dialog is too simple to support that use case. Basically you want to have something like .afterClosed() which returns Promise<boolean> or Observable<boolean> then when your user clicks yes/no you have to resolve a promise or emit a value into a subject and complete it. Then you can easily just return the result of .afterClosed() from your guard. But you would need to refactor your service and your modal for that. Of course you can have afterClosed() as a method on the modal component, but you don't want to expose all its methods to the guard or whatever would use it, so a good practice would be to introduce a DialogRef which would only expose what you want to be exposed. But why reinvent the wheel if this has been already solved? There is an amazing angular module called @angular/cdk which you can use to create a very smart modal dialog with low effort. You can check this article.

Can you just slighty refactor to something like...

canDeactivate(
    component: ComponentCanDeactivate
  ): boolean | Observable<boolean> {
    const _v = component.canDeactivate();
    if (!_v) {
      this.openConfirmDialog();
    }
    return _v;
  }
Related