AngularJS 2 - Superpowers FireBootCamp by SSW (in progress!)

Table of Contents

Chapter 1 - AngularJS 2 Superpowers FireBootCamp by SSW

Summary

  • AJS 2, Angular CLI, Components, Data Binding, Services, HTTP, Routing, Forms, ngrx, etc
  • Emmet
  • SSW Code
    • Today code - firecamp-crm-demo
    • Proven code - firecamp-crm

npm i

  • Libraries
    • Universal lib listen for SEO bot for serer-side rendering
    • Wallaby
  • Duncan Hunter Angular CLI Intro including vscode extensions
  • SSW Rules
  • RxJS (use v5, not stable v4) - simplifies reactive asynch coding
    • applying the Observer Pattern to streams (setup Observers to watch properties of the object and update UI)
    • dealing with streams of data, whereby Observable is an array, and can apply Operators to output multiple Observers (i.e. apply chains of operators to get streams such as Obs #1 .count, Obs #2 .where(x => x > 5), Obs #3 .where(x => x > 5).count()
    • Example
this.xStream = this.yFieldText.valueChanges
    .debounceTime(500)
    .distinctUntilChange()
    .switchMap(....);
  • Cont’d
    • avoids polling
    • http requests return Observables

Questions

  • code splitting - lazy loading - how differ to Webpack plugins, this is part of Angular CLI that uses Webpack?
  • where is State stored?
  • Promises vs Observables - consensus is that Observables better and use them (what about Cross-Cutting concerns??)
  • Is cache busting library added to Webpack config built-into AJS 2
  • Is Webpack code splitting how AJS 2 does Lazy Loading?
  • What is import By ... @angular/platform-browser
  • Why use variables ending with $ (i.e. this.companies$)

Component Tree

  • Module
    • Component (Typescript Class)
      • Inject services with DI
      • Prop binding from Component to Template
      • Event binding from Template (and Directives) back to Component

Lazy Loading

  • Store features in src/ to point to for lazy loading

Bootstrapping

  • main.js runs app.component.ts
  • @NgModule bootstraps AppModule and registers imports Angular Modules as dependencies so globally available
  • Interfaces files are just there for development purposes
  • Component file imports are just for TypeScript
  • Angular CLI has build chain using Webpack

Setup

  • Install AJS2 CLI https://cli.angular.io/

Angular CLI

  • Try to reduce size of bundled files
ng new ...
ng serve ...

Example App Architecture

  • App Module (top-level Typescript Class)
  • Home Module
    • Lazy load just module required
  • Company Module
    • CRUD

Scaffold and Build App

  • Generate component shorthand without unit tests ng g c company/company-list --spec false (generates component-list.component.ts / _.css / _.html)
  • Import the component logic into app.component.ts. Declare it in @NgModule declarations to make it globally available in app
  • Angular CLI version ng -v
  • Update app.component.html with Custom Directive (i.e. )
  • ngOnInit() lifecycle hook on instantiation to call function (i.e. this.companies) that retrieves from hard-coded static list, service abstraction, observable, and then back-end db
  • Handlebars `` to access properties of class model in html using built-in pipe filter
  • Create interface for class @Component property companies: any[] with ng g interface company/company to generate app/company/company.ts and change to Company[]
  • Implement Interface and import it into company-list.component.ts
  • Add styling to company-list.component.html using Bootstrap after installing npm i bootstrap --save, then update build setup angular-cli.json “styles” property with path to bootstrap.css in node_modules. DO NOT add to index.html, as the node_modules folder not exist when bundles and deploys to browser. Restart the app since changed build setup (i.e. ng serve ...)
  • Scaffold HTML Markup (using Emmet/Zen coding). Execute and hydrates to generate code on page tag | class | > tag > tag > qty table.table.table-striped>thead>tr>th*4
  • Create multiple handlebars `` at once using IDE feature
  • Snippet library by John Papa using ng2 to generate *ngFor="let company of companies" (* syntactic sugar that will be replaced when markup hydrates)
  • Wrap in Bootstrap container in html <div class="container"... (center aligned)
  • Add Component-specific CSS styling into company-list.component.css .btn-column { ... } (AJS 2 modifies the class name behind the scenes to be Component-specific .btn-column[_ngcontent...)
  • Add an “Add” button to .html
  • Retrieve data from CompanyService within getCompanies() { ...} instead of using hard-coded inline static list
  • Make Service ng g service company/company --spec false to generate company.service.ts and Move data retrieval from company-list.component.ts
  • company.service.ts uses @Injectable to chain dependencies (give all dependencies we need without having to plumb it up manually).

  • Create field-level variable and assign it in company-list.component.ts constructor(private companyService: CompanyService) { ...}.
  • DI plumbing requires dependencies to be registered in providers array of @NgModule so available globally (or optionally only to a feature module)
  • Change company.service.ts to retrieve data from using Observable by importing
...
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of'; (not entire kitchen-sink, just a subset as cherry picking so smaller build)
...
getCompanies() {
... 
    return Observable ...
}
  • Instead of using .then (Promises) for asynch, we use RxJS Observables pattern (baked into AJS2). Unwrap Observable using subscribe in component-list.component.ts. Call service using Observable.

getCompanies() { this.companyService.getCompanies() .subscribe(companies => this.companies = companies); // manual subscriber implementation

  • Alternative is Async Pipe (but ONLY use when only subscribing once, otherwise use .subscribe to minimise/consolidate amount of streams using operators like takeUntil to avoid performance issues, as each time used it creates new subscription!!) to deal with retrieved Obserable (instead of .subscribe line of code, which gets removed) (cast array of Company[] to Observable), then unwrap Observable in HTML markup using | async pipe and subscribe using async logic for us automatically

i.e. ` import {Observable … companies: Observable<Company[]>; `

  • Update Service and swap-out for call to http service using HttpModule in app.module.ts. Inject into company.service.ts constructor, and import Http
  • Service for API API Endpoint
  • Add Config file with API endpoint as class property to query i.e. API_BASE_URL = 'firebootcamp-crm-api/azurewebsite.net/api/'
import ( Http } from '@angular/http';
import 'rxjs/add/operator/map'; 
import 'rxjs/add/operator/catch';                           // offline compiler in AJS2 wants explicit throw
import 'rxjs/add/observable/of'; 
... 
constructor(private http: Http) {}
... 
... getCompanies() { 
    return this.http.get(this.API_BASE_URL + `company`)     // returns <Response>
        .map(data: Response => data.json())                 // RxJS .map like JS .map converts from Response type object to JSON data object we can use
        .catch(this.errorHandler);                          // Catch errors and pass to Error Handler

... private errorHandler(error) {
    console.log("custom error handler", error);
    return Observable.throw(error);                         // deal with error however want i.e. send to admin site like RayGun ???
}
  • Repeat for Delete Companies by updating company.service.ts passing in company id param i.e. this.http.delete(... + ${company.id}... and company-list.component.ts i.e. this.companyService.deleteCompany(companyId).subscribe(() => this.getCompanies());

  • Add Event Binding Handler for Click Event to company-list.component.html i.e. <button (click)="deleteCompany(company.id)...

  • Create AppRoutesModule outlet to DOM tree <router-outlet></router-outlet> to Route/navigate to a different Component as SPA (watch URL and change).
  • Create app.routes.ts and import Components to route to, and pass to AppModule.
  • Note: Nested routes and Nested router outlets allowed
  • Configure Routes
import { Routes, RouterModule } from '@angular/router';

// Match and inject into router outlet in DOM
const routes: Routes = [
    {path: '', redirectTo: 'company/list', pathMatch: 'full'},
    {path: 'home', redirectTo: 'company/list', pathMatch: 'full'},
    {path: 'companyList', component: CompanyListComponent}
];

@NgModule({
    // Metadata to pull in and configure
    imports: [RouterModule.forRoot(routes)],             // helper directives and logic such as router link to configure routes 
    exports: [RouterModule]
})
export class ApprouterModule {}
  • Update app.module.ts imports property array of @NgModule with AppRouterModule

  • Create route to navigate to when Add Company button clicked. Update routes with dynamic property. {path: 'company/edit/:id', component: CompanyEditComponent}

  • Add click handler to Add button in company-list.component.ts that takes us to new Component. Share same component for Add/Edit <button routerLink="/company/edit/new // when click “Add” button <button routerLink="/company/edit/ ... // when click “Edit” button

  • Add link to Company List <a routerLink="/home" ...

  • Add new CompanyEditComponent ng g c company/company-edit --spec false

  • Modify CompanyEditComponent and inject dependencies

company = <Company>{};      // create Company object loaded by ngOnInit
companyId = this.activatedRouter.snapshot.params['id'];
isNewCompany = this.companyId === 'new' ? true : false;

constructor(private companyService: CompanyService,
    private router: Router,
    private activatedRoute: ActivatedRoute
) { }
  • Update company-edit.component.ts to detect if in New or Edit mode ` ` and add logic in ngOnInit

  • Create form in company-edit html .form-group*2>label[for=item$]{item$}+input.form-control+.alert.alert-danger

  • Two-way data binding AJS 2 - Prop binding with event handler i.e. <input [(ngModel)]="company.name" // Component name property bind to input field (Banana-in-a-box syntax)

  • Note:
    • ( … ) - event handling
    • [ … ] - property binding
    • component.name - Components model
    • store data in properties of Component
    • dumb views
  • Template Reference Variable (handle to access for input field for other Components) #name="ngModel" // set variable #name to model property of the input field [hidden]="name.valid || name.pristine" // set hidden property of div if name property is valid (for validation div)
  • Configure Form <form #companyForm="ngForm" (ngSubmit)="saveCompany()" // create variable companyForm. Call method on form submit event <button type="submit" [disabled]="companyForm.invalid"

  • Update company-edit.component.ts getCompany() metod to retrieve company from companyService and .subscribe to it and assign it to the Component company property
  • Repeat for saveCompany() and ensure corresponding methods exist in companyService. Pass Company object into saveCompany method in Service. Add headers for POST to service inline (refactor into helper function)

saveCompany(company: Company) { const headers = new Headers({'content-type' : 'application/json'}); const options = new Request

  • Import Headers and Request from Http dependency

  • Programmatic Routing this.router.navigateByUrl ...

Lazy Load Homepage

  • Create a new Home Component using AJS2 CLI. Generate /src/app/home/home*
ng g module home
  • Lazy load through router and build system using conventions
  • Create /src/app/home/home.routes.ts and customise

{ path: '', component: HomeComponent }, imports forChild

@NgModule({ … HomeRouter

  • If we import Home into App, it would load all JS for both, but we only want JS for specific module being used to load using Lazy Loading
    • Decouple app.routes.ts, by passing in { path: 'home', loadChildren: 'app/home/home.module#HomeModule' } instead of just a string for the module (i.e. physical_path # class_name_for_module)
    • Restart ng serve since our new HomeModule is 0.chunk.js which needs to be built before main.bundle.js
    • Check browser Network where both are loaded, clear network traffic, reload, click the link to HomeModule, and see 0.chunk.js load (Webpack code splitting)
    • Code and compiler compiled in browser to see how fast load
    • Tree Shaking optimisation (i.e. JS file with functions, it removes/tree shakes out other fns as not used so ahead of time know dependency graph of what required). Only ES6 Class libraries can be Tree Shaken

Deploy

  • Deploy using Webpack server (part of AJS 2 CLI)

ng build --prod // read app and deploy to /dist folder (–prod does source maps, etc)

ng build --prod --aot // extra step compile offline html templates in node server, tree shaking, minification, etc (very buggy) // check if any libraries loaded that do not support AOT as may present issue

http-server // browser Empty Cache & Hard Reload, go to localhost:8080/company/list, and loads in < 1000ms

  • Load dist/index.html in production

Testing

  • Jasmine - assertion lib / BDD
  • Karma - test runner
  • Wallaby - shows inline testing results in real-time

.spec.ts in src folders

  • Isolated Test

home.component.spec.ts

import ( HomeComponent } from './home.component';

describe ('Component: HomeComponent', () => {
    it ...
}

ng test // AJS 2 auto finds .spec.ts files

  • See sample code company-list.component.spec.ts

  • Isolate test usng Mocks
  • TestBed // module
  • providers // means pass me my real CompanyService
  • NO_ERRORS_SCHEMA // continue run regardless of errors
  • spyOn // intercept requests and replace with mock

State Magagement / Async - Redux / ngRX

firebase-crm-ngrx

  • Perfect for managing complex state

  • redux pattern - implement a predictable state container
  • Store - maintains state
  • Component - subscribes to Store and passes to View
  • Reducer - only place change state
  • Dispatcher - dispatch Action Payload to Store

  • Parent and Dummy Presetation Components - can turn off change detection in Children

  • Use browser @ngrx debugger
    • Click the headings in debugger to revert state
  • See modifications in: company.reducer.ts & company-list.component.ts ?? (subscribe to store private store: Store<any>

  • redux pattern (State Management / Unidirectional Data Flow) + RxJS (Observables) + AJS 2 = ngrx

Common Bugs

  • Missing forward slash at start of route i.e. <button routerLink="/company/edit/ ...

TODO

  • Obtain slides afterward
  • Investigate using Interceptor as part of Async Pipe
  • Vue.js
  • Dynamically create links for Router by passing in relative path and reuse a path from its parent
  • tslint.json to configure Code Rules Consistency for teams is good for code reviews
  • Add Observables in routes
  • DotNet Meetup User Group in Dec (2nd week) - AJS 2 being done

Workflow

  • Switch to browser with 3 finger swipe
  • Browser debugging add breakpoints for RxJS Observable to ensure .subscribe added for it to fire and track changes
Written on November 25, 2016