Push Notifications with Angular & Express

Adding the service worker module

ng add @angular/pwa

Generating a VAPID key-pair

yarn global add web-push
web-push generate-vapid-keys --json

Subscribing to push notifications

src/app/app.component.ts

import { Component } from '@angular/core'
import { SwPush } from '@angular/service-worker'
import { PushNotificationService } from './pushNotification.service'

const VAPID_PUBLIC =
  'BNOJyTgwrEwK9lbetRcougxkRgLpPs1DX0YCfA5ZzXu4z9p_Et5EnvMja7MGfCqyFCY4FnFnJVICM4bMUcnrxWg'

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
class AppComponent {
  title = 'angular-push-notifications'

  constructor(swPush: SwPush, pushService: PushNotificationService) {
    if (swPush.isEnabled) {
      swPush
        .requestSubscription({
          serverPublicKey: VAPID_PUBLIC,
        })
        .then(subscription => {
          pushService.sendSubscriptionToTheServer(subscription).subscribe()
        })
        .catch(console.error)
    }
  }
}

src/app/pushNotification.service.ts

import { Injectable } from '@angular/core'
import { HttpClient } from '@angular/common/http'

const SERVER_URL = 'http://localhost:3000/subscription'

@Injectable()
class PushNotificationService {
  constructor(private http: HttpClient) {}

  public sendSubscriptionToTheServer(subscription: PushSubscription) {
    return this.http.post(SERVER_URL, subscription)
  }
}

Setting up a new Express project

Installing dependencies

yarn add body-parser cors express web-push

Creating an Express server

const express = require('express')
const webpush = require('web-push')
const cors = require('cors')
const bodyParser = require('body-parser')

const PUBLIC_VAPID =
  'BNOJyTgwrEwK9lbetRcougxkRgLpPs1DX0YCfA5ZzXu4z9p_Et5EnvMja7MGfCqyFCY4FnFnJVICM4bMUcnrxWg'
const PRIVATE_VAPID = '_kRzHiscHBIGftfA7IehH9EA3RvBl8SBYhXBAMz6GrI'

const fakeDatabase = []

const app = express()

app.use(cors())
app.use(bodyParser.json())

webpush.setVapidDetails('mailto:you@domain.com', PUBLIC_VAPID, PRIVATE_VAPID)

app.post('/subscription', (req, res) => {
  const subscription = req.body
  fakeDatabase.push(subscription)
})

app.post('/sendNotification', (req, res) => {
  const notificationPayload = {
    notification: {
      title: 'New Notification',
      body: 'This is the body of the notification',
      icon: 'assets/icons/icon-512x512.png',
    },
  }

  const promises = []
  fakeDatabase.forEach(subscription => {
    promises.push(
      webpush.sendNotification(
        subscription,
        JSON.stringify(notificationPayload)
      )
    )
  })
  Promise.all(promises).then(() => res.sendStatus(200))
})

app.listen(3000, () => {
  console.log('Server started on port 3000')
})

References
https://malcoded.com/posts/angular-push-notifications/
https://blog.angular-university.io/angular-push-notifications/
https://medium.com/@a.adendrata/push-notifications-with-angular-6-firebase-cloud-massaging-dbfb5fbc0eeb
https://developers.google.com/web/fundamentals/push-notifications/display-a-notification

Detect scroll to bottom of html element in Angular

@HostListener('window:scroll', ['$event'])
onWindowScroll() {
  // In chrome and some browser scroll is given to body tag
  const pos = (document.documentElement.scrollTop || document.body.scrollTop) + document.documentElement.offsetHeight;
  const max = document.documentElement.scrollHeight;
  const fixedPos = pos + 10;
  console.log(max, fixedPos);
  // pos/max will give you the distance between scroll bottom and and bottom of screen in percentage.
  if (fixedPos >= max) {
    if (!this.isFetching) {
      this.nextHistory().subscribe();
    }
  }
}

References
https://stackoverflow.com/questions/40664766/how-to-detect-scroll-to-bottom-of-html-element

Add class to body and intercept DOM using Renderer2 in Angular

import { Component, OnDestroy, Renderer2 } from '@angular/core';

export class myModalComponent implements OnDestroy {

  constructor(private renderer: Renderer2) {
    this.renderer.addClass(document.body, 'modal-open');
   }

  ngOnDestroy() {
    this.renderer.removeClass(document.body, 'modal-open');
  }

References
https://stackoverflow.com/questions/43542373/angular2-add-class-to-body-tag
https://angular.io/api/core/Renderer2

Bind and Unbind event listener for elements in Angular

constructor(private elementRef: ElementRef) {
  }

openNav() {
  this.sideNavWidth = '275px';
  this.bindBodyClick();
}

bindBodyClick() {
  setTimeout(() => {

    this.handleBodyClickListener = this.handleBodyClick.bind(this);

    this.elementRef.nativeElement.querySelector('.appMain')
      .addEventListener('click', this.handleBodyClickListener, false);

    this.elementRef.nativeElement.querySelector('.appNavbar')
      .addEventListener('click', this.handleBodyClickListener, false);
  }, 300);
}

unbindBodyClick() {
  this.elementRef.nativeElement.querySelector('.appMain')
    .removeEventListener('click', this.handleBodyClickListener, false);

  this.elementRef.nativeElement.querySelector('.appNavbar')
    .removeEventListener('click', this.handleBodyClickListener, false);
}

closeNav() {
  this.sideNavWidth = '0px';
  this.unbindBodyClick();
}

handleBodyClick(event) {
  this.closeNav();
}

References
https://stackoverflow.com/questions/41609937/how-to-bind-event-listener-for-rendered-elements-in-angular-2
https://stackoverflow.com/questions/11565471/removing-event-listener-which-was-added-with-bind

Detect window size changes using RxJS debounce in Angular

sizeChanged: Subject<boolean>;
sizeChangedDebounced;

ngOnInit() {
  // show chart on init and size changes
  this.showChart();
  this.sizeChanged = new Subject<boolean>();
  this.sizeChangedDebounced = this.sizeChanged.pipe(debounce(() => interval(1000)));
  this.sizeChangedDebounced.subscribe(() => {
    this.showChart();
  });
}

showChart() {
  // do something
}

@HostListener('window:resize', ['$event'])
onResize(event?) {
  this.sizeChanged.next(true);
}

debounce example

import { fromEvent, interval } from 'rxjs';
import { debounce } from 'rxjs/operators';

const clicks = fromEvent(document, 'click');
const result = clicks.pipe(debounce(() => interval(1000)));
result.subscribe(x => console.log(x));

References
https://rxjs-dev.firebaseapp.com/api/operators/debounce

Providing a singleton service on Angular

There are two ways to make a service a singleton in Angular:

  • Declare root for the value of the @Injectable() providedIn property
  • Include the service in the AppModule or in a module that is only imported by the AppModule

Using providedIn

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class UserService {
}

NgModule providers array

@NgModule({
  ...
  providers: [UserService],
  ...
})

References
https://angular.io/guide/singleton-services