Skip to content

Instantly share code, notes, and snippets.

@jnizet
Last active November 28, 2024 15:43
Show Gist options
  • 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...');
});
}
}
@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();
}`

@leukk
Copy link

leukk commented Nov 28, 2024

Hello, this is a great solution but it doesn't seem to work for templates containing relative links/anchors. Having the component that registers the templates defined in the application root makes href act relative to root. The same goes for angular router in the component itself. Do you know of any way that would make relative links work?

@leukk
Copy link

leukk commented Nov 28, 2024

In typical fashion, I found the solution right after posting.
I tried many ways including using absolute urls, but used the window href instead. This caused an unwanted page refresh.
One very simple way to do it that took me way too long to find is:
<a [routerLink]="window.location.pathname + '/' + relative_link">name</a>

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