Last active
December 18, 2016 20:10
-
-
Save MikeRyanDev/cf19e9314cff9143286f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* A proof-of-concept for a new modal lib for Angular 2 | |
* | |
* Goals: | |
* - Easy to style | |
* - Easy to control with @ngrx/store | |
* - Completely stateless | |
* | |
* How to use: | |
* In your root component, add `ModalConnector` to your providers array | |
* and `ModalOutlet` to your directives array. In your template add the | |
* `<modal-outlet></modal-outlet>` component. | |
* | |
* Anywhere else in your app, add `Modal` to your directives array and use | |
* the `<modal [open]="true" (dismiss)="handle()"></modal>` component. | |
* | |
* `[open]` determines whether or not the modal should be shown. `(dismiss)` | |
* is emitted when the user clicks on the backdrop | |
*/ | |
import 'rxjs/add/operator/filter'; | |
import { | |
Renderer, | |
ElementRef, | |
AfterViewInit, | |
OnDestroy, | |
Input, | |
Output, | |
EventEmitter, | |
Component, | |
HostBinding, | |
ViewQuery, | |
QueryList, | |
ChangeDetectionStrategy, | |
ChangeDetectorRef | |
} from 'angular2/core'; | |
import { BehaviorSubject } from 'rxjs/subject/BehaviorSubject'; | |
import { ReplaySubject } from 'rxjs/subject/ReplaySubject'; | |
import { Subject } from 'rxjs/Subject'; | |
import { Subscription } from 'rxjs/Subscription'; | |
import { Observable } from 'rxjs/Observable'; | |
export class ModalConnector { | |
outlet: ModalOutlet; | |
backlog: Modal[] = []; | |
attachOutlet(outlet: ModalOutlet) { | |
this.outlet = outlet; | |
this.flushBacklog(); | |
} | |
detachOutlet() { | |
this.outlet = undefined; | |
} | |
private flushBacklog() { | |
const backlog = this.backlog; | |
this.backlog = []; | |
backlog.forEach(modal => this.addModal(modal)); | |
} | |
addModal(modal: Modal) { | |
if( !!this.outlet ) { | |
this.outlet.accept(modal); | |
} | |
else { | |
this.backlog = [...this.backlog, modal]; | |
} | |
} | |
removeModal(modal: Modal) { | |
if( !!this.outlet ) { | |
this.outlet.remove(modal); | |
} | |
} | |
} | |
@Component({ | |
selector: 'modal-outlet', | |
template: ` | |
<div class="modal-wrapper" [class.show]="show" [ngClass]="wrapperClasses"> | |
<div class="modal-backdrop" (click)="dismiss($event)" #backdrop> </div> | |
</div> | |
`, | |
styles: [` | |
.modal-wrapper { | |
display: block; | |
position: fixed; | |
top: 0; | |
left: 0; | |
bottom: 0; | |
right: 0; | |
width: 100%; | |
height: 100%; | |
visibility: hidden; | |
z-index: 1000; | |
transition: visibility 300ms; | |
} | |
.modal-wrapper.show { | |
visibility: visible; | |
} | |
.modal-backdrop { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
display: block; | |
background-color: black; | |
opacity: 0; | |
transition: opacity 300ms; | |
} | |
.modal-wrapper.show .modal-backdrop { | |
position: absolute; | |
top: 0; | |
left: 0; | |
opacity: 0.7; | |
} | |
`] | |
}) | |
export class ModalOutlet implements AfterViewInit, OnDestroy { | |
constructor( | |
private connector: ModalConnector, | |
private renderer: Renderer, | |
private ref: ElementRef, | |
@ViewQuery('backdrop') private backdrops: QueryList<ElementRef> | |
) { } | |
modals: Modal[] = []; | |
get show() { | |
return !!this.modals.find(modal => modal.open); | |
} | |
get wrapperClasses() { | |
return this.modals | |
.filter(modal => modal.open && !!modal.wrapperClass) | |
.map(modal => modal.wrapperClass); | |
} | |
ngAfterViewInit() { | |
this.connector.attachOutlet(this); | |
} | |
ngOnDestroy() { | |
this.connector.detachOutlet(); | |
} | |
get targetNode(): HTMLElement { | |
return !!this.backdrops.first ? this.backdrops.first.nativeElement : undefined; | |
} | |
accept(modal: Modal) { | |
this.modals = [...this.modals, modal]; | |
this.renderer.attachViewAfter(this.targetNode, [ modal.node ]); | |
} | |
remove(modal: Modal) { | |
this.modals = this.modals.filter(m => m !== modal); | |
this.renderer.detachView([ modal.node ]); | |
} | |
dismiss($event) { | |
this.modals | |
.filter(modal => modal.open) | |
.forEach(modal => modal.dismiss.emit($event)); | |
} | |
} | |
@Component({ | |
selector: 'modal', | |
template: ` | |
<div class="modal" #modal [class.show]="open" [class.useDefault]="useDefaultStyles"> | |
<ng-content></ng-content> | |
</div> | |
`, | |
styles: [` | |
.modal.useDefault { | |
position: absolute; | |
width: 400px; | |
height: 320px; | |
top: 50%; | |
margin-top: -160px; | |
left: 50%; | |
margin-left: -200px; | |
background-color: white; | |
transform: scale(0.3); | |
opacity: 0; | |
transition: transform 300ms, opacity 300ms; | |
} | |
.modal.useDefault.show { | |
transform: scale(1); | |
opacity: 1.0; | |
} | |
`] | |
}) | |
export class Modal implements AfterViewInit, OnDestroy { | |
@Output() dismiss = new EventEmitter(); | |
@Input() wrapperClass: string; | |
@HostBinding('class.useDefault') @Input() useDefaultStyles = true; | |
@HostBinding('class.show') @Input() open = false; | |
constructor( | |
private _ref: ElementRef, | |
private _connector: ModalConnector, | |
@ViewQuery('modal') private modalList: QueryList<ElementRef> | |
) { } | |
get node(): HTMLElement { | |
return this.modalList.first.nativeElement; | |
} | |
ngAfterViewInit() { | |
this._connector.addModal(this); | |
} | |
ngOnDestroy() { | |
this._connector.removeModal(this); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Ryan,
I found your component as very useful. I am not able to write a component to use them. Please provide example.