Skip to content

Commit

Permalink
feat(modal): allow components as content
Browse files Browse the repository at this point in the history
Closes #680
Closes #727

Closes #846
  • Loading branch information
pkozlowski-opensource committed Oct 7, 2016
1 parent ff132d7 commit ecdd3a0
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 41 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>You can pass an existing component as content of the modal window. In this case remember to add content component
as an <code>entryComponents</code> section of your <code>NgModule</code>.</p>

<button class="btn btn-lg btn-outline-primary" (click)="open()">Launch demo modal</button>
36 changes: 36 additions & 0 deletions demo/src/app/components/modal/demos/component/modal-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Component} from '@angular/core';

import {NgbModal, ModalDismissReasons, NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';

@Component({
selector: 'ngbd-modal-content',
template: `
<div class="modal-header">
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Modal title</h4>
</div>
<div class="modal-body">
<p>One fine body&hellip;</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" (click)="activeModal.close('Close click')">Close</button>
</div>
`
})
export class NgbdModalContent {
constructor(public activeModal: NgbActiveModal) {}
}

@Component({
selector: 'ngbd-modal-component',
templateUrl: './modal-component.html'
})
export class NgbdModalComponent {
constructor(private modalService: NgbModal) {}

open() {
this.modalService.open(NgbdModalContent);
}
}
22 changes: 12 additions & 10 deletions demo/src/app/components/modal/demos/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import {NgbdModalBasic} from './basic/modal-basic';
import {NgbdModalComponent, NgbdModalContent} from './component/modal-component';
import {NgbdModalCustomClass} from './customclass/modal-customclass';

export const DEMO_DIRECTIVES = [NgbdModalBasic, NgbdModalCustomClass];
export const DEMO_DIRECTIVES = [NgbdModalBasic, NgbdModalComponent, NgbdModalCustomClass];
export {NgbdModalContent} from './component/modal-component';

export const DEMO_SNIPPETS = {
basic: {
code: require('!!prismjs?lang=typescript!./basic/modal-basic'),
markup: require('!!prismjs?lang=markup!./basic/modal-basic.html')
},
customclass: {
code: require('!!prismjs?lang=typescript!./customclass/modal-customclass'),
markup: require('!!prismjs?lang=markup!./customclass/modal-customclass.html')
}
'basic': {
'code': require('!!prismjs?lang=typescript!./basic/modal-basic'),
'markup': require('!!prismjs?lang=markup!./basic/modal-basic.html')},
'component': {
'code': require('!!prismjs?lang=typescript!./component/modal-component'),
'markup': require('!!prismjs?lang=markup!./component/modal-component.html')},
'customclass': {
'code': require('!!prismjs?lang=typescript!./customclass/modal-customclass'),
'markup': require('!!prismjs?lang=markup!./customclass/modal-customclass.html')}
};

5 changes: 3 additions & 2 deletions demo/src/app/components/modal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import {NgModule} from '@angular/core';
import {NgbdModal} from './modal.component';
import {NgbdSharedModule} from '../../shared';
import {NgbdComponentsSharedModule} from '../shared';
import {DEMO_DIRECTIVES} from './demos';
import {DEMO_DIRECTIVES, NgbdModalContent} from './demos';

@NgModule({
imports: [NgbdSharedModule, NgbdComponentsSharedModule],
exports: [NgbdModal],
declarations: [NgbdModal, ...DEMO_DIRECTIVES]
entryComponents: [NgbdModalContent],
declarations: [NgbdModal, NgbdModalContent, ...DEMO_DIRECTIVES]
})
export class NgbdModalModule {}
4 changes: 4 additions & 0 deletions demo/src/app/components/modal/modal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {DEMO_SNIPPETS} from './demos';
<ngbd-api-docs-class type="NgbModal"></ngbd-api-docs-class>
<ngbd-api-docs-class type="NgbModalOptions"></ngbd-api-docs-class>
<ngbd-api-docs-class type="NgbModalRef"></ngbd-api-docs-class>
<ngbd-api-docs-class type="NgbActiveModal"></ngbd-api-docs-class>
<ngb-alert [dismissible]="false">
<strong>Heads up!</strong>
The <code>NgbModal</code> service needs a container element with the <code>ngbModalContainer</code> directive. The
Expand All @@ -19,6 +20,9 @@ import {DEMO_SNIPPETS} from './demos';
<ngbd-example-box demoTitle="Modal with default options" [htmlSnippet]="snippets.basic.markup" [tsSnippet]="snippets.basic.code">
<ngbd-modal-basic></ngbd-modal-basic>
</ngbd-example-box>
<ngbd-example-box demoTitle="Components as content" [htmlSnippet]="snippets.component.markup" [tsSnippet]="snippets.component.code">
<ngbd-modal-component></ngbd-modal-component>
</ngbd-example-box>
<ngbd-example-box demoTitle="Modal with custom class"
[htmlSnippet]="snippets.customclass.markup"
[tsSnippet]="snippets.customclass.code">
Expand Down
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ export {
NgbDateParserFormatter
} from './datepicker/datepicker.module';
export {NgbDropdownModule, NgbDropdownConfig} from './dropdown/dropdown.module';
export {NgbModalModule, NgbModal, NgbModalOptions, NgbModalRef, ModalDismissReasons} from './modal/modal.module';
export {
NgbModalModule,
NgbModal,
NgbModalOptions,
NgbActiveModal,
NgbModalRef,
ModalDismissReasons
} from './modal/modal.module';
export {NgbPaginationModule, NgbPaginationConfig} from './pagination/pagination.module';
export {NgbPopoverModule, NgbPopoverConfig} from './popover/popover.module';
export {NgbProgressbarModule, NgbProgressbarConfig} from './progressbar/progressbar.module';
Expand Down
36 changes: 19 additions & 17 deletions src/modal/modal-container.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Directive,
Injector,
ReflectiveInjector,
Renderer,
TemplateRef,
ViewContainerRef,
Expand All @@ -9,18 +10,13 @@ import {
ComponentRef
} from '@angular/core';

import {isDefined} from '../util/util';
import {isDefined, isString} from '../util/util';
import {ContentRef} from '../util/popup';

import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';
import {NgbModalStack} from './modal-stack';
import {NgbModalRef} from './modal-ref';

class ModalContentContext {
close(result?: any) {}
dismiss(reason?: any) {}
}
import {NgbActiveModal, NgbModalRef} from './modal-ref';

@Directive({selector: 'template[ngbModalContainer]'})
export class NgbModalContainer {
Expand All @@ -29,16 +25,16 @@ export class NgbModalContainer {

constructor(
private _injector: Injector, private _renderer: Renderer, private _viewContainerRef: ViewContainerRef,
componentFactoryResolver: ComponentFactoryResolver, ngbModalStack: NgbModalStack) {
this._backdropFactory = componentFactoryResolver.resolveComponentFactory(NgbModalBackdrop);
this._windowFactory = componentFactoryResolver.resolveComponentFactory(NgbModalWindow);
private _componentFactoryResolver: ComponentFactoryResolver, ngbModalStack: NgbModalStack) {
this._backdropFactory = _componentFactoryResolver.resolveComponentFactory(NgbModalBackdrop);
this._windowFactory = _componentFactoryResolver.resolveComponentFactory(NgbModalWindow);

ngbModalStack.registerContainer(this);
}

open(content: string | TemplateRef<any>, options): NgbModalRef {
const modalContentContext = new ModalContentContext();
const contentRef = this._getContentRef(content, modalContentContext);
const activeModal = new NgbActiveModal();
const contentRef = this._getContentRef(content, activeModal);
const windowCmptRef =
this._viewContainerRef.createComponent(this._windowFactory, 0, this._injector, contentRef.nodes);
let backdropCmptRef: ComponentRef<NgbModalBackdrop>;
Expand All @@ -49,8 +45,8 @@ export class NgbModalContainer {
}
ngbModalRef = new NgbModalRef(this._viewContainerRef, windowCmptRef, backdropCmptRef, contentRef.viewRef);

modalContentContext.close = (result: any) => { ngbModalRef.close(result); };
modalContentContext.dismiss = (reason: any) => { ngbModalRef.dismiss(reason); };
activeModal.close = (result: any) => { ngbModalRef.close(result); };
activeModal.dismiss = (reason: any) => { ngbModalRef.dismiss(reason); };

this._applyWindowOptions(windowCmptRef.instance, options);

Expand All @@ -65,14 +61,20 @@ export class NgbModalContainer {
});
}

private _getContentRef(content: string | TemplateRef<any>, context: ModalContentContext): ContentRef {
private _getContentRef(content: any, context: NgbActiveModal): ContentRef {
if (!content) {
return new ContentRef([]);
} else if (content instanceof TemplateRef) {
const viewRef = this._viewContainerRef.createEmbeddedView(<TemplateRef<ModalContentContext>>content, context);
const viewRef = this._viewContainerRef.createEmbeddedView(<TemplateRef<NgbActiveModal>>content, context);
return new ContentRef([viewRef.rootNodes], viewRef);
} else {
} else if (isString(content)) {
return new ContentRef([[this._renderer.createText(null, `${content}`)]]);
} else {
const contentCmptFactory = this._componentFactoryResolver.resolveComponentFactory(content);
const modalContentInjector =
ReflectiveInjector.resolveAndCreate([{provide: NgbActiveModal, useValue: context}], this._injector);
const componentRef = this._viewContainerRef.createComponent(contentCmptFactory, 0, modalContentInjector);
return new ContentRef([[componentRef.location.nativeElement]], componentRef.hostView, componentRef);
}
}
}
18 changes: 18 additions & 0 deletions src/modal/modal-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@ import {Injectable, ComponentRef, ViewRef, ViewContainerRef} from '@angular/core
import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';


/**
* A reference to an active (currently opened) modal. Instances of this class
* can be injected into components passed as modal content.
*/
@Injectable()
export class NgbActiveModal {
/**
* Can be used to close a modal, passing an optional result.
*/
close(result?: any): void {}

/**
* Can be used to dismiss a modal, passing an optional reason.
*/
dismiss(reason?: any): void {}
}

/**
* A reference to a newly opened modal.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/modal/modal-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {NgbModalContainer} from './modal-container';
export class NgbModalStack {
private modalContainer: NgbModalContainer;

open(content: string | TemplateRef<any>, options = {}): NgbModalRef {
open(content: any, options = {}): NgbModalRef {
if (!this.modalContainer) {
throw new Error(
'Missing modal container, add <template ngbModalContainer></template> to one of your application templates.');
Expand Down
2 changes: 1 addition & 1 deletion src/modal/modal.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {NgbModalStack} from './modal-stack';
import {NgbModal} from './modal';

export {NgbModal, NgbModalOptions} from './modal';
export {NgbModalRef} from './modal-ref';
export {NgbModalRef, NgbActiveModal} from './modal-ref';
export {ModalDismissReasons} from './modal-dismiss-reasons';

@NgModule({
Expand Down
49 changes: 45 additions & 4 deletions src/modal/modal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Component, Injectable, ViewChild, OnDestroy} from '@angular/core';
import {Component, Injectable, ViewChild, OnDestroy, NgModule} from '@angular/core';
import {TestBed, ComponentFixture} from '@angular/core/testing';
import {By} from '@angular/platform-browser';

import {NgbModalModule, NgbModal, NgbModalRef} from './modal.module';
import {NgbModalModule, NgbModal, NgbActiveModal, NgbModalRef} from './modal.module';

const NOOP = () => {};

Expand Down Expand Up @@ -68,8 +68,7 @@ describe('ngb-modal', () => {
});

beforeEach(() => {
TestBed.configureTestingModule(
{declarations: [TestComponent, DestroyableCmpt], imports: [NgbModalModule.forRoot()], providers: [SpyService]});
TestBed.configureTestingModule({imports: [NgbModalTestModule]});
fixture = TestBed.createComponent(TestComponent);
});

Expand Down Expand Up @@ -108,6 +107,29 @@ describe('ngb-modal', () => {
expect(spyService.called).toBeTruthy();
});

it('should open and close modal from a component type', () => {
const spyService = fixture.debugElement.injector.get(SpyService);
const modalInstance = fixture.componentInstance.openCmpt(DestroyableCmpt);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveModal('Some content');
expect(spyService.called).toBeFalsy();

modalInstance.close('some result');
fixture.detectChanges();
expect(fixture.nativeElement).not.toHaveModal();
expect(spyService.called).toBeTruthy();
});

it('should inject active modal ref when component is used as content', () => {
fixture.componentInstance.openCmpt(WithActiveModalCmpt);
fixture.detectChanges();
expect(fixture.nativeElement).toHaveModal('Close');

fixture.debugElement.query(By.css('button.closeFromInside')).triggerEventHandler('click', {});
fixture.detectChanges();
expect(fixture.nativeElement).not.toHaveModal();
});

it('should open and close modal from inside', () => {
fixture.componentInstance.openTplClose();
fixture.detectChanges();
Expand Down Expand Up @@ -358,6 +380,14 @@ export class DestroyableCmpt implements OnDestroy {
ngOnDestroy(): void { this._spyService.called = true; }
}

@Component(
{selector: 'modal-content-cmpt', template: '<button class="closeFromInside" (click)="close()">Close</button>'})
export class WithActiveModalCmpt {
constructor(public activeModal: NgbActiveModal) {}

close() { this.activeModal.close('from inside'); }
}

@Component({
selector: 'test-cmpt',
template: `
Expand Down Expand Up @@ -389,7 +419,18 @@ class TestComponent {
}
}
openTpl(options?: Object) { return this.modalService.open(this.tplContent, options); }
openCmpt(cmptType: any, options?: Object) { return this.modalService.open(cmptType, options); }
openDestroyableTpl(options?: Object) { return this.modalService.open(this.tplDestroyableContent, options); }
openTplClose(options?: Object) { return this.modalService.open(this.tplContentWithClose, options); }
openTplDismiss(options?: Object) { return this.modalService.open(this.tplContentWithDismiss, options); }
}

@NgModule({
declarations: [TestComponent, DestroyableCmpt, WithActiveModalCmpt],
exports: [TestComponent, DestroyableCmpt],
imports: [NgbModalModule.forRoot()],
entryComponents: [DestroyableCmpt, WithActiveModalCmpt],
providers: [SpyService]
})
class NgbModalTestModule {
}
9 changes: 5 additions & 4 deletions src/modal/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ export class NgbModal {
constructor(private _modalStack: NgbModalStack) {}

/**
* Opens a new modal window with the specified content and using supplied options.
* Opens a new modal window with the specified content and using supplied options. Content can be provided
* as a TemplateRef or a component type. If you pass a component type as content than instances of those
* components can be injected with an instance of the NgbActiveModal class. You can use methods on the
* NgbActiveModal class to close / dismiss modals from "inside" of a component.
*/
open(content: string | TemplateRef<any>, options: NgbModalOptions = {}): NgbModalRef {
return this._modalStack.open(content, options);
}
open(content: any, options: NgbModalOptions = {}): NgbModalRef { return this._modalStack.open(content, options); }
}
2 changes: 1 addition & 1 deletion src/util/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@angular/core';

export class ContentRef {
constructor(public nodes: any[], public viewRef?: ViewRef) {}
constructor(public nodes: any[], public viewRef?: ViewRef, public componentRef?: ComponentRef<any>) {}
}

export class PopupService<T> {
Expand Down

0 comments on commit ecdd3a0

Please sign in to comment.