DEV Community

loading...
Cover image for Adding Angular Components to your static site
CodingCatDev

Adding Angular Components to your static site

ajonp profile image Alex Patterson Originally published at ajonp.com on ・4 min read

Adding dynamic features to a static site.

This is a multi part series covering all the different types of Web Components I am using on the https://ajonp.com site currently. I just wanted to show how you can use each of them at a somewhat high level.

I was inspired to share more by Max's post:

Why Angular Components

I needed to add payments to https://AJonP.com so we can start supporting longer course tutorials. So I sent out a poll on Twitter to see what we should build the Webcomponents using.

Twitter Poll

Demo

I plan to share a more in depth course on how to build all of this! For now I thought it would be cool just to see it all in action. Notice how after the site loads Firebase kicks in and checks to see if you are a pro member then dynamically hides items using a webcomponent that understands user state. The great part here is that I have many of the Angular items that access firebase already created and I don’t have to reinvent the wheel!

Thank You

Who do I have to thank for teaching all of this to me? Jeff Delaney at https://fireship.io/courses/stripe-payments/

Allowing User

It is as easy as using <ajonp-allow-if> to wrap around any element and then use display none within that component.

No more ads

An example of this is when a user registers and becomes a Pro member of AJonP, they will no longer see ads.

For this I can just wrap my Hugo Go Partial:

<ajonp-allow-if level="not-user">
  <ion-row>
    <ion-col text-center>
      <div class="ajonp-hide-lg-down">
        <!-- /21838128745/ajonp_new -->
        <div id="div-gpt-ad-xxxxxxxxxxxxxx-0" style="width: 970px; height: 90px; margin: auto;">
          <script>
            googletag.cmd.push(function () {
              googletag.display('div-gpt-ad-xxxxxxxxxxxxxx-0');
            });
          </script>
        </div>
      </div>
    </ion-col>
  </ion-row>
</ajonp-allow-if>
Enter fullscreen mode Exit fullscreen mode

Angular Parts

Template

The template is pretty straight forward, Angular either shows the component or removes it based on the *ngIf.

<div *ngIf="allowed"><slot></slot></div>

<div *ngIf="!allowed"><slot name="falsey"></slot></div>
Enter fullscreen mode Exit fullscreen mode

Angular Component

Some things to note are the @Input decorations. This allows for you to pass in all of these different items as attributes on the ajonp-allow-if component. In our example above I pass in level="not-user" to the @Input level decorator.

What is wonderful about using Angular is that you get all the nice dependency injection that you would normally get with a standard Angular component!

import { Component, ViewEncapsulation, ChangeDetectorRef, Input, AfterViewInit, ElementRef } from '@angular/core';
import { AuthService } from '../../core/services/auth.service';

@Component({
  templateUrl: './allow-if.component.html',
  encapsulation: ViewEncapsulation.ShadowDom
})
export class AllowIfComponent implements AfterViewInit {

  @Input() selector;
  @Input() level: 'pro' | 'user' | 'not-pro' | 'not-user' | 'not-user-not-pro';
  @Input() reverse = false;
  @Input() product;

  constructor(
    private cd: ChangeDetectorRef,
    public auth: AuthService,
    private el: ElementRef,
  ) { }

  ngAfterViewInit() {
    this.el.nativeElement.style.visibility = 'visible';
  }

  get allowed() {
    const u = this.auth.userDoc;
    const products = u && u.products && Object.keys(u.products);

    // Handle Product
    if (products && products.includes(this.product)) {
      return true;
    }

    // Handle Level
    switch (this.level) {
      case 'user':
        return u;

      case 'pro':
        return u && u.is_pro;

      case 'not-pro':
        return u && !u.is_pro;

      case 'not-user':
        return !u;

      case 'not-user-not-pro':
        return !u || !u.is_pro;

      default:
        return false;
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

AuthService

Here you can see I am utilizing the full firebase library for authentication, which is sweet!

import { Injectable, ApplicationRef } from '@angular/core';
import * as firebase from 'firebase/app';
import { user } from 'rxfire/auth';
import { docData } from 'rxfire/firestore';

import { Observable, of } from 'rxjs';
import { switchMap, take, tap, isEmpty } from 'rxjs/operators';

import { AjonpUser } from '../models/ajonp-user';
import { AngularfirebaseService } from './angularfirebase.service';

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

  authClient = firebase.auth();

  user$: Observable<any>;
  userDoc$: Observable<any>;

  user;
  userDoc;

  constructor(private app: ApplicationRef, private db: AngularfirebaseService) {
    // Why service subsciptions? Maintain state between route changes with change detection.
    this.user$ = user(this.authClient)
      .pipe(tap(u => {
        this.user = u;
        this.app.tick();
      }));

    this.userDoc$ = this.getUserDoc$('users').pipe(tap(u => {
      this.userDoc = u;
      this.app.tick();
    }));

    this.userDoc$.pipe(take(1)).subscribe((u: AjonpUser) => {
      if (u && Object.keys(u).length) {
        const ajonpUser: AjonpUser = { uid: u.uid };
        this.updateUserData(ajonpUser).catch(error => {
          console.log(error);
        });
      } else {
        if (this.user && Object.keys(this.user).length) {
          const data: AjonpUser = {
            uid: this.user.uid,
            email: this.user.email,
            emailVerified: this.user.emailVerified,
            displayName: this.user.displayName || this.user.email || this.user.phoneNumber,
            phoneNumber: this.user.phoneNumber,
            photoURL: this.user.photoURL,
            roles: {
              subscriber: true
            }
          };
          this.setUserData(data).catch(error => {
            console.log(error);
          });
        }
      }
    });

    this.user$.subscribe();
    this.userDoc$.subscribe();
  }

  getUserDoc$(col) {
    return user(this.authClient).pipe(
      switchMap(u => {
        return u ? docData(firebase.firestore().doc(`${col}/${(u as any).uid}`)) : of(null);
      })
    );
  }

  ///// Role-based Authorization //////

  canCreate(u: AjonpUser): boolean {
    const allowed = ['admin', 'editor'];
    return this.checkAuthorization(u, allowed);
  }

  canDelete(u: AjonpUser): boolean {
    const allowed = ['admin'];
    return this.checkAuthorization(u, allowed);
  }

  canEdit(u: AjonpUser): boolean {
    const allowed = ['admin', 'editor'];
    return this.checkAuthorization(u, allowed);
  }

  canRead(u: AjonpUser): boolean {
    const allowed = ['admin', 'editor', 'subscriber'];
    return this.checkAuthorization(u, allowed);
  }

  // determines if user has matching role
  private checkAuthorization(u: AjonpUser, allowedRoles: string[]): boolean {
    if (!u) {
      return false;
    }
    for (const role of allowedRoles) {
      if (u.roles[role]) {
        return true;
      }
    }
    return false;
  }

  public setUserData(u: AjonpUser) {
    return this.db.set(`users/${u.uid}`, u);
  }

  // Sets user data to firestore after succesful signin
  private updateUserData(u: AjonpUser) {
    return this.db.update(`users/${u.uid}`, u);
  }
  signOut() {
    this.authClient.signOut();
    location.href = '/pro';
  }
}
Enter fullscreen mode Exit fullscreen mode

The BEST Part

Now I can take every single component that I have created over the years and start using them on the site rather quickly!

Let me know what you think!

Discussion (0)

pic
Editor guide