0
votes

I have a question, if is possible to return value from Subject.next() call. Or any other possible aproach how to get response in described scenario.

My situation: I have a notify service which is used everywhere in the app (it should show message box to user, min with button ok and I need to know that user clicks on this button):

import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class NotifyMessagesService {
   private setMessageBoxData = new Subject<INotifyMessage>();

   constructor() { }

   getMessageBoxData(): Observable<INotifyMessage> {
      return this.setMessageBoxData.asObservable();
   }
    
   public notifyMessageBox(message: string, header?: string)/*: Promise<any>*/ {
      /*return new Promise(resolve => {*/
      
      this.setMessageBoxData.next({ message: message, header: header });
      /*resolve();*/ //HERE should go the response from next()
      /* });*/
   }
}

export interface INotifyMessage {
  header?: string;
  message: string;
}

And I have one component, which is subscribed to this service:

export class NotifyControllerComponent implements OnInit, OnDestroy {

@ViewChild('messageBox', null) messageBox: MessageBoxComponent;

subscription: Subscription;

constructor(private notifyService: NotifyMessagesService) {

   this.subscription = this.notifyService
      .getMessageBoxData()
      .subscribe(message => {
        if (message) {
          this.messageBox.show(`${message.message}`
            , `${message.header}`).then(() => {
              //HERE I need notify NotifyMessagesService back, that user click to the message box
            });
        }
      });

  }

  ngOnInit() { }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

Please advise how to update code examples to achive: from anywhere I call the service, it returns back after user confirm message box

export class AnyComponent implements OnInit{
   constructor(private notifyMessagesService: NotifyMessagesService){
   }

   showMessage(){
      this.notifyMessagesService.notifyMessageBox('Hi','it works').then(res=>{
         console.log('User reaction ' + res);
         //code continue
      });  
   } 
}

-> so I think that service method should be updated to return Promise, or Observable (as commented in examples), but how?

1
I think you might be going about this in the wrong manner. How about adding a callback function as an optional argument and then execute that callback whenever the user clicks the button?Mikkel Christensen
Something similiar I already try, but it is not good. Then the compoment need to be subscribed for this callback, to know, that user clicks and I need somehow find which code is waiting for this response and where continue.Wasullda
I don't know all the details and use cases but the easiest way would be just add public method in your service that will emit another observable event. You can wrap it is same observable too with different type :) Then you can subscribe to that different observable or same with different type and do as you wish.alphiii

1 Answers

0
votes

Custom confirmation dialog with callback support

This approach utilizes the ComponentFactoryResolver to create a dialog component which we can then construct dynamically whenever we need to create a confirmation dialog for the user.

Problems we need to solve to use this approach

  • Construct a component which will be the template for our confirmation dialog
  • Create a service which can create new instances of our dialog
  • Get a ViewContainerRef into our service.

The last point requires a bit of compromise, as services do not have a ViewContainerRef to attach to on their own, we will have to orchestrate our application such that our dialog service has access to a reference, before it can create the dialog component.

Confirmation Dialog Component

Let's first look at the component which will serve as our confirmation dialog.

It's a simple component with a couple of buttons, and a function reference for the callback we want to pass into it later.

@Component({
  selector: 'confirm-dialog',
  templateUrl: 'confirm-dialog.component.html',
  styleUrls: ['confirm-dialog.component.css']
})

export class ConfirmDialogComponent implements OnInit {
  componentReference!: ComponentRef<ConfirmDialogComponent>;
  confirmCallback!: () => void | undefined;
  messageOption!: MessageOption;

  constructor() {
  }

  ngOnInit(): void {
  }

  confirm() {
    this.confirmCallback();
    this.destroy();
  }

  destroy() {
    this.componentReference?.destroy();
  }

}
<div class="confirm-dialog">
  <div class="confirm-header">{{messageOption?.title}}</div>
  <div class="confirm-message">{{messageOption?.description}}</div>
  <div class="confirm-button-group">
    <button class="confirm" (click)="confirm()">Confirm</button>
    <button (click)="destroy()">Cancel</button>
  </div>
</div>
export interface MessageOption {
  title: string;
  description: string;
}

I have omitted the css for brevity

Dialog Service

Now for the next piece of the puzzle, the service which will construct our confirmation dialog.

@Injectable({providedIn: 'root'})
export class DialogService {
  private _view!: ViewContainerRef

  constructor(private resolver: ComponentFactoryResolver) {

  }

  set view(ref: ViewContainerRef) {
    this._view = ref;
  }

  get view() {
    return this._view;
  }

  confirmDialog(messageOpts: MessageOption, callback: () => void = () => {
  }) {
    // create the dialog on the view reference.
    const factory = this.resolver.resolveComponentFactory(ConfirmDialogComponent);
    const ref = this._view.createComponent(factory) as ComponentRef<ConfirmDialogComponent>;
    // set the properties
    ref.instance.messageOption = messageOpts;
    ref.instance.componentReference = ref;
    ref.instance.confirmCallback = callback;
  }

  invokedByCallback() {
    console.log("I was invoked via a callback, unless you changed the code you naughty dev you")
  }
}

The service utilizes the ComponentFactoryResolver and the ViewContainerRef to construct a new instance of the ConfirmDialogComponent.

We then pass the references we need down into the new ConfirmDialogComponent instance, including its newly created ComponentRef such that we can remove the dialog when the user has clicked confirm or cancel.

Setting up the ViewRef

The solution does not work in its current form, as the ViewRef is currently undefined.

To fix this, we need to inject the service in the component which is bootstrapping your application, and setting the ViewRef from there.

app.component.ts

export class AppComponent {
  constructor(private dialogService: DialogService, public view: ViewContainerRef) {
    dialogService.view = view;
  }
}

You can now utilize the DialogService as you normally would anywhere in your Angular app to create Confirmation Dialogs.

Stackblitz

Here is an example of usage