Skip to content

Instantly share code, notes, and snippets.

@jonepl
Last active December 31, 2022 17:10
Show Gist options
  • Save jonepl/81f7fb9170b70b9bd5a6d4845b94ee35 to your computer and use it in GitHub Desktop.
Save jonepl/81f7fb9170b70b9bd5a6d4845b94ee35 to your computer and use it in GitHub Desktop.

Angular Basics

Read Documentation

Common Errors

Why Angular?

Angular is framework which helps build interactive and dynamic single page applications (SPAs) through its compelling features that include templating, two-way binding, modularization, RESTful API handling, dependency injection, and AJAX handling

Framework (Angular) vs Library (React & Vue)

Frameworks determined how your project will be structure and are typically not flexible. Library are building blocks that can be used anywhere.

Angular Components

In order to create an Angular component, your file must have:

  • Class (Typescript) , properties and methods
  • Decorator
  • Template (HTML), data bindings and directives

Classes, Decorator, Templates & Metadata examples

Every class needs metadata in order to be a component. To associate metadata to a class you must wrap it within a Decorator (similar to a C# Attributes or Java Annonations).

Example

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

//  Metadata   
@Component({                // Component Decorator
    selector: 'pm-root',    // Selector metadata that define the components directive (AKA tag)
    template:               // (HTML) Template. Can contains data binding {{pageTitle}}
    `        
        <div>
            <h1>{{pageTitle}}</h1>  
            <div>My Example Component</div>
        </div>
    `
})
// Class/Module
export class AppComponent {
    // Property and functions used with binding
    pageTitle: string = 'Acme product Management';
    showImage: boolean = false;
    toggleImage() : void {
        this.showImage = !this.showImage
      }
}

Component Metadata

Templates

Within your Component decorator exists a template metaData value. This can be represented using Inline templates or template URL (recommended).

The cons of using Inline templates is that there is no intellisense, syntax highlighting or error checking

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

// NOTE: Only one template should be used   
@Component({
    ...
    template: // Example of inline template
    `        
        <div>
            <h1>{{pageTitle}}</h1>  
            <div>My Example Component</div>
        </div>
    `
    templateUrl: './product-list.component.html' // Example of URL template
})
// Class/Module
export class AppComponent {
    ...
}

Styles

Within your Component decorator exists a styles metaData value. This can be represented using Inline templates or styles URL (recommended).

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

// NOTE: Only one style should be used   
@Component({
    ...
    // Example of inline styles
    styles: ['thead {color: #337AB7;}']

    // Example of style URL
    stylesUrl: './product-list.component.css' 
})
// Class/Module
export class AppComponent {
    ...
}

Data Binding

Binding - coordinates communication between the component's class and its template and often involves passing data

Types of Binding

  • Text Interpolation
  • Property Binding
  • Event Binding
  • One way Binding
  • Two way Binding

One way Data Binding

Interpolation is one way binding used within the HTML Angular template. This binding uses curly braces {{}}

<h1 prop={{class-variable}}></h1>

Property Binding is one way binding from an Angular component to the Angular template. This binding uses square brackets [].

The brackets, [], cause Angular to evaluate the right-hand side of the assignment as a dynamic expression. Without the brackets, Angular treats the right-hand side as a string literal and sets the property to that static value.

<!-- src is an html element property -->
<!-- product.imageUrl is a component property -->
<img [src]='product.imageUrl'>

Event Data Binding is one way binding from an Angular template's (via HTML attribute's event property) to the Angular component via (class method). This binding uses bracket ().

Valid events: https://developer.mozilla.org/en-US/docs/Web/Events

<!-- click is an html event element property -->
<!-- toggleImage() is a component method -->
<button (click)='toggleImage()'>

Two way Data Binding gives components in your application a way to share data. Use two-way binding to listen for events and update values simultaneously between parent and child components. The syntax for this binding is a combination of Property and Event Binding [()].

<input type="text" [(ngModel)]='classMethod'/>

Angular Directives

Directives are classes that add additional behavior to elements in your Angular applications. Directives are written within the template's HTML similar to an HTML attributes.

Types of Directives

  • Structual Directives - modifies the structure the structure or layout of a view by adding, removing, or mapping elements and their children. Example below of an builtin directive ngIf annotated with "*" to be a structure directive.

    <table class="table" *ngIf='products.length'>
  • Attribute Directives - changes the appearance or behavior of an element. See angular documentation LINK

  • Components Directives - directives that have a template

  • Custom Directives - User defined functionality

Transforming Data with Pipes

Pipes can be used to transform bound properties before displaying

Types of built-in pipes

  • dates
  • number
  • decimal
  • percent
  • currency
  • json
  • etc

Pipes can be used with a | character. Examples below

  • {{varName.property | lowercase }}
  • <img [title]='varName.property | uppercase'>

Custom Pipes can be used to transform bound properties before displaying

@Pipe({
    name: 'convertToSpaces'
})
export lass ConvertToSpacesPipe implements PipeTransform {
    transform(value: string, character: string) : string {

    }
}

Makes sure to add the module to

Using custom pipes

<td>{{className.property | convertToSpaces: arg1, arg2 }}</td>

App Modules and Angular Bootstrapping

Bootstraping is the process of loading the index.html page, app-level module, and app-level component.

  • Here are the following steps to Angular Bootstrap
  1. Load index.html
  2. Load Angular, Other Libraries, and App Code
  3. Execute main.ts File
  4. Load App-Level Module
  5. Load App-Level Component
  6. Process Template

The bootstrapping process sets up the execution environment, digs the root AppComponent out of the module's bootstrap array, creates an instance of the component and inserts it within the element tag identified by the components's selector

Angular App Module

// app.module.ts

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';

// Using NgModule decorator & metadata to define a NgModule
@NgModule({
    imports: [ BrowserModule ],     // External modules
    declarations: [ AppComponent ], // Belongs to this module
    bootstrap: [ AppComponent ]     // Startup Component of the application
})

export class AppModule { }

Bootstrap

  1. Every application must bootstrap at least one component, the root application component.
  2. The bootstrap array should only be used in the root application module, AppModule.

Additional Details about the NgModule decorator

Imports Array

  1. Adding a module to the imports array makes available any components directives, and pipes defined in that modules's export array
  2. Only import what this module needs
  3. Importing a module does NOT provide access to its imported modules. (Imports are not inherited)
  4. Use the imports array to register services provide by Angular or third party modules

Declarations Array

  1. Every component, directive, and pipe we create must belong to one and only one Angular modular
  2. Only declare components, directives and pipes.
  3. All declared components, directives, and pipes are private by default
  4. The Angular module provides the template resolution environment for its component templates.

Exports Array

  1. Export any component, directive, or pipe if other components needs it.
  2. Re-export module to re-export their components, directives, and pipes.
  3. We can export something without including it in the imports array.

Provider Array

Providers Array within an NgModel have been deprecated. Use the @Injectable decorator

Growing an Angular Project

Creating new Components

  1. Create new folder
  2. Create html template file
  3. Create component file
  4. Add component to the app.modules.ts

Best Practices

Class

Class naming convention ==> FeatureName<Component>

property naming convention ==> camelCase

methods naming oftenVerbs ==> camelCase

Custom Types

In order to make a custom type you must make an interface. There two ways to use an Interface in Angular.

  1. Interface as a Data Type

    export interface IProduct {
        productId: number;
        productName: string;
        ...
    }
    products: IProduct[] = []
  2. Interface as a Feature sets

    export interface DoTiming {
        count: number;
        start(index: number) : void;
        stop() : void;
    }
    export class myComponent implements DoTiming {
        count: number = 0;
        start(index: number) void {
            // implement functionality
        }
        stop(): void{
            // implement functionality
        }
    }

Angular Life Cycle Hooks

Life Cycle Hooks are interfaces

  • OnIt - Perform component intitializtion retireve data
export class MyComponent implements OnInit {

    ...
    ngOnInit(): void {
        console.log('In OnInit');
    }
}
  • OnChange - Perform action after change to input properties

  • OnDestroy: Perform cleanup before Angular destroys component

Nested Components

Nested Components

A Input decorator property is used within a nested component to communicate with its parent component. The parent component uses property binding within it's template to pass down its data. The nested component can send information back to the parent by emitting events using event binding with the @Output decorator property. Once the event binding is set up in the parent component's template, it can be handle in the parent class.

// nestedClass.ts
import { Component, OnChanges, Input } from "@angular/core";

@Component({
    selector: 'pm-star',
    templateUrl: './star.component.html',
    styleUrls: ['./star.component.css'],
})
export class StarComponent implements OnChanges{
    // Enables input binding from parent
    @Input() rating: number = 0;
    // Enables output binding to parent
    @Output() ratingClicked: EventEmitter<string> = new EventEmitter<string>();

    onClick() : void {
        this.ratingClicked.emit(`The rating ${this.rating} stars!`)
    }
}
    ...
        <tr>
            <!-- parent Class' template using Property binding to pass along data to it's child component-->
            <td><pm-star [rating]="product.starRating" (ratingClicked)="parentClickHandler()"></pm-star></td>
        </tr>
    ...
    // parentClass.ts
    @Component({
        ...
    })
    export class ParentComponent {
        ...
        onRatingClicked(message: string) : void {
            this.pageTitle = 'Product List' + " " + message

        // Parent class handling event binding from child component
        parentClickHandler(message: string) : void {
            this.varName = message
        }
    }

Services and Dependency Injections

A services is a component with a focus purpose. Angular Injector manages singleton services.

When to create a service

  • Functionality is not required by the view
  • Shared logic or business rules are needed across components

Angular Injector

Angular Injectors are structured in a heiracrchel manner. When the constructor of a class is invoked. Angular traverses the heirarcy from the bottom (class) up (App).

  • Create the service class
  • Define the metadata with a decorator
  • Import what we need

Configuring a Services

In order to configure a service, you have to register the service with the angular injector. There are 3 ways to associate your an Angular service to the Angular provider.

  • Annotation Provider Injection (root or class level)
  • Class Level Injection
  • Module Level Injection

Annotation Provider Injection

  1. Start by adding the Injectable anotation and specify how it should be provided to Angular
import { Injectable } from '@angular/core'

// This class decorator signals to Angular that this is a Service. Using in this manner makes the
// provider "Tree Shakeable (will not be included if it is not used)"
@Injectable({
    // Indicates registering to root Angular Injector. Angular version 6+
    providedIn: 'root'
})
export class RandomService {
    getClassProp() : IClassProp[] {
        ...
    }
}
  1. Inject your service into constructor of the classes that need it. To avoid side effects, use the property in the appropriate life cycle hook.
export class SomeClass implements OnInit{
    
    this.property = []

    // Injecting the service into the class
    constructor(private randomService: RandomService) {}

    // Assigning the data from the service
    ngOnInit() : void {
        this.property = this.randomService.getProperty()
    }
}

Alternatively, you can register the Injector with a specific component or to an entire App Component.

** Class level Injection**

@Component({
    templateUrl : './some.component.html',
    providers: [RandomService]
})

Module Level Injection

Or instead, you can injector your service via Module level injection

// app.module.ts
@NgModule({
    declarations: [
        AppComponent,
    ],
    ...,
    bootstrap: [AppComponent],
    // Makes service available everywhere with this App
    providers: [
        RandomService, // Implict token representation
        { provide: LoggerService, useClass: LoggerService } // Explicit token representation
    ] 
})

export class AppModule { }

NOTE: Services are Singletons and can contain state. This state can be use to share across multiple components.

Asynchrous Services

Aysnchrous Services - services with methods that execute asynchronous.

Different return types (Observables, Promises).

Reactive Extensions - a library for composing data using observable sequences. Angular uses Reactive Extensions for working with data.

An Angular Observable sequence is a collection of items overtime. Observable doesn't do anything until you subscribe. There are 3 types notification.

  • Next notification - used to process responses
  • Error notification - used for error handling
  • Complete notification - used to complete a Observable notification

NOTE: We can specify a pipeline of observables to transform the incoming observable.

General Usage Steps for Observables

  1. Create an observable.
  2. Subscribe to the Observable within the Component that needs the data.
  3. Unsubscribe to the Observable within the Component that used the data.

HTTP and Observables Example

  1. Add HTTPClient to Angular module
@NgModule({
    declarations : [...],
    imports: [
        ...,
        HttpClientModule // Includes the Http module
    ]
})
export class AppModule {}
  1. Use a Service component (@Injectable) to handle HttpClient

  2. Use Dependency Injection within this service's constructor to access HttpClient's Observable return object

// RandomService.ts
@Injectable({
    providedIn: 'root'
})
export class RandomService {

    constructor(private http: HttpClient) {}

    ...
}
  1. Call a Http method to return a Observable object
    • Observables data can be transform using pipe method
import { Observable, throwError } from "rxjs";
import { HttpClient, HttpErrorResponse } from "@angular/common/http";


// RandomService.ts
@Injectable({
    providedIn: 'root'
})
export class RandomService {

    constructor(private http: HttpClient) {}

    getItems() : Observable<ICustomDataType[]>{
        // Stores Observable using Http GET
        let prodObservable = this.http.get<ICustomDataType[]>(this.randomUrl);
        // Example of Observable pipe
        return prodObservable.pipe(
            tap(data => console.log('All: ', JSON.stringify(data))),
            catchError(this.handleError)
        );
    }

    handleError(err: HttpErrorResponse) {
        ...

        return throwError('Some Message')
    }
}
  1. With the component using the Observable, subscribe to the Observable
    • define the next, error and complete notification as needed
    • the next notification should handle the async call within a life cycle hook
  2. Unsubscribe to the Observable within the Component that used the data. Use a life cycle hook like ngOnDestroy to handle the destruction of the subscription
// RandomComponet.ts
import { Subscription } from 'rxjs';
import { RandomService } from "./RandomService"

@Component({
    ...
})
export class RandomComponet implements OnInit, OnDestroy {
    
    subscription!: Subscription;
    compItems: string[] = []
    
    constructor(private randomService: RandomService) {}

    ngOnInit() : void {
        // Subscribes to the Observable from our Dependency Injected Serivce
        this.subscription = this.productService.getItems()
            .subscribe({
                next: items => { 
                    this.compItems = items;
                },
                error: err => this.errorMessage = err,
                () => console.log('completion function. Good for logging')
            });
    }

    ngOnDestroy(): void {
        // Unsubscribes from the Observable from our Dependency Injected Serivce
        this.subscription.unsubscribe();
    }

}

Usage

  • Start Observable
  • Pipe emitted
  • Process notification next, error, complete
  • Stop observable
import { Observable, range } from 'rxjs'
import { map, filter } from 'rxjs/operators'

const source$: Observable<number> = range(0, 10);

source$.pipe(
    map(x => x * 3),
    filter(x => x % 2 === 0)
).subscribe(x => console.log(x))
x.subscribe()

x.subscribe(Observer)

const sub = x.subscribe({
    nextFn,
    errorFn,
    completeFn
})

Angular Routing

Routing Basics

Angular is used to design Single Page Applications. In a SPA, each components take turns appearing on page. The routing is responsible for displaying a component. An angular application has one router that is managed by Angulars router service. Register the router serivice provider and delcares ther router directives.

General Usage Steps for enabling Routing

  1. Base element is set to href="/" in the index.html file
  2. Add routing module and all routes to root module
  3. Define routes in a nav bar within the highest level component
  4. Add router directive to the above component
// app.module.ts
@NgModule({
    declarations: [...]
    imports: [
        ...
        // 1. Adding router module and all routes Order matters. 
        RouterModule.forRoot([
            {path: "products", component: ProductListComponent },
            {path: "products/:id", component: ProductDetailComponent },
            {path: "welcome", component: WelcomeComponent },
            {path: "", reidrectTo: "welcome", pathMatch: "full" },
            {path: "**", component: PageNotFoundComponent },
        ])
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
// app.component.html
<!-- 2. Defining Navbar to give user the ability to navigate between routes -->
<nav class="navbar navbar-expand navbar-light bg-light">
    <a href="" class="navbar-brand">{{pageTitle}}</a>
    <ul class="nav nav-pills">
        <li><a class="nav-link" routerLink="/welcome">Home</a></li>
        <li><a class="nav-link" routerLink="/products">Product List</a></li>
    </ul>
</nav>
<div class="container">
    <!-- 3. Adding router directive -->
    <router-outlet></router-outlet>
</div>

Parameter Routing

  • Routing parameters
  • Activating routes with code
  • Protecting routes with guards

To route parameters we must use two new routing modules

  • ActivatedRoute
  • Router

Inject these modules into your component's life cycle hook.

In order to access routing information for one time usage. You can access you parameter using one time usage snapshot or dynamically with Observables

Exmaple

<!-- example.component.html -->
<td>
    <!-- arg[0] ==> path, arg[1]  ==> id -->
    <a [routerLink]="['/products', product.productId]">{{product.productName}}</a>
</td>
// example.component.ts

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

// Angular Service Inject provides the ActivatedRoute
constructor(private route: ActivatedRoute) {}

// use if route doesn't change
this.route.snapshot.paramMap.get('id')

// gets route param with Observable
this.route.paramMap.subscribe(params => console.log(params.get('id')));

NOTE: You can implement safe navigation error by using *ngIf or the ? operator for the variable that holds you item information.

Building a Guard

In order to build a Guard you must:

  1. Create your Guard and implement the true and false condition for you Gaurd.
// ProductDetailGuard.ts
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable({
    providedin : 'root'
})
export class ProductDetailGuard implements CanActivate {

    canActivate() : boolean {
        if(condition) {
            ...
            return true // Allows route
        } else {
            ...
            this.router.navigate(['/products']); // routes back to your page
            return false
        }
    }
}
  1. Include the Gaurd within the root module
// app.module.ts
import { ProductDetailGuard } from './products/product-detail.guard';

@NgModule({
    declarations: [...]
    imports: [
        ...
        // 1. Adding router module and all routes Order matters. 
        RouterModule.forRoot([
            ...
            {
                path: "products/:id",
                canActivate: [ProductDetailGuard], // Including
                component: ProductDetailComponent 
            },
        ])
    ],
    bootstrap: [AppComponent]
})
export class AppModule { }
    canActivate: [ProductDetailGuard],

Split NgMode into Feature modules

ng g m products/product --flat -m app

A command line interface for Angular developers to assit with

  • Building Angular applications
  • Generating Angular files
  • Building and serving the application
  • Running tests
  • Building the application for deployment

See documentation for details.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment