Angular4 - how to make CanDeactive guard using dialogService confirmation work with chained observables?

老子叫甜甜 提交于 2019-12-08 07:59:46

问题


I have a CanDeactive guard based on the Hero app in the official docs.

This guard first checks if my component does not need cleanup, and it immediately returns false if so.

Otherwise it shows a confirm dialog to the user. The dialog is provided by this service (identical to the docs):

import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';

/**
 * Async modal dialog service
 * DialogService makes this app easier to test by faking this service.
 * TODO: better modal implementation that doesn't use window.confirm
 */
@Injectable()
export class DialogService {
  /**
   * Ask user to confirm an action. `message` explains the action and choices.
   * Returns observable resolving to `true`=confirm or `false`=cancel
   */
  confirm(message?: string): Observable<boolean> {
    const confirmation = window.confirm(message || 'Is it OK to leave the page?');

    return Observable.of(confirmation);
  };
}

If the user answers 'No' (false) to the dialog, then CanDeactive also immediately returns false. This is why it has two return types: Observable<boolean> and boolean (again, as per docs).

  canDeactivate(): Observable<boolean> | boolean {
    console.log('deactivating');
    // Allow immediate navigation if no cleanup needed
    if (this.template.template_items.filter((obj) => 
         { return !obj.is_completed }).length < 2)
      return true;

    // Otherwise ask the user with the dialog service
    this._dialogService.confirm('You have some empty items.
      Is it OK if I delete them?').subscribe(
      confirm => {
        console.log('confirm is ', confirm);
        if (confirm) return this.onDestroyCleanup().subscribe();
        else return false;
      }
    );
  }

Where I differ from the docs is that if the user answers yes (true) to the confirm dialog, I need to do some cleanup and then call my api - which you can see is the line return this.onDestroyCleanup().subscribe().

So I don't want to return true immediately, but first call this method and return true or false from that (false only if the api call failed).

Here is that method, which calls the api and maps the result of the api call to true or false:

  onDestroyCleanup():Observable<boolean> {
    console.log('cleaning up');
    this.template.template_items.forEach((item, index) => {
      // insert marker flag to delete all incomplete items
      if (!item.is_completed) {
        this.template.template_items[index]['_destroy'] = true;
      };
    });
    // a final save to remove the incomplete items
    return this._templateService.patchTemplate(this.template).take(1).map(
      result => {
        console.log('cleanup done: ', result);
        return true;
      },
      error => {
        console.log('cleanup done: ', error);
        this._apiErrorService.notifyErrors(error.json());
        return false;
      }
    );
  }

Everything works except the user stays on the page when they answer Yes. The console output is:

deactivating
confirm is true
cleaning up
cleanup done: [returned json object]

Seeing that output I realised that CanDeactive is not returning a result, so I changed the last part of the CanDeactive to this:

    ...same code up to here...
    // Otherwise ask the user with the dialog service
    return this._dialogService.confirm('You have some empty items. 
          Is it OK if I delete them?').map(
      confirm => {
        console.log('confirm is ', confirm);
        if (confirm) this.onDestroyCleanup().subscribe(r => {return r});
        else return false;
      }
    );
  }

But I get the same result.

So now I don't know whether I am structuring this correctly or if perhaps my onDestroyCleanup code is the thing that is wrong.

UPDATE

To help understanding, this code obviously works:

// Otherwise ask the user with the dialog service
return this._dialogService.confirm('You have some empty items. Is it 
       OK if I delete them?').map(
  confirm => {
    console.log('confirm is ', confirm);
    if (confirm) return true;
    else return false;
  }
);

...because it directly returns either true or false. The problem is how to replace the 'return true' with code that calls another Observable and when that resolves, to return the result of THAT one.

But to illustrate further, this does not work as it says "Type Observable <false> | Observable<boolean> is not assignable to type boolean | Observable<boolean>":

// Otherwise ask the user with the dialog service
return this._dialogService.confirm('You have some empty items. Is it 
       OK if I delete them?').map(
  confirm => {
    console.log('confirm is ', confirm);
    if (confirm) return Observable.of(true); 
    else return false;
  }
);

回答1:


By doing return this.onDestroyCleanup().subscribe(); in your canDeactivate method, you are returning a subscription, not an observable. Also you need to return a chained observable sequence (with flatMap/mergeMap). This way the canDeactivate method altogether returns an observable that can be subscribed to.

canDeactivate(): Observable<boolean> | boolean {
    console.log('deactivating');
    // Allow immediate navigation if no cleanup needed
    if (this.template.template_items.filter((obj) => 
        { return !obj.is_completed }).length < 2)
        return true;

    // Otherwise ask the user with the dialog service
    return this._dialogService.confirm('You have some empty items. Is it OK if I delete them?')
        // chain confirm observable with cleanup observable
        .flatMap(confirm => {
            console.log('confirm is ', confirm);
            if (confirm) {
                return this.onDestroyCleanup();
            } else {
                return Observable.of(false);
            }
        });
}

As you can see, we are now returning a chained observable sequence that the CanDeactivate Guard can subscribe to.

When you do swap out your confirm you can modify your confirm() method to something like

confirm(message?: string): Observable<boolean> {
    return new Observable(observer => {
        // bring up custom confirm message somehow
        customConfirm(message, (confirmation: boolean) => {
            // emit result. This would likely be in a callback function of your custom confirm message implementation
            observer.next(confirmation);
            observer.complete();
        }); 
    });
};

Now your confirm is returning an asynchronous implementation with an observable.



来源:https://stackoverflow.com/questions/47014700/angular4-how-to-make-candeactive-guard-using-dialogservice-confirmation-work-w

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!