Using ControlValueAccessor to Create Custom Form Controls in Angular

import { Component, forwardRef, HostBinding, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

@Component({
  selector: 'rating-input',
  template: `
    <span
      *ngFor="let starred of stars; let i = index"
      (click)="onTouched(); rate(i + (starred ? (value > i + 1 ? 1 : 0) : 1))">
      <ng-container *ngIf="starred; else noStar">⭐</ng-container>
      <ng-template #noStar>·</ng-template>
    </span>
  `,
  styles: [`
    span {
      display: inline-block;
      width: 25px;
      line-height: 25px;
      text-align: center;
      cursor: pointer;
    }
  `],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => RatingInputComponent),
      multi: true
    }
  ]
})
export class RatingInputComponent implements ControlValueAccessor {

  stars: boolean[] = Array(5).fill(false);

  // Allow the input to be disabled, and when it is make it somewhat transparent.
  @Input() disabled = false;
  @HostBinding('style.opacity')
  get opacity() {
    return this.disabled ? 0.25 : 1;
  }

  // Function to call when the rating changes.
  onChange = (rating: number) => {};

  // Function to call when the input is touched (when a star is clicked).
  onTouched = () => {};

  get value(): number {
    return this.stars.reduce((total, starred) => {
      return total + (starred ? 1 : 0);
    }, 0);
  }

  rate(rating: number) {
    if (!this.disabled) {
      this.writeValue(rating);
    }
  }

  // Allows Angular to update the model (rating).
  // Update the model and changes needed for the view here.
  writeValue(rating: number): void {
    this.stars = this.stars.map((_, i) => rating > i);
    this.onChange(this.value)
  }

  // Allows Angular to register a function to call when the model (rating) changes.
  // Save the function as a property to call later here.
  registerOnChange(fn: (rating: number) => void): void {
    this.onChange = fn;
  }

  // Allows Angular to register a function to call when the input has been touched.
  // Save the function as a property to call later here.
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }

  // Allows Angular to disable the input.
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

}

References
https://alligator.io/angular/custom-form-control/
https://blog.thoughtram.io/angular/2016/07/27/custom-form-controls-in-angular-2.html

Loading data before components using Router resolver in Angular

As the name suggests, we can add a resolve function to the route which loads the component that has an API call to do. This will cause the component to be only loaded and displayed by Angular once the API call (or whatever else we define) is loaded.

Create a separate class which we’ll have the resolver functionality

// resolver.ts
import { Injectable } from '@angular/core';
import { Resolve } from '@angular/router';
import { Observable } from 'rxjs';
import { ApiService } from './api.service';

@Injectable()
export class Resolver implements Resolve<Observable<string>> {
  constructor(private api: ApiService) { }

  resolve() {
    return this.api.getProducts();
  }
}

Add this class as a provider

// app.module.ts
import { Resolver } from './resolver';
// ...
providers: [Resolver],

app-routing.module.ts

// app-routing.module.ts
import { Resolver } from './resolver';

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'products', component: ProductsComponent, resolve: { products: Resolver } }
];
// products.component.ts
import { ActivatedRoute } from '@angular/router';
// ...
constructor(private route: ActivatedRoute) { }
products;
ngOnInit() {
  this.products = this.route.snapshot.data.products;
}

Access to Route Params

import { Injectable } from '@angular/core';
import { HnService } from './hn.service';

import { Resolve } from '@angular/router';

import { ActivatedRouteSnapshot } from '@angular/router';

@Injectable()
export class HnResolver implements Resolve<any> {
  constructor(private hnService: HnService) {}

  resolve(route: ActivatedRouteSnapshot) {
    return this.hnService.getPost(route.paramMap.get('id'));
  }
}

References
https://blog.fullstacktraining.com/loading-data-before-components-in-angular/
https://alligator.io/angular/route-resolvers/

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:[email protected]', 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