Skip to content

Instantly share code, notes, and snippets.

@jnizet
Last active October 27, 2022 16:54
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save jnizet/15c7a0ab4188c9ce6c79ca9840c71c4e to your computer and use it in GitHub Desktop.
Save jnizet/15c7a0ab4188c9ce6c79ca9840c71c4e to your computer and use it in GitHub Desktop.
How to create a reusable service allowing to open a confirmation modal from anywhere with ng-bootstrap
import { Component, Injectable, Directive, TemplateRef } from '@angular/core';
import { NgbModal, NgbModalRef, NgbModalOptions } from '@ng-bootstrap/ng-bootstrap';
/**
* Options passed when opening a confirmation modal
*/
interface ConfirmOptions {
/**
* The title of the confirmation modal
*/
title: string,
/**
* The message in the confirmation modal
*/
message: string
}
/**
* An internal service allowing to access, from the confirm modal component, the options and the modal reference.
* It also allows registering the TemplateRef containing the confirm modal component.
*
* It must be declared in the providers of the NgModule, but is not supposed to be used in application code
*/
@Injectable()
export class ConfirmState {
/**
* The last options passed ConfirmService.confirm()
*/
options: ConfirmOptions;
/**
* The last opened confirmation modal
*/
modal: NgbModalRef;
/**
* The template containing the confirmation modal component
*/
template: TemplateRef<any>;
}
/**
* A confirmation service, allowing to open a confirmation modal from anywhere and get back a promise.
*/
@Injectable()
export class ConfirmService {
constructor(private modalService: NgbModal, private state: ConfirmState) {}
/**
* Opens a confirmation modal
* @param options the options for the modal (title and message)
* @returns {Promise<any>} a promise that is fulfilled when the user chooses to confirm, and rejected when
* the user chooses not to confirm, or closes the modal
*/
confirm(options: ConfirmOptions): Promise<any> {
this.state.options = options;
this.state.modal = this.modalService.open(this.state.template);
return this.state.modal.result;
}
}
/**
* The component displayed in the confirmation modal opened by the ConfirmService.
*/
@Component({
selector: 'confirm-modal-component',
template: `<div class="modal-header">
<button type="button" class="close" aria-label="Close" (click)="no()">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">{{ options.title}}</h4>
</div>
<div class="modal-body">
<p>{{ options.message }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" (click)="yes()">Yes</button>
<button type="button" class="btn btn-secondary" (click)="no()">No</button>
</div>`
})
export class ConfirmModalComponent {
options: ConfirmOptions;
constructor(private state: ConfirmState) {
this.options = state.options;
}
yes() {
this.state.modal.close('confirmed');
}
no() {
this.state.modal.dismiss('not confirmed');
}
}
/**
* Directive allowing to get a reference to the template containing the confirmation modal component,
* and to store it into the internal confirm state service. Somewhere in the view, there must be
*
* ```
* <template confirm>
* <confirm-modal-component></confirm-modal-component>
* </template>
* ```
*
* in order to register the confirm template to the internal confirm state
*/
@Directive({
selector: "template[confirm]"
})
export class ConfirmTemplateDirective {
constructor(confirmTemplate: TemplateRef<any>, state: ConfirmState) {
state.template = confirmTemplate;
}
}
@Component({
selector: 'some-applicative-component',
templateUrl: './some-applicative-component.html'
})
export class SomeApplicativeComponent {
constructor(private confirmService: ConfirmService) {}
deleteFoo() {
this.confirmService.confirm({ title:'Confirm deletion', message: 'Do you really want to delete this foo?' }).then(
() => {
console.log('deleting...');
},
() => {
console.log('not deleting...');
});
}
}
@jnizet
Copy link
Author

jnizet commented Aug 24, 2019

@pflugs30 you shouldn't use this code. It was a workaround for the lack of component support in ngb modals a long time ago.

Here's a better solution: https://github.com/Ninja-Squad/globe42/blob/master/frontend/src/app/confirm.service.ts, https://github.com/Ninja-Squad/globe42/tree/master/frontend/src/app/confirm-modal-content.

@pflugs30
Copy link

pflugs30 commented Aug 26, 2019

@jnizet Thanks for the update. I will take a look at that other link you posted. Much appreciated!

Edit: I am aware of the limitations of the NgbModals. In my current project, I need to use Angular 5 and NgbModals v1.1.2 (as I showed in my StackBlitz). Until I have the time to update my app to the latest packages, I figured your excellent example would be most helpful. :-)

@omarouen
Copy link

hello,
i used this inside form valueschanges but i had ExpressionChangedAfterItHasBeenCheckedError exception due to Expression has changed after it was checked. Previous value: 'ng-untouched: true'. Current value: 'ng-untouched: false'. Do you have any idea please?

@jnizet
Copy link
Author

jnizet commented Nov 27, 2019

@omarouen You should ask a question, with a complete minimal example reproducing the issue, on StackOverflow.

@iamwilson
Copy link

@omarouen - try to add a change detection strategy to your app as follows.

`import { ChangeDetectorRef, AfterContentChecked} from '@angular/core';
constructor(
private cdref: ChangeDetectorRef) { }

ngAfterContentChecked() {
this.cdref.detectChanges();
}`

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment