Yes! and it doesn't require a third part library either. One can subscribe to that service can be imported to any component send an alert notification. I also inject a loaded class and fade in, which is included now.
It's when I first started I wished someone would have shown me, so I extend this courtesy to you.
This is my default loader for all applications.
app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {HttpClientModule} from '@angular/common/http'
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
app.component.ts
import { Component } from '@angular/core';
import { Router, NavigationStart, NavigationEnd } from '@angular/router';
import { Subscription, merge, of, fromEvent, map } from 'rxjs';
import { NotifyViewService } from './common/services/notify-view.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'YOURAPPNAMEHERE';
constructor(private router: Router, private notify: NotifyViewService) {
}
internetstate$: Subscription = Subscription.EMPTY;
ngOnInit() {
//const html ref
const htmlEl = document.getElementById('root') as HTMLElement;
// element ref loader
const loadingEl = document.getElementById('loader') as HTMLElement;
// element ref content
const contentEl = document.getElementById('content') as HTMLElement;
// this.offlineEventCheck();
// this.offlineNavigatorCheck();
// subscribe to router to get events
this.router.events.subscribe((events)=>{
// listen for navigation started then remove the none class from loader element
if(events instanceof NavigationStart){
loadingEl?.classList.remove('loaded');
}
// listen for navigation end
if(events instanceof NavigationEnd){
// add 1.5 second delay to see the loader and allow time for content to load
setTimeout(()=>{
// add the none class to hide the loader element
loadingEl?.classList.add('none');
// remove the none class to show the content element router-outlet
contentEl.classList.add("fadeIn");
contentEl?.classList.remove('none');
}, 1500)
}
})
// check to see if the internet is connected
this.internetState();
}
ngOnChanges(){
// if there are changes - just run the internet state
// currently will not fire without changes/simple changes
this.internetState()
}
ngOnDestroy() {
// unsubcribe from ther internet state
this.internetstate$.unsubscribe()
}
internetState() {
// this.internetstate = navigator.onLine;
// listen for fromEvent window to get state
// pipe and map the navigator online boolean
// subcribe to the boolean and toggle the slass
// then set a notification - if its online it will be ignored programatically
// must exact match true - could be toggled on boolean I didnt set false or true
this.internetstate$ = merge( of (null),
fromEvent(window, 'online'),
fromEvent(window, 'offline')
)
.pipe(map(() => navigator.onLine))
.subscribe(state => {
// console lof the state remove or rem out before production
console.log('state', state);
// if internet is true its online
if(state===true){
const message = {
name: "online",
message:""
}
// send notify alert with online name
this.notify.setNotify(message)
}
// if internet is false its offline
if(state===false){
// console.log false remove or rem out before production
console.log('false')
// set a 200 millisecond timeout to allow the notification to receive the message constant
setTimeout(()=>{
const message = {
name: "offline",
message: "this application requires internet connectivity"
}
// send the message constant
this.notify.setNotify(message)
},200)
}
});
}
app.component.css
.none{
display: none !important;
height: 0 !important;
}
app.component.html
<div class="loaded" id="loader">
<div id="ctn-preloader" class="ctn-preloader">
<div class="animation-preloader">
<div class="spinner"></div>
<div class="txt-loading">
<span data-text-preloader="L" class="letters-loading">
L
</span>
<span data-text-preloader="O" class="letters-loading">
O
</span>
<span data-text-preloader="A" class="letters-loading">
A
</span>
<span data-text-preloader="D" class="letters-loading">
D
</span>
<span data-text-preloader="I" class="letters-loading">
I
</span>
<span data-text-preloader="N" class="letters-loading">
N
</span>
<span data-text-preloader="G" class="letters-loading">
G
</span>
</div>
</div>
<div class="loader-section section-left"></div>
<div class="loader-section section-right"></div>
</div>
</div>
<!-- content outlet -->
<div class="none" id="content">
<router-outlet></router-outlet>
</div>
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>YOURAPPNAMEHERE</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
</head>
<body id="body">
<!-- style is the fix for angular smooth scroll events -->
<style>
body{
margin:0;
padding:0;
scroll-behavior: smooth !important;
}
</style>
<app-root></app-root>
</body>
</html>
**services/notify.service.ts **
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
interface NotifyI {
name: string;
message: string;
stack?: string;
}
@Injectable({
providedIn: 'root'
})
export class NotifyViewService {
private viewportnotifySubject = new BehaviorSubject({} as NotifyI);
public viewportnotifyObserver = this.viewportnotifySubject.asObservable();
private shownotifySubject = new BehaviorSubject(false);
public shownotifyObserver = this.shownotifySubject.asObservable();
constructor() { }
setNotify(message:NotifyI){
this.viewportnotifySubject.next(message);
this.shownotifySubject.next(true);
}
clearNotify(){
const clear:NotifyI={
name: null as unknown as string,
message: null as unknown as string,
stack: null as unknown as string
}
this.viewportnotifySubject.next(clear as any as NotifyI);
this.shownotifySubject.next(false)
}
}
style.css
:root {
--primary-color: #222222;
--secondary-color: #d72323;
--yellow-color: #ffc107;
--text-gray-color: #4a4c59;
--white-color: #ffffff;
--ofwhite-color: #e6e6e6;
--black-color: #000000;
--light-color: #a5a5a5;
--light-color2: #aaaaaa;
--dark-color: #242424;
--sky-color: #647589;
--border-color: #e4e4e4;
--border-color2: #e4e4e4;
--border-color3: #dddddd;
--border-dark: #383434;
--bg-gray-color: #f7f8fc;
--gray-color: #ebebeb;
--gray-color2: #f5f5f5;
--rubik: "Rubik", sans-serif;
--work-sans: "Work Sans", sans-serif;
--roboto:"Roboto", sans-serif;
--body-font-size: 1.7rem;
--body-font-weight: 400;
--body-line-height: 2.6rem;
--headings-weight: 700;
--transition: all 0.3s ease 0s;
--container-fluid-offset: 16rem;
}
/*
28. Preloader css
*/
.ctn-preloader {
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
height: 100%;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
position: fixed;
left: 0;
top: 0;
width: 100%;
z-index: 9999;
pointer-events: none;
}
.ctn-preloader .animation-preloader {
position: absolute;
z-index: 100;
}
.ctn-preloader .animation-preloader .spinner {
-webkit-animation: spinner 1s infinite linear;
animation: spinner 1s infinite linear;
border-radius: 50%;
border: 4px solid var(--light-color2);
border-top-color: var(--black-color);
height: 9em;
margin: 0 auto 3.5em auto;
width: 9em;
}
.ctn-preloader .animation-preloader .txt-loading {
font-weight: 900;
text-align: center;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font-family:var(--roboto);
color: var(--md-ref-palette-primary90);
font-size: 3.5rem;
}
.ctn-preloader .animation-preloader .txt-loading .letters-loading:before {
animation: letters-loading 5s infinite;
color: var(--md-ref-palette-primary90);
content: attr(data-text-preloader);
left: 0;
opacity: 0;
position: absolute;
top: 0;
-webkit-transform: rotateY(-90deg);
transform: rotateY(-90deg);
-webkit-animation: letters-loading 5s infinite;
}
.ctn-preloader .animation-preloader .txt-loading .letters-loading {
color: rgba(233, 233, 233, 0.2);
position: relative;
}
.ctn-preloader
.animation-preloader
.txt-loading
.letters-loading:nth-child(2):before {
-webkit-animation-delay: 0.2s;
animation-delay: 0.2s;
}
.ctn-preloader
.animation-preloader
.txt-loading
.letters-loading:nth-child(3):before {
-webkit-animation-delay: 0.3s;
animation-delay: 0.3s;
}
.ctn-preloader
.animation-preloader
.txt-loading
.letters-loading:nth-child(4):before {
-webkit-animation-delay: 0.4s;
animation-delay: 0.4s;
}
.ctn-preloader
.animation-preloader
.txt-loading
.letters-loading:nth-child(5):before {
-webkit-animation-delay: 0.5s;
animation-delay: 0.5s;
}
.ctn-preloader
.animation-preloader
.txt-loading
.letters-loading:nth-child(6):before {
-webkit-animation-delay: 0.6s;
animation-delay: 0.6s;
}
.ctn-preloader
.animation-preloader
.txt-loading
.letters-loading:nth-child(7):before {
-webkit-animation-delay: 0.7s;
animation-delay: 0.7s;
}
.ctn-preloader .loader-section {
background: var(--md-ref-palette-primary30);
height: 100%;
position: fixed;
top: 0;
width: calc(50% + 1px);
}
.ctn-preloader .loader-section.section-left {
left: 0;
}
.ctn-preloader .loader-section.section-right {
right: 0;
}
.loaded .animation-preloader {
opacity: 0;
transition: 0.5s ease-out;
-webkit-transition: 0.5s ease-out;
-moz-transition: 0.5s ease-out;
-ms-transition: 0.5s ease-out;
-o-transition: 0.5s ease-out;
}
.loaded .loader-section.section-left {
-webkit-transition: 0.7s 0.3s all cubic-bezier(0.1, 0.1, 0.1, 1);
transition: 0.7s 0.3s all cubic-bezier(0.1, 0.1, 0.1, 1);
-webkit-transform: translateX(-101%);
transform: translateX(-101%);
}
.loaded .loader-section.section-right {
-webkit-transition: 0.7s 0.3s all cubic-bezier(0.1, 0.1, 0.1, 1);
transition: 0.7s 0.3s all cubic-bezier(0.1, 0.1, 0.1, 1);
-webkit-transform: translateX(101%);
transform: translateX(101%);
}
/* Animación del preloader */
@-webkit-keyframes spinner {
to {
-webkit-transform: rotateZ(360deg);
transform: rotateZ(360deg);
}
}
@keyframes spinner {
to {
-webkit-transform: rotateZ(360deg);
transform: rotateZ(360deg);
}
}
/* Animación de las letras cargando del preloader */
@-webkit-keyframes letters-loading {
0%,
75%,
100% {
opacity: 0;
-webkit-transform: rotateY(-90deg);
transform: rotateY(-90deg);
}
25%,
50% {
opacity: 1;
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
}
}
@keyframes letters-loading {
0%,
75%,
100% {
opacity: 0;
-webkit-transform: rotateY(-90deg);
transform: rotateY(-90deg);
}
25%,
50% {
opacity: 1;
-webkit-transform: rotateY(0deg);
transform: rotateY(0deg);
}
}
/*# sourceMappingURL=style.css.map */
/* fadeIn */
@-webkit-keyframes fadeIn {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-moz-keyframes fadeIn {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@-o-keyframes fadeIn {
0% {
opacity: 0; }
100% {
opacity: 1; } }
@keyframes fadeIn {
0% {
opacity: 0; }
100% {
opacity: 1; } }
.fadeIn {
-webkit-animation: fadeIn .25s ease-in;
-moz-animation: fadeIn .25s ease-in;
-o-animation: fadeIn .25s ease-in;
animation: fadeIn .25s ease-in; }
html, body { height: 100%; }
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; }
i {
color: #222222
}
terminal or command line:
npm install bootstrap then import the min and icons as a font.
npm install bootstrap-icons
edit angular.json to use bootstrap.
"styles": [
"node_modules/bootstrap/dist/css/bootstrap.min.css",
"node_modules/bootstrap-icons/font/bootstrap-icons.css",
"src/styles.css"
],
"scripts": [
"node_modules/bootstrap/dist/js/bootstrap.bundle.min.js"
]