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...');
});
}
}
@fnalin
Copy link

fnalin commented Dec 21, 2016

Thank you very much.
I used it here

@alex-ponce
Copy link

Excellent work. Thank you for creating such an elegant solution!

@plachy-jozef
Copy link

Thank you for nice elegant solution 👍

@harry122
Copy link

Cannot set property 'survey_move_dateModel' of undefined

html file

Pick Up Date:

          <template [ngIf]="survey.survey_move_date!=''">
            {{survey.survey_move_date | date:'dd MMM, y'}}
          </template>
        </p>
      </div>
      <div class="iconedit">
        <a class="fancybox" (click)="openPickUpCalender()" *ngIf="!seedLoader">
          <img src="../../../../images/edit-icn.png" >
        </a>


      </div>
    </div>

open.component.ts

private stringToPickupObject(survey_move_date:string){
if(survey_move_date!='' && survey_move_date!=null){
let pkpDate:any =new Date(survey_move_date);
return {year:pkpDate.getFullYear() , month:pkpDate.getMonth()+1, day:pkpDate.getDate()};
}
}

openPickUpCalender() {
const NgbmodalRef: any = this.modalService.open(pickupcomponent, {size: 'sm'});
// this.survey.survey_move_date='2017-01-02'
NgbmodalRef.componentInstance.survey_move_dateModel = this.stringToPickupObject(this.survey.survey_move_date);
NgbmodalRef.componentInstance.survey_move_date = this.survey.survey_move_date;
NgbmodalRef.componentInstance.stateId = this.survey.survey_source_address.address_state_id;
NgbmodalRef.componentInstance.holidays = this.holidays;

NgbmodalRef.result.then((survey_move_date:any) => {
  this.survey.survey_move_date = survey_move_date;
}, (reason:any) => {
//   console.log('Date not selected')
})

}

pickup.component.ts

import {Component, Input} from '@angular/core';

import {
NgbModal, ModalDismissReasons, NgbActiveModal, NgbDatepickerConfig,
NgbDateStruct
} from '@ng-bootstrap/ng-bootstrap';
import {FormGroup, FormBuilder} from "@angular/forms";

@component({
selector: 'pickup-modal',
template: `


<button type="button" class="close" aria-label="Close" (click)="activeModal.close(null)">
×

Move Date


<div class="modal-body">
 <ngb-datepicker #dp [(ngModel)]="survey_move_dateModel" name="move_date" #moveDate="ngbDatepicker"  
                        (ngModelChange)="onPickupDateChange($event)"
                        (navigate)="date = $event.next" ></ngb-datepicker>
                       
 </div> 
 <div class="modal-footer">
 <button class="btn-default" (click)="activeModal.close(survey_move_date)">Done</button> 

`
})
export class PickUpModalComponent {

@Input()survey_move_dateModel: any;
@Input()survey_move_date: any;
@Input()stateId: number;
@Input()holidays: any[];

constructor(private pickupDateConfig: NgbDatepickerConfig, private fb: FormBuilder, private activeModal:NgbActiveModal) {
    let currentDate: any = new Date();
    let minDate: any = new Date();
    let maxDate: any = new Date();

    if (currentDate.getHours() > 12) {
        minDate.setDate(currentDate.getDate() + 2);
        maxDate.setDate(currentDate.getDate() + 92);
        pickupDateConfig.minDate = {year: minDate.getFullYear(), month: minDate.getMonth() + 1, day: minDate.getDate()};
        pickupDateConfig.maxDate = {year: maxDate.getFullYear(), month: maxDate.getMonth() + 1, day: maxDate.getDate()};
    } else {
        minDate.setDate(currentDate.getDate() + 1);
        maxDate.setDate(currentDate.getDate() + 91);
        pickupDateConfig.minDate = {year: minDate.getFullYear(), month: minDate.getMonth() + 1, day: minDate.getDate()};
        pickupDateConfig.maxDate = {year: maxDate.getFullYear(), month: maxDate.getMonth() + 1, day: maxDate.getDate()};
    }

    // pickupDateConfig.outsideDays = 'hidden';
    this.onPickupDateChange(this.survey_move_dateModel);
}




private onPickupDateChange(pickupDate: any) {
    if (pickupDate != null && typeof pickupDate == 'object') {
        this.survey_move_date = pickupDate.year + '-' + pickupDate.month + '-' + pickupDate.day;
    }
}



isDisabled = (date: NgbDateStruct) =>
    this.getDisabledDates(date.year, date.month, date.day);




private getDisabledDates(year: number, month: number, date: number) {
    if (typeof this.holidays!= 'undefined' && this.stateId) {
        return this.getHolidayDate(this.stateId, month, year).indexOf(date) > -1 ? true : false;
    } else {
        return false
    }

}


private getHolidayDate(stateId: number, month: number, year: number) {

    let stateHolidays: any[] = this.getOriginHolidays(stateId);
    let holiDatesByMonth: any[] = [];
    stateHolidays.forEach((function (holiDates: any) {

        let d: any = new Date(holiDates.holiday_date);
        if (month == d.getMonth() + 1 && year == d.getFullYear()) {
            holiDatesByMonth.push(d.getDate());
        }
    }))
    return holiDatesByMonth;
}


private getOriginHolidays(stateId: number) {
    return this.holidays.filter(function (holiday: any) {
        return holiday.state_id == stateId ? holiday : false;
    })
}

}

can anyone help me in finding the error.

@cnicho
Copy link

cnicho commented Jan 24, 2018

Hi,
I'm trying this in Angular 5 but my example does not render the template HTML; I get an empty modal-content. When I click the area that would be in the background (that has been grayed out) the 'not deleting ..' message is logged so part of it is working.
Any suggestions?
image

@iamjsmith
Copy link

iamjsmith commented Feb 10, 2018

In Angular v4 template has been deprecated in favour of ng-template and completely removed in v5

@Darren777
Copy link

@cnicho I'm getting the exact same problem as you. if any one has any luck please let us know

@jesshannon
Copy link

Change the code in your main app template to this:

<ng-template confirm>
    <confirm-modal-component></confirm-modal-component>
</ng-template>

Then change the selector directive to this:

@Directive({
    selector: "[confirm]"
})

That's working for me in Angular5

@Danielapariona
Copy link

Example of how to use the directive, please.

@pranithan-kang
Copy link

Very thank you!

@whisher
Copy link

whisher commented Sep 10, 2018

Hi there,
thanks a lot buddy to sharing :)
Based on your example I've made a simple module
to use with ie CanDeactivate
https://gist.github.com/whisher/c5726e30ea40a4d5caf8b77ab8b0d48a

@cpell
Copy link

cpell commented May 2, 2019

Here is a working sample on Angular 7 for anyone having issues. The directive needs to be included in the declarations.
https://stackblitz.com/edit/angular-muowrf

@pflugs30
Copy link

@jnizet Thank you for the awesome sample!
@tshannon Thanks for the nudge in the right direction for Angular 5.

I used the code from the gist and moved it into a StackBlitz project. I split the separate items into their own files, optimized the references and dependencies, and added a few more options to test the pattern. I then wrapped the confirmation dialog code into a separate NgModule for optimal reuse. It should be drop-in ready for use in another project.

Please note that I used the package versions I did because that's what was pre-existing in my project. Your mileage may vary. :-)

@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