5

I have a list of objects. The user can click on one, which then loads a child component to edit that component.

The problem I have is that when the user goes back to the list component, the child component has to do some cleanup in the ngOnDestroy method - which requires making a call to the server to do a final 'patch' of the object. Sometimes this processing can be a bit slow.

Of course what happens is the user arrives back on the list, and that api call completes before the database transaction from the ngOnDestroy completes, and thus the user sees stale data.

  ngOnDestroy(){
    this.destroy$.next();
    this.template.template_items.forEach((item, index) => {
      // mark uncompleted items for deletion
      if (!item.is_completed) {
        this.template.template_items[index]['_destroy'] = true;
      };
    });
    // NOTE 
    // We don't care about result, this is a 'silent' save to remove empty items,
    // but also to ensure the final sorted order is saved to the server
    this._templateService.patchTemplate(this.template).subscribe();
    this._templateService.selectedTemplate = null;
  } 

I understand that doing synchronous calls is not recommended as it blocks the UI/whole browser, which is not great.

I am sure there are multiple ways to solve this but really don't know which is the best (especially since Angular does not support sync requests so I would have to fall back to standard ajax to do that).

One idea I did think of was that the ngOnDestroy could pass a 'marker' to the API, and it could then mark that object as 'processing'. When the list component does its call, it could inspect each object to see if it has that marker and show a 'refresh stale data' button for any object in that state (which 99% of the time would only be a single item anyway, the most recent one the user edited). Seems a bit of a crap workaround and requires a ton of extra code compared to just changing an async call to a sync call.

Others must have encountered similar issues, but I cannot seem to find any clear examples except this sync one.

EDIT

Note that this child component already has a CanDeactive guard on it. It asks the user to confirm (ie. discard changes). So if they click to confirm, then this cleanup code in ngOnDestroy is executed. But note this is not a typical angular form where the user is really 'discarding' changes. Essentially before leaving this page the server has to do some processing on the final set of data. So ideally I don't want the user to leave until ngOnDestroy has finished - how can I force it to wait until that api call is done?

My CanDeactive guard is implemented almost the same as in the official docs for the Hero app, hooking into a general purpose dialog service that prompts the user whether they wish to stay on the page or proceed away. Here it is:

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

    // Otherwise ask the user with the dialog service and return its
    // observable which resolves to true or false when the user decides
    return this._dialogService.confirm('You have some empty items. Is it OK if I delete them?');
  }

The docs do not make it clear for my situation though - even if I move my cleanup code from ngOnDestroy to a "YES" method handler to the dialog, it STILL has to call the api, so the YES handler would still complete before the API did and I'm back with the same problem.

UPDATE

After reading all the comments I am guessing the solution is something like this. Change the guard from:

    return this._dialogService.confirm('You have some empty items. 
        Is it OK if I delete them?');

to

    return this._dialogService.confirm('You have some empty items.
        Is it OK if I delete them?').subscribe(result => {
      ...if yes then call my api and return true...
      ...if no return false...
      });
rmcsharry
  • 5,363
  • 6
  • 65
  • 108
  • Look at CanDeactivate or guards in general, see [here](https://stackoverflow.com/questions/41148020/angular2-candeactivate-guard) for example. – Matt Oct 30 '17 at 09:20
  • Thanks, that's not the solution. I'll update the question to explain why. – rmcsharry Oct 30 '17 at 09:23
  • @rmcsharry that is a solution. your user confirmation in `CanDeactivate` is not a solution. – dee zg Oct 30 '17 at 09:28
  • If that is the solution, then why doesn't it work? I already have a CanDeactivate guard. When the guard activates the user is prompted. When they 'proceed' away, ngOnDestroy is called to do the cleanup work. – rmcsharry Oct 30 '17 at 09:35

2 Answers2

4

As you said, there are many ways and they depend on other details how your whole app, data-flow and ux-flow is setup but it feels like you might want to take a look at CanDeactivate guard method which ensures user cannot leave route until your Observable<boolean>|Promise<boolean> are resolved to true.

So, its a way for async waiting until your service confirms things are changed on server.

[UPDATE]

it depends on your user confirmation implementation but something along these lines...

waitForServiceToConfirmWhatever(): Observable<boolean> {
    return yourService.call(); //this should return Observable<boolean> with true emitted when your server work is done
  }

canDeactivate(): Observable<boolean> {

    if(confirm('do you want to leave?') == true)   
      return this.waitForServiceToConfirmWhatever();
    else
      Observable.of(false)
  }
dee zg
  • 13,793
  • 10
  • 42
  • 82
  • Thanks but the route already has a CanDeactive guard. It prompts the user Yes/No and if they answer Yes, then ngOnDestroy code is fired which is doing the cleanup. I can't do the cleanup before that. Hence a catch-22. – rmcsharry Oct 30 '17 at 09:22
  • what i would do is to have yes/no prompt just as a regular click handler in component. then if user clicks yes, i would to `router.navigate['whereverYourListIs']`. and then, use `CanDeactivate` as described in my answer. i see no reason that you handle yes/no in `CanDeactivate` guard. That belongs to component itself, not even in components `ngOnDestroy` as its just a regular user action like anything else not necesarily related to components lifetime in anyway as simple 'no' from user should do nothing. hope that makes sense. – dee zg Oct 30 '17 at 09:27
  • I don't handle yes/no. The user can navigate to anywhere and I don't know (or care) where they may want to go next. The CanDeactive fires wherever they choose to go. It detects if there is still work to be done (ie. the object is not in a 'final' state) and the guard throws up the prompt (basically, are you sure you want to leave and throw away this other stuff?). Clicking NO does nothing, as CanDeactive is meant to do - the user stays on the page. If they click yes, the guard allows them to proceed, the component gets unloaded and ngOnDestroy is automatically called. – rmcsharry Oct 30 '17 at 09:33
  • I think the answer might lie in your statement "user cannot leave route until your Observable|Promise are resolved to true" - so how do I change my guard to ensure a call to the API is resolved before leaving? Can I chain the api call to the dialog service call for example? – rmcsharry Oct 30 '17 at 09:44
  • ok, and what prevents you that after user clicks 'yes' you fire additional request to your service and wait for its `Observable` to resolve to `true` and then allow user to leave (`CanDeactivate` return `true`)? – dee zg Oct 30 '17 at 09:44
  • Thanks for that update, it makes it much clearer what you mean now. I can see how this will work - and I learned a lot more than just solving this particular problem (I can see a reusable code pattern here) so thank you! – rmcsharry Oct 30 '17 at 09:52
  • I tried your idea and it almost works but I am clearly missing something in my understanding. If you have time please take a look at this new question: https://stackoverflow.com/questions/47014700/angular4-how-to-make-candeactive-work-with-2-linked-observables – rmcsharry Oct 30 '17 at 11:59
1

One "workaround" I can think of is to have your list based in client. You have the list as a JS array or object and show the UI based on that. After editing in the details screen, have a stale flag on the item which the service called on ngOnDestroy clears while updating the other related data.

jaibatrik
  • 6,770
  • 9
  • 33
  • 62
  • That's a good idea which I actually like and will probably have to do in v2 to support 'offline' mode (coupled with using local storage). So +1, thanks for the idea. – rmcsharry Oct 30 '17 at 10:05