Skip to content

Commit

Permalink
feat: support for TemplateRef and literal string as modal content
Browse files Browse the repository at this point in the history
BREAKING CHANGE: until now a modal can only open a component, this change allows a modal to open A `ContainerContent` which is a type the can be `string`, `TemplateRef` or a component (`Type`). To support this feature some partially breaking changes were made in the `Modal` class
For developers using the library the `modal.open(componentType: any, config?: OverlayConfig)`, the 1st parameter is now called `content` and it's type is  `ContainerContent`: `modal.open(content: ContainerContent, config?: OverlayConfig)`.
For developers building plugins the same rule apply on the 2nd parameter of the`create` method.
 Also, the `createModal` method is deprecated, plugins should now use the `createBackdrop` and `createContainer` methods.
 Each container component supplied to `createContainer` must express `<ng-content` in it's template so the content (string, templateRef, component) can be injected into it.
  • Loading branch information
Shlomi Assaf (shlassaf) committed Aug 30, 2016
1 parent d115100 commit 2e6f6a0
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 60 deletions.
19 changes: 3 additions & 16 deletions src/components/angular2-modal/components/base-dynamic-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,29 +102,16 @@ export class BaseDynamicComponent implements OnDestroy {
*/
protected _addComponent<T>(type: any,
vcRef: ViewContainerRef,
bindings?: ResolvedReflectiveProvider[]): ComponentRef<T> {
bindings: ResolvedReflectiveProvider[] = [],
projectableNodes: any[][] = []): ComponentRef<T> {
const cmpRef =
createComponent(vcRef.injector.get(ComponentFactoryResolver), type, vcRef, bindings || []);
createComponent(vcRef.injector.get(ComponentFactoryResolver), type, vcRef, bindings, projectableNodes);

cmpRef.changeDetectorRef.detectChanges();

return cmpRef;
}

protected _addContent<T>(content: string | TemplateRef<any>,
vcRef: ViewContainerRef,
context: any): any[] {

if (!content) return [];

if (typeof content === 'string') {
return [[this.renderer.createText(null, `${content}`)]];
} else if (content instanceof TemplateRef) {
return vcRef.createEmbeddedView(content, context).rootNodes;
} else {

}
}

private onEnd(event: TransitionEvent | AnimationEvent): void {

Expand Down
6 changes: 4 additions & 2 deletions src/components/angular2-modal/framework/createComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import {
export function createComponent(cfr: ComponentFactoryResolver,
type: any,
vcr: ViewContainerRef,
bindings: ResolvedReflectiveProvider[]): ComponentRef<any> {
bindings: ResolvedReflectiveProvider[],
projectableNodes?: any[][]): ComponentRef<any> {
return vcr.createComponent(
cfr.resolveComponentFactory(type),
vcr.length,
getInjector(vcr, bindings)
getInjector(vcr, bindings),
projectableNodes
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/components/angular2-modal/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export {
ModalComponent,
OverlayRenderer,
OverlayConfig,
CloseGuard
CloseGuard,
ContainerContent
} from './models/tokens';

export { Modal, DOMOverlayRenderer } from './providers/index';
Expand Down
4 changes: 4 additions & 0 deletions src/components/angular2-modal/models/tokens.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
ComponentRef,
ViewContainerRef,
TemplateRef,
Type,
ResolvedReflectiveProvider
} from '@angular/core';

Expand All @@ -17,6 +19,8 @@ export enum DROP_IN_TYPE {

export type WideVCRef = ViewContainerRef | string;

export type ContainerContent = string | TemplateRef<any> | Type;

export interface OverlayPlugin extends Function {
<T>(component: any, dialogRef: DialogRef<T>, config: OverlayConfig): Maybe<DialogRef<any>>;
}
Expand Down
34 changes: 26 additions & 8 deletions src/components/angular2-modal/overlay/overlay.component.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
declare const clearTimeout: any;

import {
ApplicationRef,
Component,
ComponentRef,
ElementRef,
EmbeddedViewRef,
ResolvedReflectiveProvider,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
Renderer
Renderer,
TemplateRef
} from '@angular/core';

import { PromiseCompleter, supportsKey } from '../framework/utils';
import { DialogRef } from '../models/dialog-ref';
import { BaseDynamicComponent } from '../components/index';


interface ComponentLinkerData {
component: any;
bindings?: ResolvedReflectiveProvider[];
projectableNodes?: any[][];
}
/**
* Represents the modal overlay.
*/
Expand All @@ -26,23 +32,35 @@ import { BaseDynamicComponent } from '../components/index';
'(body:keydown)': 'documentKeypress($event)'
},
encapsulation: ViewEncapsulation.None,
template: `<span #vcRef></span>`
template:
`<template #innerView></template>
<template #template let-ctx>
<template [swapCmp]="ctx.component" [swapCmpBindings]="ctx.bindings" [swapCmpProjectables]="ctx.projectableNodes"></template>
</template>
`
})
export class ModalOverlay extends BaseDynamicComponent {
private beforeDestroyHandlers: Array<() => Promise<void>>;
@ViewChild('vcRef', {read: ViewContainerRef}) private vcRef: ViewContainerRef;

@ViewChild('innerView', {read: ViewContainerRef}) private innerVcr: ViewContainerRef;
@ViewChild('template') private template: TemplateRef<any>;

constructor(private dialogRef: DialogRef<any>,
private appRef: ApplicationRef,
private vcr: ViewContainerRef,
el: ElementRef,
renderer: Renderer) {
super(el, renderer);
this.activateAnimationListener();
}


addComponent<T>(type: any, bindings?: ResolvedReflectiveProvider[]): ComponentRef<T> {
return super._addComponent<T>(type, this.vcRef, bindings);
addEmbeddedComponent(linkData: ComponentLinkerData): EmbeddedViewRef<ComponentLinkerData> {
return this.vcr.createEmbeddedView(this.template, {
$implicit: linkData
});
}

addComponent<T>(type: any, bindings: ResolvedReflectiveProvider[] = [], projectableNodes: any[][] = []): ComponentRef<T> {
return super._addComponent<T>(type, this.innerVcr, bindings, projectableNodes);
}

fullscreen(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
import {
Component,
ElementRef,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
ResolvedReflectiveProvider,
ComponentRef,
Renderer
} from '@angular/core';

Expand All @@ -27,20 +23,14 @@ import { MessageModalPreset } from'./presets/message-modal-preset';
[class.modal-lg]="dialog.context.size == \'lg\'"
[class.modal-sm]="dialog.context.size == \'sm\'">
<div class="modal-content" style="display:block" role="document" overlayDialogBoundary>
<span #dlg></span>
<ng-content></ng-content>
</div>
</div>`
})
export class BSModalContainer extends BaseDynamicComponent {
@ViewChild('dlg', {read: ViewContainerRef}) private vcRef: ViewContainerRef;

constructor(public dialog: DialogRef<MessageModalPreset>,
constructor(public dialog: DialogRef<MessageModalPreset>,
el: ElementRef, renderer: Renderer) {
super(el, renderer);
this.activateAnimationListener();
}

addComponent<T>(type: any, bindings?: ResolvedReflectiveProvider[]): ComponentRef<T> {
return super._addComponent<T>(type, this.vcRef, bindings);
}
}
27 changes: 13 additions & 14 deletions src/components/angular2-modal/plugins/bootstrap/modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ import 'rxjs/add/operator/combineLatest';

import {
Injectable,
ResolvedReflectiveProvider as RRP
ResolvedReflectiveProvider as RRP,
ReflectiveInjector,
Renderer
} from '@angular/core';

import {
Maybe,
ContainerContent,
Overlay,
DialogRef,
Modal as Modal_,
CSSBackdrop,
CSSDialogContainer,
PromiseCompleter
} from '../../../../components/angular2-modal';

Expand All @@ -23,8 +25,8 @@ import { TwoButtonPresetBuilder } from './../bootstrap/presets/two-button-preset

@Injectable()
export class Modal extends Modal_ {
constructor(overlay: Overlay) {
super(overlay);
constructor(overlay: Overlay, renderer: Renderer) {
super(overlay, renderer);
}

alert(): OneButtonPresetBuilder {
Expand All @@ -40,18 +42,15 @@ export class Modal extends Modal_ {
}

protected create(dialogRef: DialogRef<any>,
type: any,
content: ContainerContent,
bindings?: RRP[]): Maybe<DialogRef<any>> {

let refs = this.createModal(dialogRef, CSSBackdrop, BSModalContainer);

refs.containerRef
.instance.addComponent(type, bindings);

const backdropRef = this.createBackdrop(dialogRef, CSSBackdrop);
const containerRef = this.createContainer(dialogRef, BSModalContainer, content, bindings);

let overlay = dialogRef.overlayRef.instance;
let backdrop = refs.backdropRef.instance;
let container = refs.containerRef.instance;
let backdrop = backdropRef.instance;
let container = containerRef.instance;

dialogRef.inElement ? overlay.insideElement() : overlay.fullscreen();

Expand All @@ -69,8 +68,8 @@ export class Modal extends Modal_ {
backdrop.addClass('in');
container.addClass('in');

if (refs.containerRef.location.nativeElement) {
refs.containerRef.location.nativeElement.focus();
if (containerRef.location.nativeElement) {
containerRef.location.nativeElement.focus();
}

overlay.beforeDestroy(() => {
Expand Down
43 changes: 36 additions & 7 deletions src/components/angular2-modal/providers/modal.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {
ComponentRef,
TemplateRef,
ReflectiveInjector,
ResolvedReflectiveProvider
ResolvedReflectiveProvider, Type, Renderer
} from '@angular/core';

import { Overlay } from '../overlay/index';
import { Class, Maybe } from '../framework/utils';
import { OverlayConfig } from '../models/tokens';
import { OverlayConfig, ContainerContent } from '../models/tokens';
import { DialogRef } from '../models/dialog-ref';
import { ModalControllingContextBuilder } from '../models/overlay-context';

Expand All @@ -18,7 +19,7 @@ export class UnsupportedDropInError extends Error {
}

export abstract class Modal {
constructor(public overlay: Overlay) { }
constructor(public overlay: Overlay, private renderer: Renderer) { }


alert(): ModalControllingContextBuilder<any> {
Expand All @@ -35,11 +36,11 @@ export abstract class Modal {

/**
* Opens a modal window inside an existing component.
* @param componentType The angular Component to render as the modal content.
* @param content The content to display, either string, template ref or a component.
* @param config Additional settings.
* @returns {Promise<DialogRef>}
*/
open(componentType: any, config?: OverlayConfig): Promise<DialogRef<any>> {
open(content: ContainerContent, config?: OverlayConfig): Promise<DialogRef<any>> {
config = config || {} as any;
try {
let dialogs = this.overlay.open(config, this.constructor);
Expand All @@ -52,7 +53,7 @@ export abstract class Modal {
// TODO: Currently supporting 1 view container, hence working on dialogs[0].
// upgrade to multiple containers.
return Promise.resolve(
this.create(dialogs[0], componentType, config.bindings)
this.create(dialogs[0], content, config.bindings)
);

} catch (e) {
Expand All @@ -69,16 +70,44 @@ export abstract class Modal {
* @returns {MaybeDialogRef<any>}
*/
protected abstract create(dialogRef: DialogRef<any>,
type: any,
type: ContainerContent,
bindings?: ResolvedReflectiveProvider[]): Maybe<DialogRef<any>>;


protected createBackdrop<T>(dialogRef: DialogRef<any>, BackdropComponent: Class<T>): ComponentRef<T> {
const b = ReflectiveInjector.resolve([{provide: DialogRef, useValue: dialogRef}]);
return dialogRef.overlayRef.instance.addComponent<T>(BackdropComponent, b);
}

protected createContainer<T>(
dialogRef: DialogRef<any>,
ContainerComponent: Class<T>,
content: string | TemplateRef<any> | Type,
bindings?: ResolvedReflectiveProvider[]): ComponentRef<T> {

const b = ReflectiveInjector.resolve([{provide: DialogRef, useValue: dialogRef}])
.concat(bindings || []);

let nodes: any[];
if (typeof content === 'string') {
nodes = [[this.renderer.createText(null, `${content}`)]];
} else if (content instanceof TemplateRef) {
nodes = [this.overlay.defaultViewContainer.createEmbeddedView(content, dialogRef.context).rootNodes];
} else {
nodes = [dialogRef.overlayRef.instance.addEmbeddedComponent({ component: content, bindings: b }).rootNodes];
}
return dialogRef.overlayRef.instance.addComponent<T>(ContainerComponent, b, nodes);
}


/**
* A helper function for derived classes to create backdrop & container
* @param dialogRef
* @param backdrop
* @param container
* @returns { backdropRef: ComponentRef<B>, containerRef: ComponentRef<C> }
*
* @deprecated use createBackdrop and createContainer instead
*/
protected createModal<B, C>(dialogRef: DialogRef<any>, backdrop: Class<B>, container: Class<C>)
: { backdropRef: ComponentRef<B>, containerRef: ComponentRef<C> } {
Expand Down

0 comments on commit 2e6f6a0

Please sign in to comment.