Skip to content

Commit

Permalink
feat(modal): add initial version of the modal service
Browse files Browse the repository at this point in the history
Closes #3
  • Loading branch information
pkozlowski-opensource committed Aug 29, 2016
1 parent 4115ee4 commit 3ef99c5
Show file tree
Hide file tree
Showing 24 changed files with 796 additions and 80 deletions.
20 changes: 20 additions & 0 deletions demo/src/app/components/modal/demos/basic/modal-basic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<template #content let-c="close" let-d="dismiss">
<div class="modal-header">
<button type="button" class="close" aria-label="Close" (click)="d('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)="c('Close click')">Close</button>
</div>
</template>

<button class="btn btn-lg btn-outline-primary" (click)="open(content)">Launch demo modal</button>

<hr>

<pre>{{closeResult}}</pre>
31 changes: 31 additions & 0 deletions demo/src/app/components/modal/demos/basic/modal-basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {Component} from '@angular/core';

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

@Component({
selector: 'ngbd-modal-basic',
templateUrl: './modal-basic.html'
})
export class NgbdModalBasic {
closeResult: string;

constructor(private modalService: NgbModal) {}

open(content) {
this.modalService.open(content).result.then((result) => {
this.closeResult = `Closed with: ${result}`;
}, (reason) => {
this.closeResult = `Dismissed ${this.getDismissReason(reason)}`;
});
}

private getDismissReason(reason: any): string {
if (reason === ModalDismissReasons.ESC) {
return 'by pressing ESC';
} else if (reason === ModalDismissReasons.BACKDROP_CLICK) {
return 'by clicking on a backdrop';
} else {
return `with: ${reason}`;
}
}
}
10 changes: 10 additions & 0 deletions demo/src/app/components/modal/demos/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {NgbdModalBasic} from './basic/modal-basic';

export const DEMO_DIRECTIVES = [NgbdModalBasic];

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

9 changes: 5 additions & 4 deletions demo/src/app/components/modal/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
export * from './modal.component';

import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';

import {NgbdModal} from './modal.component';
import {NgbdSharedModule} from '../../shared';
import {NgbdComponentsSharedModule} from '../shared';
import {DEMO_DIRECTIVES} from './demos';

@NgModule({
imports: [CommonModule],
imports: [NgbdSharedModule, NgbdComponentsSharedModule],
exports: [NgbdModal],
declarations: [NgbdModal]
declarations: [NgbdModal, ...DEMO_DIRECTIVES]
})
export class NgbdModalModule {}
20 changes: 18 additions & 2 deletions demo/src/app/components/modal/modal.component.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import {Component} from '@angular/core';

import {DEMO_SNIPPETS} from './demos';

@Component({
selector: 'ngbd-modal',
template: `
<ngbd-api-docs directive="NgbModal"></ngbd-api-docs>
<template ngbModalContainer></template>
<ngbd-content-wrapper component="Modal">
<ngbd-api-docs directive="NgbModal"></ngbd-api-docs>
<ngb-alert [dismissible]="false">
<strong>Heads up!</strong>
The <code>NgbModal</code> service needs a container element with the <code>ngbModalContainer</code> directive. The
<code>ngbModalContainer</code> directive marks the place in the DOM where modals are opened. Be sure to add
<code>&lt;template ngbModalContainer&gt;&lt;/template&gt;</code> somewhere under your application root element.
</ngb-alert>
<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-content-wrapper>
`
})
export class NgbdModal {}
export class NgbdModal {
snippets = DEMO_SNIPPETS;
}
33 changes: 18 additions & 15 deletions demo/src/app/components/shared/api-docs/api-docs.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,23 @@ <h2>
angularticsCategory="{{ demoTitle }}">{{apiDocs.className}}</a>
</h2>
<p>{{apiDocs.description}}</p>
<table class="table table-sm table-hover">
<tbody>
<tr>
<td class="col-md-3">Selector</td>
<td class="col-md-9"><code>{{apiDocs.selector}}</code></td>
</tr>
<tr *ngIf="apiDocs.exportAs">
<td class="col-md-3">Exported as</td>
<td class="col-md-9"><code>{{apiDocs.exportAs}}</code></td>
</tr>
</tbody>
</table>

<template [ngIf]="apiDocs.inputs.length">
<template [ngIf]="isDirective">
<table class="table table-sm table-hover">
<tbody>
<tr *ngIf="apiDocs.selector">
<td class="col-md-3">Selector</td>
<td class="col-md-9"><code>{{apiDocs.selector}}</code></td>
</tr>
<tr *ngIf="apiDocs.exportAs">
<td class="col-md-3">Exported as</td>
<td class="col-md-9"><code>{{apiDocs.exportAs}}</code></td>
</tr>
</tbody>
</table>
</template>

<template [ngIf]="isDirective && apiDocs.inputs.length">
<section>
<h3 id="inputs">Inputs</h3>
<table class="table table-sm table-hover">
Expand All @@ -40,7 +43,7 @@ <h3 id="inputs">Inputs</h3>
</section>
</template>

<template [ngIf]="apiDocs.outputs.length">
<template [ngIf]="isDirective && apiDocs.outputs.length">
<section>
<h3 id="outputs">Outputs</h3>
<table class="table table-sm table-hover">
Expand All @@ -54,7 +57,7 @@ <h3 id="outputs">Outputs</h3>
</section>
</template>

<template [ngIf]="apiDocs.exportAs && apiDocs.methods.length">
<template [ngIf]="isDirective ? apiDocs.methods.length && apiDocs.exportAs : apiDocs.methods.length">
<section>
<h3 id="methods">Methods</h3>
<table class="table table-sm table-hover">
Expand Down
3 changes: 3 additions & 0 deletions demo/src/app/components/shared/api-docs/api-docs.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import docs from '../../../../api-docs';
templateUrl: './api-docs.component.html'
})
export class NgbdApiDocs {
isDirective;

apiDocs;
@Input() set directive(directiveName) {
this.apiDocs = docs[directiveName];
this.isDirective = this.apiDocs.selector;
};

private _methodSignature(method) {
Expand Down
1 change: 1 addition & 0 deletions demo/src/app/shared/side-nav/side-nav.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class SideNavComponent {
'Carousel',
'Collapse',
'Dropdown',
'Modal',
'Pagination',
'Popover',
'Progressbar',
Expand Down
6 changes: 4 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {NgbButtonsModule} from './buttons/radio.module';
import {NgbCarouselModule} from './carousel/carousel.module';
import {NgbCollapseModule} from './collapse/collapse.module';
import {NgbDropdownModule} from './dropdown/dropdown.module';
import {NgbModalModule, NgbModal, NgbModalOptions, NgbModalRef, ModalDismissReasons} from './modal/modal.module';
import {NgbPaginationModule} from './pagination/pagination.module';
import {NgbPopoverModule} from './popover/popover.module';
import {NgbProgressbarModule} from './progressbar/progressbar.module';
Expand All @@ -16,13 +17,14 @@ import {NgbTooltipModule} from './tooltip/tooltip.module';
import {NgbTypeaheadModule} from './typeahead/typeahead.module';

export {NgbPanelChangeEvent} from './accordion/accordion.module';
export {NgbModal, NgbModalOptions, NgbModalRef, ModalDismissReasons} from './modal/modal.module';
export {NgbTabChangeEvent} from './tabset/tabset.module';

@NgModule({
exports: [
NgbAccordionModule, NgbAlertModule, NgbButtonsModule, NgbCarouselModule, NgbCollapseModule, NgbDropdownModule,
NgbPaginationModule, NgbPopoverModule, NgbProgressbarModule, NgbRatingModule, NgbTabsetModule, NgbTimepickerModule,
NgbTooltipModule, NgbTypeaheadModule
NgbModalModule, NgbPaginationModule, NgbPopoverModule, NgbProgressbarModule, NgbRatingModule, NgbTabsetModule,
NgbTimepickerModule, NgbTooltipModule, NgbTypeaheadModule
]
})
export class NgbModule {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {TestBed} from '@angular/core/testing';

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component} from '@angular/core';

@Component({selector: 'ngb-modal-backdrop', template: '', host: {'class': 'modal-backdrop'}})
@Component({selector: 'ngb-modal-backdrop', template: '', host: {'class': 'modal-backdrop fade in'}})
export class NgbModalBackdrop {
}
75 changes: 75 additions & 0 deletions src/modal/modal-container.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
Directive,
Injector,
Renderer,
TemplateRef,
ViewContainerRef,
ComponentFactoryResolver,
ComponentFactory,
ComponentRef
} from '@angular/core';

import {isDefined} from '../util/util';

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) {}
}

@Directive({selector: 'template[ngbModalContainer]'})
export class NgbModalContainer {
private _backdropFactory: ComponentFactory<NgbModalBackdrop>;
private _windowFactory: ComponentFactory<NgbModalWindow>;

constructor(
private _injector: Injector, private _renderer: Renderer, private _viewContainerRef: ViewContainerRef,
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 nodes = this._getContentNodes(content, modalContentContext);
const windowCmptRef = this._viewContainerRef.createComponent(this._windowFactory, 0, this._injector, nodes);
let backdropCmptRef: ComponentRef<NgbModalBackdrop>;
let ngbModalRef: NgbModalRef;

if (options.backdrop !== false) {
backdropCmptRef = this._viewContainerRef.createComponent(this._backdropFactory, 0, this._injector);
}
ngbModalRef = new NgbModalRef(this._viewContainerRef, windowCmptRef, backdropCmptRef);

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

this._applyWindowOptions(windowCmptRef.instance, options);

return ngbModalRef;
}

private _applyWindowOptions(windowInstance: NgbModalWindow, options: Object): void {
['backdrop', 'keyboard', 'size'].forEach((optionName: string) => {
if (isDefined(options[optionName])) {
windowInstance[optionName] = options[optionName];
}
});
}

private _getContentNodes(content: string | TemplateRef<any>, context: ModalContentContext): any[] {
if (!content) {
return [];
} else if (content instanceof TemplateRef) {
return [this._viewContainerRef.createEmbeddedView(<TemplateRef<ModalContentContext>>content, context).rootNodes];
} else {
return [[this._renderer.createText(null, `${content}`)]];
}
}
}
File renamed without changes.
58 changes: 58 additions & 0 deletions src/modal/modal-ref.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {ComponentRef, ViewContainerRef} from '@angular/core';

import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';

/**
* A reference to a newly opened modal.
*/
export class NgbModalRef {
private _resolve: (result?: any) => void;
private _reject: (reason?: any) => void;

/**
* A promise that is resolved when a modal is closed and rejected when a modal is dismissed.
*/
result: Promise<any>;

constructor(
private _viewContainerRef: ViewContainerRef, private _windowCmptRef: ComponentRef<NgbModalWindow>,
private _backdropCmptRef?: ComponentRef<NgbModalBackdrop>) {
_windowCmptRef.instance.dismissEvent.subscribe((reason: any) => { this.dismiss(reason); });

this.result = new Promise((resolve, reject) => {
this._resolve = resolve;
this._reject = reject;
});
}

/**
* Can be used to close a modal, passing an optional result.
*/
close(result?: any) {
if (this._windowCmptRef) {
this._resolve(result);
this._removeModalElements();
}
}

/**
* Can be used to dismiss a modal, passing an optional reason.
*/
dismiss(reason?: any) {
if (this._windowCmptRef) {
this._reject(reason);
this._removeModalElements();
}
}

private _removeModalElements() {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowCmptRef.hostView));
if (this._backdropCmptRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._backdropCmptRef.hostView));
}

this._windowCmptRef = null;
this._backdropCmptRef = null;
}
}
9 changes: 9 additions & 0 deletions src/modal/modal-stack.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {NgbModalStack} from './modal-stack';

describe('modal stack', () => {

it('should throw if a container element was not registered', () => {
const modalStack = new NgbModalStack();
expect(() => { modalStack.open('foo'); }).toThrowError();
});
});
20 changes: 20 additions & 0 deletions src/modal/modal-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {Injectable, TemplateRef} from '@angular/core';

import {NgbModalRef} from './modal-ref';
import {NgbModalContainer} from './modal-container';

@Injectable()
export class NgbModalStack {
private modalContainer: NgbModalContainer;

open(content: string | TemplateRef<any>, options = {}): NgbModalRef {
if (!this.modalContainer) {
throw new Error(
'Missing modal container, add <template ngbModalContainer></template> to one of your application templates.');
}

return this.modalContainer.open(content, options);
}

registerContainer(modalContainer: NgbModalContainer) { this.modalContainer = modalContainer; }
}

4 comments on commit 3ef99c5

@masaanli
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When will this be inside a version which can be grapped from npm, just to be curious

@pkozlowski-opensource
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As soon as Angular 2 rc.6 is out we are going to cut alpha.3 release with the modal service.

@pkozlowski-opensource
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@masaanli we've just release alpha.3 with the modal service: https://ng-bootstrap.github.io/#/components/modal

@masaanli
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkozlowski-opensource Thank you, gonna implementate it! And maybe giving back some suggestions or thing where I'm stuggling about.

Please sign in to comment.