Skip to content

Commit 3ef99c5

Browse files
feat(modal): add initial version of the modal service
Closes #3
1 parent 4115ee4 commit 3ef99c5

24 files changed

+796
-80
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<template #content let-c="close" let-d="dismiss">
2+
<div class="modal-header">
3+
<button type="button" class="close" aria-label="Close" (click)="d('Cross click')">
4+
<span aria-hidden="true">&times;</span>
5+
</button>
6+
<h4 class="modal-title">Modal title</h4>
7+
</div>
8+
<div class="modal-body">
9+
<p>One fine body&hellip;</p>
10+
</div>
11+
<div class="modal-footer">
12+
<button type="button" class="btn btn-secondary" (click)="c('Close click')">Close</button>
13+
</div>
14+
</template>
15+
16+
<button class="btn btn-lg btn-outline-primary" (click)="open(content)">Launch demo modal</button>
17+
18+
<hr>
19+
20+
<pre>{{closeResult}}</pre>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {Component} from '@angular/core';
2+
3+
import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap';
4+
5+
@Component({
6+
selector: 'ngbd-modal-basic',
7+
templateUrl: './modal-basic.html'
8+
})
9+
export class NgbdModalBasic {
10+
closeResult: string;
11+
12+
constructor(private modalService: NgbModal) {}
13+
14+
open(content) {
15+
this.modalService.open(content).result.then((result) => {
16+
this.closeResult = `Closed with: ${result}`;
17+
}, (reason) => {
18+
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
19+
});
20+
}
21+
22+
private getDismissReason(reason: any): string {
23+
if (reason === ModalDismissReasons.ESC) {
24+
return 'by pressing ESC';
25+
} else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
26+
return 'by clicking on a backdrop';
27+
} else {
28+
return `with: ${reason}`;
29+
}
30+
}
31+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {NgbdModalBasic} from './basic/modal-basic';
2+
3+
export const DEMO_DIRECTIVES = [NgbdModalBasic];
4+
5+
export const DEMO_SNIPPETS = {
6+
basic: {
7+
code: require('!!prismjs?lang=typescript!./basic/modal-basic'),
8+
markup: require('!!prismjs?lang=markup!./basic/modal-basic.html')}
9+
};
10+
Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
export * from './modal.component';
22

33
import {NgModule} from '@angular/core';
4-
import {CommonModule} from '@angular/common';
5-
64
import {NgbdModal} from './modal.component';
5+
import {NgbdSharedModule} from '../../shared';
6+
import {NgbdComponentsSharedModule} from '../shared';
7+
import {DEMO_DIRECTIVES} from './demos';
78

89
@NgModule({
9-
imports: [CommonModule],
10+
imports: [NgbdSharedModule, NgbdComponentsSharedModule],
1011
exports: [NgbdModal],
11-
declarations: [NgbdModal]
12+
declarations: [NgbdModal, ...DEMO_DIRECTIVES]
1213
})
1314
export class NgbdModalModule {}
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,25 @@
11
import {Component} from '@angular/core';
22

3+
import {DEMO_SNIPPETS} from './demos';
4+
35
@Component({
46
selector: 'ngbd-modal',
57
template: `
6-
<ngbd-api-docs directive="NgbModal"></ngbd-api-docs>
8+
<template ngbModalContainer></template>
9+
<ngbd-content-wrapper component="Modal">
10+
<ngbd-api-docs directive="NgbModal"></ngbd-api-docs>
11+
<ngb-alert [dismissible]="false">
12+
<strong>Heads up!</strong>
13+
The <code>NgbModal</code> service needs a container element with the <code>ngbModalContainer</code> directive. The
14+
<code>ngbModalContainer</code> directive marks the place in the DOM where modals are opened. Be sure to add
15+
<code>&lt;template ngbModalContainer&gt;&lt;/template&gt;</code> somewhere under your application root element.
16+
</ngb-alert>
17+
<ngbd-example-box demoTitle="Modal with default options" [htmlSnippet]="snippets.basic.markup" [tsSnippet]="snippets.basic.code">
18+
<ngbd-modal-basic></ngbd-modal-basic>
19+
</ngbd-example-box>
20+
</ngbd-content-wrapper>
721
`
822
})
9-
export class NgbdModal {}
23+
export class NgbdModal {
24+
snippets = DEMO_SNIPPETS;
25+
}

demo/src/app/components/shared/api-docs/api-docs.component.html

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,23 @@ <h2>
77
angularticsCategory="{{ demoTitle }}">{{apiDocs.className}}</a>
88
</h2>
99
<p>{{apiDocs.description}}</p>
10-
<table class="table table-sm table-hover">
11-
<tbody>
12-
<tr>
13-
<td class="col-md-3">Selector</td>
14-
<td class="col-md-9"><code>{{apiDocs.selector}}</code></td>
15-
</tr>
16-
<tr *ngIf="apiDocs.exportAs">
17-
<td class="col-md-3">Exported as</td>
18-
<td class="col-md-9"><code>{{apiDocs.exportAs}}</code></td>
19-
</tr>
20-
</tbody>
21-
</table>
2210

23-
<template [ngIf]="apiDocs.inputs.length">
11+
<template [ngIf]="isDirective">
12+
<table class="table table-sm table-hover">
13+
<tbody>
14+
<tr *ngIf="apiDocs.selector">
15+
<td class="col-md-3">Selector</td>
16+
<td class="col-md-9"><code>{{apiDocs.selector}}</code></td>
17+
</tr>
18+
<tr *ngIf="apiDocs.exportAs">
19+
<td class="col-md-3">Exported as</td>
20+
<td class="col-md-9"><code>{{apiDocs.exportAs}}</code></td>
21+
</tr>
22+
</tbody>
23+
</table>
24+
</template>
25+
26+
<template [ngIf]="isDirective && apiDocs.inputs.length">
2427
<section>
2528
<h3 id="inputs">Inputs</h3>
2629
<table class="table table-sm table-hover">
@@ -40,7 +43,7 @@ <h3 id="inputs">Inputs</h3>
4043
</section>
4144
</template>
4245

43-
<template [ngIf]="apiDocs.outputs.length">
46+
<template [ngIf]="isDirective && apiDocs.outputs.length">
4447
<section>
4548
<h3 id="outputs">Outputs</h3>
4649
<table class="table table-sm table-hover">
@@ -54,7 +57,7 @@ <h3 id="outputs">Outputs</h3>
5457
</section>
5558
</template>
5659

57-
<template [ngIf]="apiDocs.exportAs && apiDocs.methods.length">
60+
<template [ngIf]="isDirective ? apiDocs.methods.length && apiDocs.exportAs : apiDocs.methods.length">
5861
<section>
5962
<h3 id="methods">Methods</h3>
6063
<table class="table table-sm table-hover">

demo/src/app/components/shared/api-docs/api-docs.component.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import docs from '../../../../api-docs';
77
templateUrl: './api-docs.component.html'
88
})
99
export class NgbdApiDocs {
10+
isDirective;
11+
1012
apiDocs;
1113
@Input() set directive(directiveName) {
1214
this.apiDocs = docs[directiveName];
15+
this.isDirective = this.apiDocs.selector;
1316
};
1417

1518
private _methodSignature(method) {

demo/src/app/shared/side-nav/side-nav.component.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export class SideNavComponent {
1212
'Carousel',
1313
'Collapse',
1414
'Dropdown',
15+
'Modal',
1516
'Pagination',
1617
'Popover',
1718
'Progressbar',

src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {NgbButtonsModule} from './buttons/radio.module';
66
import {NgbCarouselModule} from './carousel/carousel.module';
77
import {NgbCollapseModule} from './collapse/collapse.module';
88
import {NgbDropdownModule} from './dropdown/dropdown.module';
9+
import {NgbModalModule, NgbModal, NgbModalOptions, NgbModalRef, ModalDismissReasons} from './modal/modal.module';
910
import {NgbPaginationModule} from './pagination/pagination.module';
1011
import {NgbPopoverModule} from './popover/popover.module';
1112
import {NgbProgressbarModule} from './progressbar/progressbar.module';
@@ -16,13 +17,14 @@ import {NgbTooltipModule} from './tooltip/tooltip.module';
1617
import {NgbTypeaheadModule} from './typeahead/typeahead.module';
1718

1819
export {NgbPanelChangeEvent} from './accordion/accordion.module';
20+
export {NgbModal, NgbModalOptions, NgbModalRef, ModalDismissReasons} from './modal/modal.module';
1921
export {NgbTabChangeEvent} from './tabset/tabset.module';
2022

2123
@NgModule({
2224
exports: [
2325
NgbAccordionModule, NgbAlertModule, NgbButtonsModule, NgbCarouselModule, NgbCollapseModule, NgbDropdownModule,
24-
NgbPaginationModule, NgbPopoverModule, NgbProgressbarModule, NgbRatingModule, NgbTabsetModule, NgbTimepickerModule,
25-
NgbTooltipModule, NgbTypeaheadModule
26+
NgbModalModule, NgbPaginationModule, NgbPopoverModule, NgbProgressbarModule, NgbRatingModule, NgbTabsetModule,
27+
NgbTimepickerModule, NgbTooltipModule, NgbTypeaheadModule
2628
]
2729
})
2830
export class NgbModule {

src/modal/modal_backdrop.spec.ts renamed to src/modal/modal-backdrop.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {TestBed} from '@angular/core/testing';
22

3-
import {NgbModalBackdrop} from './modal_backdrop';
3+
import {NgbModalBackdrop} from './modal-backdrop';
44

55
describe('ngb-modal-backdrop', () => {
66

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component} from '@angular/core';
22

3-
@Component({selector: 'ngb-modal-backdrop', template: '', host: {'class': 'modal-backdrop'}})
3+
@Component({selector: 'ngb-modal-backdrop', template: '', host: {'class': 'modal-backdrop fade in'}})
44
export class NgbModalBackdrop {
55
}

src/modal/modal-container.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
Directive,
3+
Injector,
4+
Renderer,
5+
TemplateRef,
6+
ViewContainerRef,
7+
ComponentFactoryResolver,
8+
ComponentFactory,
9+
ComponentRef
10+
} from '@angular/core';
11+
12+
import {isDefined} from '../util/util';
13+
14+
import {NgbModalBackdrop} from './modal-backdrop';
15+
import {NgbModalWindow} from './modal-window';
16+
import {NgbModalStack} from './modal-stack';
17+
import {NgbModalRef} from './modal-ref';
18+
19+
class ModalContentContext {
20+
close(result?: any) {}
21+
dismiss(reason?: any) {}
22+
}
23+
24+
@Directive({selector: 'template[ngbModalContainer]'})
25+
export class NgbModalContainer {
26+
private _backdropFactory: ComponentFactory<NgbModalBackdrop>;
27+
private _windowFactory: ComponentFactory<NgbModalWindow>;
28+
29+
constructor(
30+
private _injector: Injector, private _renderer: Renderer, private _viewContainerRef: ViewContainerRef,
31+
componentFactoryResolver: ComponentFactoryResolver, ngbModalStack: NgbModalStack) {
32+
this._backdropFactory = componentFactoryResolver.resolveComponentFactory(NgbModalBackdrop);
33+
this._windowFactory = componentFactoryResolver.resolveComponentFactory(NgbModalWindow);
34+
35+
ngbModalStack.registerContainer(this);
36+
}
37+
38+
open(content: string | TemplateRef<any>, options): NgbModalRef {
39+
const modalContentContext = new ModalContentContext();
40+
const nodes = this._getContentNodes(content, modalContentContext);
41+
const windowCmptRef = this._viewContainerRef.createComponent(this._windowFactory, 0, this._injector, nodes);
42+
let backdropCmptRef: ComponentRef<NgbModalBackdrop>;
43+
let ngbModalRef: NgbModalRef;
44+
45+
if (options.backdrop !== false) {
46+
backdropCmptRef = this._viewContainerRef.createComponent(this._backdropFactory, 0, this._injector);
47+
}
48+
ngbModalRef = new NgbModalRef(this._viewContainerRef, windowCmptRef, backdropCmptRef);
49+
50+
modalContentContext.close = (result: any) => { ngbModalRef.close(result); };
51+
modalContentContext.dismiss = (reason: any) => { ngbModalRef.dismiss(reason); };
52+
53+
this._applyWindowOptions(windowCmptRef.instance, options);
54+
55+
return ngbModalRef;
56+
}
57+
58+
private _applyWindowOptions(windowInstance: NgbModalWindow, options: Object): void {
59+
['backdrop', 'keyboard', 'size'].forEach((optionName: string) => {
60+
if (isDefined(options[optionName])) {
61+
windowInstance[optionName] = options[optionName];
62+
}
63+
});
64+
}
65+
66+
private _getContentNodes(content: string | TemplateRef<any>, context: ModalContentContext): any[] {
67+
if (!content) {
68+
return [];
69+
} else if (content instanceof TemplateRef) {
70+
return [this._viewContainerRef.createEmbeddedView(<TemplateRef<ModalContentContext>>content, context).rootNodes];
71+
} else {
72+
return [[this._renderer.createText(null, `${content}`)]];
73+
}
74+
}
75+
}

src/modal/modal-ref.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import {ComponentRef, ViewContainerRef} from '@angular/core';
2+
3+
import {NgbModalBackdrop} from './modal-backdrop';
4+
import {NgbModalWindow} from './modal-window';
5+
6+
/**
7+
* A reference to a newly opened modal.
8+
*/
9+
export class NgbModalRef {
10+
private _resolve: (result?: any) => void;
11+
private _reject: (reason?: any) => void;
12+
13+
/**
14+
* A promise that is resolved when a modal is closed and rejected when a modal is dismissed.
15+
*/
16+
result: Promise<any>;
17+
18+
constructor(
19+
private _viewContainerRef: ViewContainerRef, private _windowCmptRef: ComponentRef<NgbModalWindow>,
20+
private _backdropCmptRef?: ComponentRef<NgbModalBackdrop>) {
21+
_windowCmptRef.instance.dismissEvent.subscribe((reason: any) => { this.dismiss(reason); });
22+
23+
this.result = new Promise((resolve, reject) => {
24+
this._resolve = resolve;
25+
this._reject = reject;
26+
});
27+
}
28+
29+
/**
30+
* Can be used to close a modal, passing an optional result.
31+
*/
32+
close(result?: any) {
33+
if (this._windowCmptRef) {
34+
this._resolve(result);
35+
this._removeModalElements();
36+
}
37+
}
38+
39+
/**
40+
* Can be used to dismiss a modal, passing an optional reason.
41+
*/
42+
dismiss(reason?: any) {
43+
if (this._windowCmptRef) {
44+
this._reject(reason);
45+
this._removeModalElements();
46+
}
47+
}
48+
49+
private _removeModalElements() {
50+
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowCmptRef.hostView));
51+
if (this._backdropCmptRef) {
52+
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._backdropCmptRef.hostView));
53+
}
54+
55+
this._windowCmptRef = null;
56+
this._backdropCmptRef = null;
57+
}
58+
}

src/modal/modal-stack.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import {NgbModalStack} from './modal-stack';
2+
3+
describe('modal stack', () => {
4+
5+
it('should throw if a container element was not registered', () => {
6+
const modalStack = new NgbModalStack();
7+
expect(() => { modalStack.open('foo'); }).toThrowError();
8+
});
9+
});

src/modal/modal-stack.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import {Injectable, TemplateRef} from '@angular/core';
2+
3+
import {NgbModalRef} from './modal-ref';
4+
import {NgbModalContainer} from './modal-container';
5+
6+
@Injectable()
7+
export class NgbModalStack {
8+
private modalContainer: NgbModalContainer;
9+
10+
open(content: string | TemplateRef<any>, options = {}): NgbModalRef {
11+
if (!this.modalContainer) {
12+
throw new Error(
13+
'Missing modal container, add <template ngbModalContainer></template> to one of your application templates.');
14+
}
15+
16+
return this.modalContainer.open(content, options);
17+
}
18+
19+
registerContainer(modalContainer: NgbModalContainer) { this.modalContainer = modalContainer; }
20+
}

0 commit comments

Comments
 (0)