I made a small Popup component to show different error/success...etc. messages.
Template:
<div [@visibilityChanged]="isVisible" *ngIf="shouldShow" [ngClass]="messageType">
<ng-content></ng-content>
</div>
Component:
import {
Component, OnChanges, Input,
Renderer2, AfterViewInit, ElementRef, AfterViewChecked,
HostListener
} from '@angular/core';
import { trigger, state, animate, transition, style } from '@angular/animations';
/**
* This class represents the popup component.
*/
@Component({
moduleId: module.id,
selector: 'sd-popup',
templateUrl: 'popup.component.html',
styleUrls: ['popup.component.css'],
animations: [trigger('visibilityChanged', [
transition(':enter', [
style({ opacity: 0 }),
animate(100, style({ opacity: 1 }))
]),
transition(':leave', [
style({ opacity: 1 }),
animate(100, style({ opacity: 0 }))
])
])
]
})
export class PopUpComponent implements AfterViewInit, AfterViewChecked {
@Input() messageType: string | string[];
@Input() errorState: boolean;
@Input() parentEl: Element;
public shouldShow: boolean;
constructor(private popupEl: ElementRef,
private renderer: Renderer2) {
}
ngAfterViewInit(): void {
// Append to body for better calculation
document.body.appendChild(this.popupEl.nativeElement);
//use random delay for load balancing
this.renderer.listen(this.parentEl, 'keydown', () => {
setTimeout(() => {
this.shouldShow = this.errorState;
}, this.getRandomInt(1, 50));
});
}
@HostListener('window:resize')
@HostListener('window:scroll')
ngAfterViewChecked(): void {
// calculate position when view checked,window is resized or scrolling
// consider to use load balancing here as well (random delay might be visible)
this.calculatePosition(this.popupEl.nativeElement, this.parentEl);
}
public calculatePosition(element: any, target: any): void {
const targetOffset = target.getBoundingClientRect();
this.renderer.setStyle(element, 'top', `${(targetOffset.top + document.documentElement.scrollTop) - element.offsetHeight - 5}px`);
this.renderer.setStyle(element, 'left', `${targetOffset.left + (target.offsetWidth / 2 - element.offsetWidth / 2)}px`);
this.renderer.setStyle(element, 'zindex', `${target.zindex + 100}px`);
}
getRandomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}
SCSS:
:host {
position:absolute;
opacity: 0.9;
}
.errorMessage {
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
color: #fff;
background-image: none;
background-clip: padding-box;
border-radius: 0.25rem;
}
.warning {
@extend .errorMessage;
background-color: #CD464D;
border: 1px solid #CD464D;
}
.success {
@extend .errorMessage;
background-color: rgb(15, 230, 147);
border: 1px solid rgb(15, 230, 147);
}
div:after, div:before {
top: 100%;
left: 50%;
border: solid transparent;
content: " ";
height: 0;
width: 0;
position: absolute;
pointer-events: none;
}
div:after {
border-color: rgba(136, 183, 213, 0);
border-top-color: inherit;
border-width: 5px;
margin-left: -5px;
}
div:before {
border-color: rgba(194, 225, 245, 0);
border-top-color: inherit;
border-width: 6px;
margin-left: -6px;
}
And finally usage:
<input #passInput type="password" name="password" id="password" class="form-control" formControlName="password"/>
<sd-popup [parentEl]="passInput" [errorState]="password.errors?.required && (password.dirty || password.touched)"
messageType="success">Password is required</sd-popup>
Tell me what do you think about it!