Skip to content

Commit

Permalink
feat(model): introduce 'container' option
Browse files Browse the repository at this point in the history
This is major refactoring of the NgbModalService that makes it
easier to use (we no longer need to add ngbModalContainer) and
fixes 2 existing issues.

BREAKING CHANGE:

The `ngbModalContainer` directive is no longer needed and was
removed from this project. Just remove any references to the
`<template ngbModalContainer></template>` from your projects.

Closes #1018
Closes #1264

Closes #1373
  • Loading branch information
pkozlowski-opensource committed Mar 13, 2017
1 parent 79dd101 commit 743db91
Show file tree
Hide file tree
Showing 10 changed files with 212 additions and 156 deletions.
7 changes: 0 additions & 7 deletions demo/src/app/components/modal/modal.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,11 @@ import {DEMO_SNIPPETS} from './demos';
@Component({
selector: 'ngbd-modal',
template: `
<template ngbModalContainer></template>
<ngbd-content-wrapper component="Modal">
<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
<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" [snippets]="snippets" component="modal" demo="basic">
<ngbd-modal-basic></ngbd-modal-basic>
</ngbd-example-box>
Expand Down
2 changes: 1 addition & 1 deletion misc/plunk-gen.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { ${demoImports} } from '${demoImport}';
This is a demo plnkr forked from the <strong>ng-bootstrap</strong> project: Angular powered Bootstrap.
Visit <a href="https://ng-bootstrap.github.io/" target="_blank">https://ng-bootstrap.github.io</a> for more widgets and demos.
</p>
<hr>${componentName === 'modal' ? '\n\n <template ngbModalContainer></template>\n' : ''}
<hr>
<${demoSelector}></${demoSelector}>
</div>
Expand Down
86 changes: 0 additions & 86 deletions src/modal/modal-container.ts

This file was deleted.

18 changes: 12 additions & 6 deletions src/modal/modal-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Injectable, ComponentRef, ViewRef, ViewContainerRef} from '@angular/core';
import {Injectable, ComponentRef} from '@angular/core';

import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';
Expand Down Expand Up @@ -49,8 +49,8 @@ export class NgbModalRef {
result: Promise<any>;

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

this.result = new Promise((resolve, reject) => {
Expand Down Expand Up @@ -81,12 +81,18 @@ export class NgbModalRef {
}

private _removeModalElements() {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowCmptRef.hostView));
const windowNativeEl = this._windowCmptRef.location.nativeElement;
windowNativeEl.parentNode.removeChild(windowNativeEl);
this._windowCmptRef.destroy();

if (this._backdropCmptRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._backdropCmptRef.hostView));
const backdropNativeEl = this._backdropCmptRef.location.nativeElement;
backdropNativeEl.parentNode.removeChild(backdropNativeEl);
this._backdropCmptRef.destroy();
}

if (this._contentRef && this._contentRef.viewRef) {
this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._contentRef.viewRef));
this._contentRef.viewRef.destroy();
}

this._windowCmptRef = null;
Expand Down
9 changes: 0 additions & 9 deletions src/modal/modal-stack.spec.ts

This file was deleted.

94 changes: 84 additions & 10 deletions src/modal/modal-stack.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,94 @@
import {Injectable, Injector, ComponentFactoryResolver} from '@angular/core';
import {
ApplicationRef,
Injectable,
Injector,
ReflectiveInjector,
ComponentFactory,
ComponentFactoryResolver,
ComponentRef,
TemplateRef
} from '@angular/core';

import {NgbModalRef} from './modal-ref';
import {NgbModalContainer} from './modal-container';
import {ContentRef} from '../util/popup';
import {isDefined, isString} from '../util/util';

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

@Injectable()
export class NgbModalStack {
private modalContainer: NgbModalContainer;
private _backdropFactory: ComponentFactory<NgbModalBackdrop>;
private _windowFactory: ComponentFactory<NgbModalWindow>;

constructor(
private _applicationRef: ApplicationRef, private _injector: Injector,
private _componentFactoryResolver: ComponentFactoryResolver) {
this._backdropFactory = _componentFactoryResolver.resolveComponentFactory(NgbModalBackdrop);
this._windowFactory = _componentFactoryResolver.resolveComponentFactory(NgbModalWindow);
}

open(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, options): NgbModalRef {
const containerSelector = options.container || 'body';
const containerEl = document.querySelector(containerSelector);

if (!containerEl) {
throw new Error(`The specified modal container "${containerSelector}" was not found in the DOM.`);
}

const activeModal = new NgbActiveModal();
const contentRef = this._getContentRef(moduleCFR, contentInjector, content, activeModal);

let windowCmptRef: ComponentRef<NgbModalWindow>;
let backdropCmptRef: ComponentRef<NgbModalBackdrop>;
let ngbModalRef: NgbModalRef;

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

if (options.backdrop !== false) {
backdropCmptRef = this._backdropFactory.create(this._injector);
this._applicationRef.attachView(backdropCmptRef.hostView);
containerEl.appendChild(backdropCmptRef.location.nativeElement);
}
windowCmptRef = this._windowFactory.create(this._injector, contentRef.nodes);
this._applicationRef.attachView(windowCmptRef.hostView);
containerEl.appendChild(windowCmptRef.location.nativeElement);

ngbModalRef = new NgbModalRef(windowCmptRef, contentRef, backdropCmptRef);

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

this._applyWindowOptions(windowCmptRef.instance, options);

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

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

private _getContentRef(
moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any,
context: NgbActiveModal): ContentRef {
if (!content) {
return new ContentRef([]);
} else if (content instanceof TemplateRef) {
const viewRef = content.createEmbeddedView(context);
this._applicationRef.attachView(viewRef);
return new ContentRef([viewRef.rootNodes], viewRef);
} else if (isString(content)) {
return new ContentRef([[document.createTextNode(`${content}`)]]);
} else {
const contentCmptFactory = moduleCFR.resolveComponentFactory(content);
const modalContentInjector =
ReflectiveInjector.resolveAndCreate([{provide: NgbActiveModal, useValue: context}], contentInjector);
const componentRef = contentCmptFactory.create(modalContentInjector);
this._applicationRef.attachView(componentRef.hostView);
return new ContentRef([[componentRef.location.nativeElement]], componentRef.hostView, componentRef);
}
}
}
6 changes: 2 additions & 4 deletions src/modal/modal.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {NgModule, ModuleWithProviders} from '@angular/core';

import {NgbModalContainer} from './modal-container';
import {NgbModalBackdrop} from './modal-backdrop';
import {NgbModalWindow} from './modal-window';
import {NgbModalStack} from './modal-stack';
Expand All @@ -11,10 +10,9 @@ export {NgbModalRef, NgbActiveModal} from './modal-ref';
export {ModalDismissReasons} from './modal-dismiss-reasons';

@NgModule({
declarations: [NgbModalContainer, NgbModalBackdrop, NgbModalWindow],
declarations: [NgbModalBackdrop, NgbModalWindow],
entryComponents: [NgbModalBackdrop, NgbModalWindow],
providers: [NgbModal],
exports: [NgbModalContainer]
providers: [NgbModal]
})
export class NgbModalModule {
static forRoot(): ModuleWithProviders { return {ngModule: NgbModalModule, providers: [NgbModal, NgbModalStack]}; }
Expand Down

0 comments on commit 743db91

Please sign in to comment.