I created one for this specific task and thought about embedding bootstrap modal as template but came across a couple of issues such as the fact that the only useful piece out of bootstrap modal would be the class names. Not surprisingly, didn't want that for this specific task since I needed to distinguish between window modals and progress modals on components.
Overlay
@Component({
selector: 'overlay',
template:
`<div [ngClass]="isOpen ? 'opened' : 'closed'">
<div class="modal" role="dialog">
<div class="modalBody">
<div *ngIf="isSaving">
<span class="text-success text-bold">
Saving...
</span>
</div>
<div *ngIf="isSaved">
<span class="text-success text-bold">
Saved.
</span>
</div>
</div>
</div>
</div>`,
styles: [
'.modal { position:absolute; width: 100%; height: 100%; margin: -30px; background-color: rgba(255, 255, 255, 0.7); z-index: 1000; text-align: center; }',
'.closed { visibility: hidden; }',
'.opened { visibility: visible; }',
'.modalBody { top: 45%; left: 25%; width: 50%; position: absolute; }',
'.text-bold { font-weight: 800; font-size: 1.5em; }'
]
})
export class Overlay implements OnChanges, AfterContentInit {
@Input() isSaving: boolean = false;
@Input() isSaved: boolean = false;
@Input() containerElement: HTMLElement;
isOpen = false;
private modalElement;
constructor(private element: ElementRef, private animationBuilder: AnimationBuilder) { }
ngOnChanges() {
if (this.modalElement) {
if (this.isSaving == true || this.isSaved == true) {
this.toggleAnimation(true);
}
else if (this.isSaving == false && this.isSaved == false) {
this.toggleAnimation(false);
}
}
}
ngAfterContentInit() {
this.containerElement.style.position = 'relative';
this.modalElement = this.element.nativeElement.querySelector('.modal');
}
private toggleAnimation(isOpen) {
var startCss = { backgroundColor: 'rgba(255, 255, 255, 0)' };
var endCss = { backgroundColor: 'rgba(255, 255, 255, 0.7)' };
if (isOpen) {
this.isOpen = true
this.animation(
true,
this.modalElement,
400,
startCss,
endCss,
null
);
}
else {
this.animation(
isOpen,
this.modalElement,
400,
startCss,
endCss,
() => {
this.isOpen = false;
}
);
}
}
private animation(isStart, element, duration, startCss, endCss, finishedCallback) {
var animation = this.animationBuilder.css();
animation.setDuration(duration);
if (isStart) {
animation.setFromStyles(startCss).setToStyles(endCss);
} else {
animation.setFromStyles(endCss).setToStyles(startCss)
}
animation.start(element);
if (finishedCallback) {
setTimeout(finishedCallback, duration);
}
}
}
Usage
As is, you'll need a relative container in order for overlay
to fill its parent. The css definitely needs modifications to accommodate for a few scenarios such as mobile devices and non-positioned containers. Here's how it's currently used:
HTML
<form action="/edit" method="post" #myForm="ngForm" (ngSubmit)="save ajax method that will update the isSaving and isSaved accordingly" novalidate>
<div style="position: relative;" #overlayContainer>
<overlay [isSaving]="isSaving" [isSaved]="isSaved" [containerElement]="overlayContainer"></overlay>
</div>
</form>
After saving the form
, an overlay
is shown within the containerElement
for 400ms and is faded out and goes hidden afterwards until the next save attempt. The isSaving
and isSaved
binding values are the responsibility of the any component that wishes to use the overlay
.