Ngx-bootstrap: feat(modal): add functionality for identifying which modal triggered the events

Created on 26 Oct 2017  路  10Comments  路  Source: valor-software/ngx-bootstrap

Suppose we have two modals in a component and are subscribed to events available, we are unable to identify from which modal is triggered.

Plunkr: https://embed.plnkr.co/jas1u9F7DchyBeN33hCw/

comp(modal) easy (hours) feat-request

Most helpful comment

I've come up with a solution that works for opening modals using the service. This approach could probably be used just as easily for template modals as well. Basically I created a simple base modal component:

import {BsModalRef} from 'ngx-bootstrap';
import {Subject} from 'rxjs/Subject';

export class BaseModalComponent {
  public onHide: Subject<any> = new Subject<any>();

  constructor(private bsModalRef: BsModalRef) {}

  close(data: any = null): void {
    this.onHide.next(data);
    this.bsModalRef.hide();
  }
}

All of the modal components that I create extend this class and pass the modal ref into the super call:

import {Component} from '@angular/core';
import {BsModalRef} from 'ngx-bootstrap';
import {BaseModalComponent} from '../base-modal/base-modal.component';

@Component({
  selector: 'app-confirmation-modal',
  templateUrl: `
<div class="modal-header">
  <h4 class="modal-title pull-left">{{titleMsg}}</h4>
  <button type="button" class="close pull-right" aria-label="Close" (click)="close(false)">
    <span aria-hidden="true">&times;</span>
  </button>
</div>
<div class="modal-body">
  <span>{{confirmationMsg}}</span>
</div>
<div class="modal-footer">
  <button type="button" class="btn btn-primary" (click)="close(true)">Ok</button>
  <button type="button" class="btn btn-default" (click)="close(true)">Cancel</button>
</div>
`,
  styleUrls: ['./confirmation-modal.component.scss']
})
export class ConfirmationModalComponent extends BaseModalComponent {
  confirmationMsg = '';
  titleMsg = '';
  constructor(bsModalRef: BsModalRef) {
    super(bsModalRef);
  }
}

then the caller is able to do this:

this.modalService.show(ConfirmationModalComponent).content.onHide
    .take(1)
    .subscribe((result: boolean) => {
        console.log(`user clicked ${result ? 'ok' : 'cancel'}`);
    });

Using this approach, you never have to listen to this.modalService.onHide, which will always fire no matter which modal is hidden. In my case I have multiple modals opened with the same modal service and closing any of them fired event handlers for all of them.

All 10 comments

You're right, there's no functionality that can help a user to identify which modal has triggered the event. We need to implement an id of modal or something like that. Btw, a small PR with this fix would be helpful :)

Any idea when this might be implemented?

When can this be expected to be released

Here's my solution. This works well with templated dialogs. It might not work for everyone, but I thought it'd be good to share anyway. Create a separate service. In my example it is "SessionService". Add two public members to it, one being a semaphore counting the number of open open modals (this can be useful to see if stacked dialogs are still open), another an eventEmitter with string payload.

@Injectable()
export class SessionService implements OnDestroy {
    private _modals = 0;
    private subscriptions: Subscription[] = [];
    public childUpdated = new EventEmitter<string>();
    public get isModal(): boolean {
        return this._modals > 0;
    }
    constructor(private modalService: BsModalService
    ) {
        this.subscriptions.push(this.modalService.onShow.subscribe((evt: any) => this._modals++));
        this.subscriptions.push(this.modalService.onHide.subscribe((evt: any) => this._modals--));
    }
    ngOnDestroy() {
        this.subscriptions.forEach(s => s.unsubscribe());
    }
}
@Component({...})
export class DialogComponent {
   @Input() dialogId: string = 'some default id';
   public modalRef: BsModalRef = null;
   ....
   constructor(private sessionService: SessionService) { }
   hideMe() {
      if (this.okPressedOrSomethingLikeThat) {
        this.sessionService.childUpdated.emit(this.dialogId);
      }
      if (this.modalRef) this.modalRef.hide();
    }
    public initModal(modalRef: BsModalRef, dialogId: string) {
      this.modalRef = modalRef;
      this.dialogId = dialogId;
      ...
    }
}
@Component({...})
export class HostComponent {
    ....
    constructor(private sessionService: SessionService) { }
    ngOnInit() {
        ....
        this.subscriptions.push(this.sessionService.childUpdated.subscribe((child: string) => {
            switch (child) {
                case 'dialogId1':
                case 'dialogId2':
                    break;
            }
        }));
    }
    ngOnDestroy() {
        this.subscriptions.forEach(s => s.unsubscribe());
    }
    public showDialog1(): void {
        const modalRef = this.modalService.show(DialogComponent,
            Object.assign({}, {
                animated: true,
                keyboard: true,
                backdrop: true,
                ignoreBackdropClick: false
            }, { class: 'modal-lg' })
        );
        (<DialogComponent>modalRef.content).initModal(modalRef, 'dialogId1');
    }
}

You can apply the same technique to nested dialogs as well. The hosting parent will get events with proper dialog ids.

// D

P.S. sorry for the garbled up code format - not my fault ;)

So I'm not supper content with this solution, however here is workaround i have for this issues:

let thisModalNumber = this.modalService.getModalsCount();
this.modalService.show(MyDialogComponent);

let sub = this.modalService.onHide.subscribe(() => {
    if (thisModalNumber === this.modalService.getModalsCount()) {
        if (sub)
            sub.unsubscribe();

        //
        // rest of your code here to process after only this dialog was closed (not a nested child)
        //
    }
});

Why not have the modal service show method return an observable? This would make things a lot less complicated it seems. The modalRef hide method could take an argument that the observable could emit.

@Component({
    selector: 'my-modal',
    template: `
<div class="modal-footer">
  <button type="button" class="btn btn-primary" (click)="bsModalRef.hide(true)">Ok</button>
  <button type="button" class="btn btn-default" (click)="bsModalRef.hide(false)">Cancel</button>
</div>
`
})
export class ConfirmationComponent {}
this.modalService.show(ConfirmationComponent)
    .take(1)
    .subscribe((resp) => {
        console.log(`user clicked ${resp ? 'ok' : 'cancel'}`);
    });

The onShow and onHide events really have no business being on the service. The events should come from the modalRef itself. I haven't looked at the source so I don't know if this change would be too big of an undertaking given the current code base but I think it would probably simplify things, not only for the user-facing API, but for the code base itself.

I've come up with a solution that works for opening modals using the service. This approach could probably be used just as easily for template modals as well. Basically I created a simple base modal component:

import {BsModalRef} from 'ngx-bootstrap';
import {Subject} from 'rxjs/Subject';

export class BaseModalComponent {
  public onHide: Subject<any> = new Subject<any>();

  constructor(private bsModalRef: BsModalRef) {}

  close(data: any = null): void {
    this.onHide.next(data);
    this.bsModalRef.hide();
  }
}

All of the modal components that I create extend this class and pass the modal ref into the super call:

import {Component} from '@angular/core';
import {BsModalRef} from 'ngx-bootstrap';
import {BaseModalComponent} from '../base-modal/base-modal.component';

@Component({
  selector: 'app-confirmation-modal',
  templateUrl: `
<div class="modal-header">
  <h4 class="modal-title pull-left">{{titleMsg}}</h4>
  <button type="button" class="close pull-right" aria-label="Close" (click)="close(false)">
    <span aria-hidden="true">&times;</span>
  </button>
</div>
<div class="modal-body">
  <span>{{confirmationMsg}}</span>
</div>
<div class="modal-footer">
  <button type="button" class="btn btn-primary" (click)="close(true)">Ok</button>
  <button type="button" class="btn btn-default" (click)="close(true)">Cancel</button>
</div>
`,
  styleUrls: ['./confirmation-modal.component.scss']
})
export class ConfirmationModalComponent extends BaseModalComponent {
  confirmationMsg = '';
  titleMsg = '';
  constructor(bsModalRef: BsModalRef) {
    super(bsModalRef);
  }
}

then the caller is able to do this:

this.modalService.show(ConfirmationModalComponent).content.onHide
    .take(1)
    .subscribe((result: boolean) => {
        console.log(`user clicked ${result ? 'ok' : 'cancel'}`);
    });

Using this approach, you never have to listen to this.modalService.onHide, which will always fire no matter which modal is hidden. In my case I have multiple modals opened with the same modal service and closing any of them fired event handlers for all of them.

@instantaphex
That's what exactly I am doing at the moment, minus the super class.
There was a scenario that makes me create a pull request.
I cannot remember why I did that.

Hello guys, any news on this issue? :)

When we use modal with component, the associated component will be destroyed when you close/hide your popup so we can identify close/hide event using the ondestroy event of the associated component :

in your parent component where you initialize the modal you define your modal using observable like that :
` let destroySubject = new Subject();

        new Observable<Callback>(observer => {
            const initialState = {
                onConfirm: (callback: Callback) => {
                    observer.next(callback);
                },
                destroySubject
            };
            this.modalService.show(CallbackModalComponent, {initialState, class: 'modal-lg'});
        })
      .pipe(takeUntil(destroySubject))
      .subscribe();`

the destroySubject is the subject related to the modal component destroy event.

The modal component take in params the initialState and the destroySubject that will be linked with the onDestroy like that :

`export class CallbackModalComponent implements OnInit, OnDestroy {

callback: Callback;
onConfirm: (callback: Callback) => void;
destroySubject: Subject<void>;

constructor(public bsModalRef: BsModalRef) {
}

ngOnDestroy(): void {
    this.destroySubject.next();
}

ngOnInit() {
}

onSubmit() {
    this.onConfirm(callback);
    this.bsModalRef.hide();
}

}`

the observable that initializing you modal will complete when the modal is hided.
So here each modal will have its onHide event based on the onDestroy of the related modal component.

Was this page helpful?
0 / 5 - 0 ratings