Architecture


    // you cant refer directly to a class until its been defined
    // break the circularity
    providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],
  

NgModule


    // --- APP.MODULE.TS

    import { NgModule } from '@angular/core';
    // when you want to run your app in a browser
    import { BrowserModule } from '@angular/platform-browser';
    // when you want to use pipes and directives like NgIf, NgFor
    // import { CommonModule } from '@angular/common'; // for feature modules
    // when you want to build template driven forms (includes NgModel)
    import { FormsModule } from '@angular/forms';
    // when you want to build reactive forms
    import { ReactiveFormsModule } from '@angular/forms';
    // when you want to use RouterLink, .forRoot(), and .forChild()
    import { RouterModule } from '@angular/router';
    // when you want to talk to a server
    import { HttpClientModule } from '@angular/common/http';

    import { AppComponent } from './app.component'; // App Root

    // Feature Modules
    // import to add to the imports array below
    import {
      CustomerDashboardModule
    } from './customer-dashboard/customer-dashboard.module';
    import { CoreModule } from './core/core.module';

    import { UserService } from './user.service';

    // UI module case
    export function SOME_SERVICE_FACTORY(parentService: SomeService) {
      return parentService || new SomeService();
    }

    @NgModule({

      // --- Static, compiler configuration
      // which components, directives, and pipes belong to module, local scope (private visibility)
      // declare every component in exactly one NgModule class
      // error - if you declare same class in more than one module
      // invisible to components in a different module unless they are exported
      // import the module when you need it elsewhere
      declarations: [
        AppComponent // new app project root NgModule has only one component
      ], // Configure the selectors

      // --- Composability / Grouping
      // other NgModules that this module needs to function properly
      // those that export components, directives, or pipes that component templates in this module reference
      // composing NgModules together
      imports: [
        CommonModule,
        BrowserModule, // use browser-specific services
        FormsModule,
        HttpClientModule,
        ...
        CustomerDashboardModule
        // CustomerDashboardModule.forRoot({userName: 'Miss Marple'})
      ],

      // --- Runtime, or injector configuration
      // services the application needs
      // they become available app-wide
      // global scope (public visibility)
      // specify providers at the component level (preferred)
      // scope them when using feature modules and lazy loading
      providers: [
        UserService,
        Logger
        // UI module case
        {
          provide: SomeService,
          deps: [[new Optional(), new SkipSelf(), SomeService]],
          useFactory: SOME_SERVICE_FACTORY
        }
      ],

      // subset of declarations that should be visible and usable
      // in the component templates of other NgModules
      // making NgModules available to other parts of the app
      exports: [
        AppComponent
      ],

      // components that are automatically bootstrapped
      // usually the root component of the application
      // Angular can launch with multiple bootstrap components,
      // each with its own location in the host web page
      bootstrap: [ AppComponent ]

      // root NgModule has no reason to export anything
      // because other modules dont need to import the root NgModule

    })
    export class AppModule { }

    // --- APP.COMPONENT.TS

    import { Component } from '@angular/core';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'app works!';
    }

    // --- APP.COMPONENT.TS

    <h1>
      {{title}}
    </h1>
    // selector from CustomerDashboardComponent
    <app-customer-dashboard></app-customer-dashboard>

    // --- CUSTOMER-DASHBOARD/CUSTOMER-DASHBOARD.MODULE.TS - feature module

    import {
      NgModule, ModuleWithProviders, Optional, SkipSelf
    } from '@angular/core';
    //import { CommonModule } from '@angular/common';
    @NgModule({
      imports: [
        //CommonModule,
        SharedModule
      ],
      exports: [
        // export this module inner component
        // for <app-customer-dashboard> usage in root module
        CustomerDashboardComponent
      ],
      declarations: [
        CustomerDashboardComponent
      ],
    })
    export class CustomerDashboardModule {
      constructor (@Optional() @SkipSelf() parentModule: CustomerDashboardModule) {
        if (parentModule) {
          throw new Error(
            'CustomerDashboardModule is already loaded. Import it in the AppModule only');
        }
      }
      static forRoot(config: UserServiceConfig): ModuleWithProviders<CoreModule> {
        return {
          ngModule: CoreModule,
          providers: [
            { provide: UserServiceConfig, useValue: config }
          ]
        };
      }
    }

    // --- CUSTOMER-DASHBOARD/CUSTOMER-DASHBOARD.COMPONENT.TS

    import { Component } from '@angular/core';
    @Component({
      selector: 'app-customer-dashboard',
      templateUrl: './customer-dashboard.component.html',
      styleUrls: ['./customer-dashboard.component.css']
    })
    export class CustomerDashboardComponent { }

    // --- CUSTOMER-DASHBOARD/CUSTOMER-DASHBOARD.COMPONENT.HTML

    <p>
      customer-dashboard works!
    </p>

    // --- USER.SERVICE.TS

    import { Injectable, Optional } from '@angular/core';
    let nextId = 1;
    export class UserServiceConfig {
      userName = 'Philip Marlowe';
    }
    @Injectable({
      // make automatically available to the whole application, singleton by default
      providedIn: 'root'
      // UserService is unavailable to applications unless UserModule is imported
      providedIn: UserModule,
    })
    export class UserService {
      id = nextId++;
      private _userName = 'Sherlock Holmes';
      constructor(@Optional() config: UserServiceConfig) {
        if (config) { this._userName = config.userName; }
      }
      get userName() {
        // Demo: add a suffix if this service has been created more than once
        const suffix = this.id > 1 ? ` times ${this.id}` : '';
        return this._userName + suffix;
      }
    }

    // --- SHARED/SHARED.MODULE.TS

    import { NgModule }            from '@angular/core';
    import { CommonModule }        from '@angular/common';
    import { FormsModule }         from '@angular/forms';
    import { AwesomePipe }         from './awesome.pipe';
    import { HighlightDirective }  from './highlight.directive';
    @NgModule({
      imports:      [ CommonModule ],
      declarations: [ AwesomePipe, HighlightDirective ],
      exports:      [ AwesomePipe, HighlightDirective,
                      CommonModule, FormsModule ]
    })
    export class SharedModule { }
  

possible modules separation


    app/
    |-- app.module.ts
    |-- app-routing.module.ts
    |-- core/
        |-- auth/
          |-- auth.module.ts (optional since Angular 6)
          |-- auth.service.ts
          |-- index.ts
        |-- othermoduleofglobalservice/
    |-- ui/
        |-- carousel/
          |-- carousel.module.ts
          |-- index.ts
          |-- carousel/
              |-- carousel.component.ts
              |-- carousel.component.css
        |-- othermoduleofreusablecomponents/
    |-- heroes/
        |-- heroes.module.ts
        |-- heroes-routing.module.ts
        |-- shared/
          |-- heroes.service.ts
          |-- hero.ts
        |-- pages/
          |-- heroes/
              |-- heroes.component.ts
              |-- heroes.component.css
          |-- hero/
              |-- hero.component.ts
              |-- hero.component.css
        |-- components/
          |-- heroes-list/
              |-- heroes-list.component.ts
              |-- heroes-list.component.css
          |-- hero-details/
              |-- hero-details.component.ts
              |-- hero-details.component.css
    |-- othermoduleofpages/

    // --- MODULES OF PAGES, will contain 3 things

    @NgModule({
      imports: [CommonModule, MatCardModule, PagesRoutingModule],
      declarations: [PageComponent, PresentationComponent]
    })
    export class PagesModule {}
    // - /shared - services and interfaces
    @Injectable({ providedIn: 'root' })
    export class SomeService {
      constructor(protected http: HttpClient) {}
      getData() {
        return this.http.get<SomeData>('/path/to/api');
      }
    }
    // - /pages - routed components
    @Component({
      template: `<app-presentation *ngIf="data" [data]="data"></app-presentation>`
    })
    export class PageComponent {
      data: SomeData;
      constructor(protected someService: SomeService) {}
      ngOnInit() {
        this.someService.getData().subscribe((data) => {
          this.data = data;
        });
      }
    }
    // - /components - pure presentation components
    @Component({
      selector: 'app-presentation',
      template: `<h1>{{data.title}}</h1>`
    })
    export class PresentationComponent {
      @Input() data: SomeData;
    }

    // --- MODULES OF REUSABLE COMPONENTS

    @NgModule({
      imports: [CommonModule],
      declarations: [PublicComponent, PrivateComponent],
      exports: [PublicComponent]
    })
    export class UiModule {}
    // - extra code for each public UI service to prevent them to be loaded several times
    export function SOME_SERVICE_FACTORY(parentService: SomeService) {
      return parentService || new SomeService();
    }
    @NgModule({
      providers: [{
        provide: SomeService,
        deps: [[new Optional(), new SkipSelf(), SomeService]],
        useFactory: SOME_SERVICE_FACTORY
      }]
    })
    export class UiModule {}
    // - entry point, export the NgModule, the public/exported components
    // (and maybe directives, pipes, public services, interfaces and injection tokens)
    export { SomeUiComponent }  from './some-ui/some-ui.component';
    export { UiModule } from './ui.module';

    // --- MODULES OF GLOBAL SERVICES, not necessary since Angular 6

    @NgModule({
      providers: [SomeService]
    })
    export class SomeModule {}
  
use of each feature module type and their typical characteristics, in real world apps, you may see hybrids
Feature Module Guidelines
Domain deliver a user experience dedicated to a particular application domain like editing a customer or placing an order, typically have a top component that acts as the feature root and private, supporting sub-components descend from it, consist mostly of declarations, only the top component is exported, rarely have providers, when they do, the lifetime of the provided services should be the same as the lifetime of the module, typically imported exactly once by a larger feature module, might be imported by the root AppModule of a small application that lacks routing

Routed domain feature modules whose top components are the targets of router navigation routes, all lazy-loaded modules are routed feature modules by definition, dont export anything because their components never appear in the template of an external componentm lazy-loaded routed feature module should not be imported by any module, doing so would trigger an eager load, defeating the purpose of lazy loading that means you wont see them mentioned among the AppModule imports, an eager loaded routed feature module must be imported by another module so that the compiler learns about its components, rarely have providers, when they do, the lifetime of the provided services should be the same as the lifetime of the module, dont provide application-wide singleton services in a routed feature module or in a module that the routed module imports

Routing provides routing configuration for another module and separates routing concerns from its companion module, typically does the following: - defines routes, - adds router configuration to the modules imports, - adds guard and resolver service providers to the module providers, - name of the routing module should parallel the name of its companion module, using the suffix "Routing": FooModule in foo.module.ts has a routing module named FooRoutingModule in foo-routing.module.ts, if the companion module is the root AppModule, the AppRoutingModule adds router configuration to its imports with RouterModule.forRoot(routes), all other routing modules are children that import RouterModule.forChild(routes) - routing module re-exports the RouterModule as a convenience so that components of the companion module have access to router directives such as RouterLink and RouterOutlet. does not have its own declarations, components, directives, and pipes are the responsibility of the feature module, not the routing module, routing module should only be imported by its companion module

Service provide utility services such as data access and messaging, they consist entirely of providers and have no declarations, HttpClientModule is a good example of a service module, root AppModule is the only module that should import service modules
Widget makes components, directives, and pipes available to external modules, many third-party UI component libraries are widget modules, widget module should consist entirely of declarations, most of them exported, should rarely have providers, import widget modules in any module whose component templates need the widgets
Shared commonly used directives, pipes, and components in one NgModule, typically named SharedModule, then import just that NgModule in other parts of app, import the shared NgModule in domain NgModules, including lazy-loaded NgModules, shared NgModules should not include providers, nor should any of its imported or re-exported NgModules include providers
key characteristics of each feature module group
Feature Module Declarations Providers Exports Imported by
Domain Yes Rare Top component Feature, AppModule
Routed Yes Rare No None
Routing No Yes (Guards) RouterModule Feature (for routing)
Service No Yes No AppModule
Widget Yes Rare Yes Feature

Component/Directives


    import {
      AfterContentChecked,
      AfterContentInit,
      AfterViewChecked,
      AfterViewInit,
      DoCheck,
      OnChanges,
      OnDestroy,
      OnInit,
      SimpleChanges
    } from '@angular/core';
    import { Component, Input } from '@angular/core';
    import { LoggerService }    from './logger.service';

    // class Hero {
    //   constructor(public name: string) {}
    // }
    import { Hero } from './hero';

    let nextId = 1;
    export class PeekABoo implements OnInit {
      constructor(private logger: LoggerService) { }
      // implement OnInit's `ngOnInit` method
      ngOnInit() { this.logIt(`OnInit`); }
      logIt(msg: string) {
        this.logger.log(`#${nextId++} ${msg}`);
      }
    }

    @Component({
      // supported selectors include: element, [attribute], .class, and :not()
      selector:    'app-hero-list', // component tag
      templateUrl: './hero-list.component.html', // host view: link OR inline template
      // template: `
      //   <h1>{{title}}</h1>
      //   <h2>My favorite hero is: {{myHero}}</h2>`,
      // template: ` // for child view hooks
      // <div>-- child view begins --</div>
      //   <app-child-view></app-child-view>
      // <div>-- child view ends --</div>
      // <p *ngIf="comment" class="comment">
      //   {{comment}}
      // </p>`,
      // template: ` // for content hooks
      // <div>-- projected content begins --</div>
      //   <ng-content></ng-content>
      // <div>-- projected content ends --</div>
      // <p *ngIf="comment" class="comment">
      //   {{comment}}
      // </p>`
      providers:  [ HeroService ], // array of services providers for component
      styles: ['p {background: LightYellow; padding: 8px}'],
      styleUrls: ['./hero-app.component.css'], // .scss, .less, .styl
      // encapsulation: ViewEncapsulation.Emulated|None|ShadowDom
    })
    // Don't HAVE to mention the Lifecycle Hook interfaces
    // unless we want typing and tool support.
    export class PeekABooComponent extends PeekABoo implements
                  OnChanges, OnInit, DoCheck,
                  AfterContentInit, AfterContentChecked,
                  AfterViewInit, AfterViewChecked,
                  OnDestroy {

      // --------------------------------

      @Input() name: string;
      @Input() hero: Hero;
      @Input() power: string;
      private verb = 'initialized';
      private logger: LoggerService;

      title = 'Tour of Heroes';
      myHero = 'Windstorm';
      // OR
      // title: string;
      // myHero: string;
      // constructor() {
      //   this.title = 'Tour of Heroes';
      //   this.myHero = 'Windstorm';
      // }

      heroes = ['Windstorm', 'Bombasto', 'Magneta', 'Tornado'];
      // OR
      // heroes = [
      //   new Hero(1, 'Windstorm'),
      //   new Hero(13, 'Bombasto'),
      //   new Hero(15, 'Magneta'),
      //   new Hero(20, 'Tornado')
      // ];
      // myHero = this.heroes[0];

      constructor(logger: LoggerService) {
        super(logger);
        let is = this.name ? 'is' : 'is not';
        this.logIt(`name ${is} known at construction`);
      }
      // only called for/if there is an @input variable set by parent.
      ngOnChanges(changes: SimpleChanges) {
        let changesMsgs: string[] = [];
        for (let propName in changes) {
          if (propName === 'name') {
            let name = changes['name'].currentValue;
            changesMsgs.push(`name ${this.verb} to "${name}"`);
          } else {
            changesMsgs.push(propName + ' ' + this.verb);
          }
          // let chng = changes[propName];
          // let cur  = JSON.stringify(chng.currentValue);
          // let prev = JSON.stringify(chng.previousValue);
          // this.changeLog.push(`${propName}: current = ${cur}, prev = ${prev}`);
        }
        this.logIt(`OnChanges: ${changesMsgs.join('; ')}`);
        this.verb = 'changed'; // next time it will be a change
      }
      oldPower = '';
      ngDoCheck() { // Called frequently!
        this.changeDetected = false;
        if (this.power !== this.oldPower) {
          this.changeDetected = true;
          this.changeLog.push(`to "${this.power}" from "${this.oldPower}"`);
          this.oldPower = this.power;
        }
      }

      // following are called frequently!
      /*
        @Component({
          selector: 'app-child-view',
          template: '<input [(ngModel)]="hero">'
        })
        export class ChildViewComponent {
          hero = 'Magneta';
        }
      */
      @ContentChild(ChildViewComponent) ontentChild: ChildViewComponent;
      ngAfterContentChecked() {
        if (this.prevHero === this.contentChild.hero) {
          this.logIt('AfterContentChecked (no change)');
        } else {
          this.prevHero = this.contentChild.hero;
          this.logIt('AfterContentChecked');
          this.doSomething();
        }
      }
      ngAfterContentInit() {
        this.logIt('AfterContentInit');
        this.doSomething();
      }
      // hooks that Angular calls after it creates a component child views
      // Query for a VIEW child of type
      @ViewChild(ChildViewComponent) viewChild: ChildViewComponent;
      ngAfterViewChecked() {
        // viewChild is updated after the view has been checked
        if (this.prevHero === this.viewChild.hero) {
          this.logIt('AfterViewChecked (no change)');
        } else {
          this.prevHero = this.viewChild.hero;
          this.logIt('AfterViewChecked');
          this.doSomething();
        }
      }
      ngAfterViewInit() {
        // viewChild is set after the view has been initialized
        this.logIt('AfterViewInit');
        this.doSomething();
      }
      // surrogate for real business logic sets the `comment`
      private doSomething() {
        let c = this.viewChild.hero.length > 10 ? `That's a long name` : '';
        if (c !== this.comment) {
          // Wait a tick (!) because the component view has already been checked
          this.logger.tick_then(() => this.comment = c);
        }
        // logger tick functions, schedules a view refresh to ensure display catches up
        // tick() {  this.tick_then(() => { }); }
        // tick_then(fn: () => any) { setTimeout(fn, 0); }
        // ---
        this.comment = this.contentChild.hero.length > 10 ? `That's a long name` : '';
      }
      reset() {
        this.logger.clear();
        // quickly remove/reload AfterView/ContentComponent which recreates it
        this.show = false;
        this.logger.tick_then(() => this.show = true);
      }

      ngOnDestroy() { this.logIt(`OnDestroy`); }

      // --------------------------------
      hasChild = false;
      hookLog: string[];
      heroName = 'Windstorm';
      private logger: LoggerService;
      constructor(logger: LoggerService) {
        this.logger = logger;
        this.hookLog = logger.logs;
      }
      toggleChild() {
        this.hasChild = !this.hasChild;
        if (this.hasChild) {
          this.heroName = 'Windstorm';
          this.logger.clear(); // clear log on create
        }
        this.hookLog = this.logger.logs;
        this.logger.tick();
      }
      updateHero() {
        this.heroName += '!';
        this.logger.tick();
      }
      // --------------------------------
    }
  

setting component inputs


    @Component({
      selector: 'app-bank-account',
      inputs: ['bankName', 'id: account-id'],
      template: `
        Bank Name: {{ bankName }}
        Account Id: {{ id }}`
    })
    export class BankAccountComponent {
      bankName: string|null = null;
      id: string|null = null;
      // this property is not bound, and wont be automatically updated by Angular
      normalizedBankName: string|null = null;
    }

    @Component({
      selector: 'app-my-input',
      template: `
        <app-bank-account
          bankName="RBC"
          account-id="4747">
        </app-bank-account>`
    })
    export class MyInputComponent { }
  

setting component outputs


    // two event emitters that emit on an interval
    // emit an output every second, while the other emits every five seconds
    @Directive({
      selector: 'app-interval-dir',
      outputs: ['everySecond', 'fiveSecs: everyFiveSeconds']
    })
    export class IntervalDirComponent {
      everySecond = new EventEmitter<string>();
      fiveSecs = new EventEmitter<string>();
      constructor() {
        setInterval(() => this.everySecond.emit('event'), 1000);
        setInterval(() => this.fiveSecs.emit('event'), 5000);
      }
    }

    @Component({
      selector: 'app-my-output',
      template: `
        <app-interval-dir
          (everySecond)="onEverySecond()"
          (everyFiveSeconds)="onEveryFiveSeconds()">
        </app-interval-dir>`
    })
    export class MyOutputComponent {
      onEverySecond() { console.log('second'); }
      onEveryFiveSeconds() { console.log('five seconds'); }
    }
  

injecting a class with a view provider


    class Greeter {
      greet(name:string) { return 'Hello ' + name + '!'; }
     }

     @Directive({
       selector: 'needs-greeter'
     })
     class NeedsGreeter {
       greeter:Greeter;
       constructor(greeter:Greeter) { this.greeter = greeter; }
     }

     @Component({
       selector: 'greet',
       viewProviders: [ Greeter ],
       template: `<needs-greeter></needs-greeter>`
     })
     class HelloWorld { }
  

navigate the component tree with DI


    export abstract class Parent { name: string; }
    const DifferentParent = Parent;
    // Helper method to provide the current component instance in the name of a "parentType"
    // The "parentType" defaults to "Parent" when omitting the second parameter
    export function provideParent (component: any, parentType?: any) {
      return {
        provide: parentType || Parent,
        useExisting: forwardRef(() => component)
      };
    }

    @Component({
      selector: 'alice',
      template: `
        <div class="a">
          <h3>{{name}}</h3>
          <barry></barry>
          <beth></beth>
          <bob></bob>
          <carol></carol>
        </div> `,
      providers:  [ provideParent(AliceComponent) ]
    })
    export class AliceComponent implements Parent {
      name = 'Alice';
    }

    const templateB = `
    <div class="b">
      <div>
        <h3>{{name}}</h3>
        <p>My parent is {{parent?.name}}</p>
      </div>
      <carol></carol>
      <chris></chris>
    </div>`;
    @Component({
      selector:   'barry',
      template:   templateB,
      providers:  [{
        // token
        provide: Parent,
        // break circular reference by having refer to itself
        useExisting: forwardRef(() => BarryComponent)
      }]
    })
    export class BarryComponent implements Parent {
      name = 'Barry';
      constructor( @SkipSelf() @Optional() public parent: Parent ) { }
    }
    @Component({
      selector:   'bob',
      template:   templateB,
      providers:  [ provideParent(BobComponent) ]
    })
    export class BobComponent implements Parent {
      name = 'Bob';
      constructor( @SkipSelf() @Optional() public parent: Parent ) { }
    }
    @Component({
      selector:   'beth',
      template:   templateB,
      providers:  [ provideParent(BethComponent, DifferentParent) ]
    })
    export class BethComponent implements Parent {
      name = 'Beth';
      constructor( @SkipSelf() @Optional() public parent: Parent ) { }
    }

    // Alice
    // - Barry, My parent is Alice
    // - - Carol, My parent is Barry
    // - - Chris, My parent is Barry
    // - Beth, My parent is Alice
    // - - Carol, My parent is Beth
    // - - Chris, My parent is Beth
    // - Bob, My parent is Alice
    // - - Carol, My parent is Bob
    // - - Chris, My parent is Bob
    // - Carol, My parent is Alice
  


inject into a derived class


    @Component({
      selector: 'app-unsorted-heroes',
      template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
      providers: [HeroService]
    })
    export class HeroesBaseComponent implements OnInit {
      constructor(private heroService: HeroService) { }
      heroes: Array<Hero>;
      ngOnInit() {
        this.heroes = this.heroService.getAllHeroes();
        this.afterGetHeroes();
      }
      // Post-process heroes in derived class override.
      protected afterGetHeroes() {}
    }
    //--------------------------------
    @Component({
      selector: 'app-sorted-heroes',
      template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
      providers: [HeroService]
    })
    export class SortedHeroesComponent extends HeroesBaseComponent {
      constructor(heroService: HeroService) {
        super(heroService);
      }
      protected afterGetHeroes() {
        this.heroes = this.heroes.sort((h1, h2) => {
          return h1.name < h2.name ? -1 :
                (h1.name > h2.name ? 1 : 0);
        });
      }
    }
  

custom directive


    import { Directive, OnInit, OnDestroy } from '@angular/core';
    import { LoggerService } from './logger.service';
    let nextId = 1;
    // Spy on any element to which it is applied, usage:
    // <div mySpy>...</div>
    // <div *ngFor="let hero of heroes" mySpy class="heroes"></div>
    @Directive({
      // supported selectors include: element, [attribute], .class, and :not()
      selector: '[mySpy]'
    })
    export class SpyDirective implements OnInit, OnDestroy {
      constructor(private logger: LoggerService) { }
      ngOnInit()    { this.logIt(`onInit`); }
      ngOnDestroy() { this.logIt(`onDestroy`); }
      private logIt(msg: string) {
        this.logger.log(`Spy #${nextId++} ${msg}`);
      }
    }
  

standalone components


    // --- PhotoGalleryComponent can directly import another standalone component ImageGridComponent
    @Component({
      standalone: true,
      selector: 'photo-gallery',
      imports: [ImageGridComponent], // used to reference standalone directives and pipes
      template: `
        ... <image-grid [images]="imageList"></image-grid>
      `,
    })
    export class PhotoGalleryComponent {
      // component logic
    }

    // --- using existing NgModules in a standalone component:
    // not marked as standalone, but instead declared and exported by an existing NgModule
    // import the NgModule directly into the standalone component
    @Component({
      standalone: true,
      selector: 'photo-gallery',
      // an existing module is imported directly into a standalone component
      imports: [MatButtonModule],
      template: `
        ... <button mat-button>Next Page</button>
      `,
    })
    export class PhotoGalleryComponent {
      // logic
    }

    // --- using standalone components in NgModule-based applications
    // imported into existing NgModules-based contexts
    @NgModule({
      declarations: [AlbumComponent],
      exports: [AlbumComponent],
      imports: [PhotoGalleryComponent],
    })
    export class AlbumModule {}

    // --- bootstrapping an application using a standalone component
    // bootstrap Angular app without any NgModule by using a standalone component as the app root component.
    // in the main.ts file
    import {bootstrapApplication} from '@angular/platform-browser';
    import {PhotoAppComponent} from './app/photo.app.component';
    bootstrapApplication(PhotoAppComponent);
    // configuring dependency injection
    // is based on explicitly configuring a list of Providers for dependency injection
    // "provide"-prefixed functions can be used to configure different systems without needing to import NgModules
    bootstrapApplication(PhotoAppComponent, {
      providers: [
        {provide: BACKEND_URL, useValue: 'https://photoapp.looknongmodules.com/api'},
        provideRouter([ /* app routes */ ]), // in place of RouterModule.forRoot, to configure the router
        // ...
      ]
    });
    // existing libraries may rely on NgModules for configuring DI
    // Angular router uses the RouterModule.forRoot() helper to set up routing in an application
    // use existing NgModules in bootstrapApplication via the importProvidersFrom utility
    bootstrapApplication(PhotoAppComponent, {
      providers: [
        {provide: BACKEND_URL, useValue: 'https://photoapp.looknongmodules.com/api'},
        importProvidersFrom(
          RouterModule.forRoot([/* app routes */]),
        ),
        // ...
      ]
    });
  

directive composition API, hostDirectives


    // instantiated: MenuBehavior - AdminMenu
    // receives inputs (ngOnInit): MenuBehavior - AdminMenu
    // applies host bindings: MenuBehavior - AdminMenu
    @Component({
      selector: 'admin-menu',
      template: 'admin-menu.html',
      hostDirectives: [MenuBehavior],
    })
    export class AdminMenu { }
    @Component({
      selector: 'mat-menu',
      hostDirectives: [
        HasColor, // reuses all the inputs, outputs, and associated logic
        {
          directive: CdkMenu, // reuse only the logic and the selected inputs
          inputs: ['cdkMenuDisabled: disabled'],
          outputs: ['cdkMenuClosed: closed']
        }
      ]
    })
    class MatMenu {}
    // order of operations extends to nested chains of host directives:
    // instantiated: Tooltip - CustomTooltip - EvenMoreCustomTooltip
    // receives inputs: Tooltip - CustomTooltip - EvenMoreCustomTooltip
    // applies host bindings: Tooltip - CustomTooltip - EvenMoreCustomTooltip
    @Directive({...})
    export class Tooltip { }
    @Directive({
      hostDirectives: [Tooltip],
    })
    export class CustomTooltip { }
    @Directive({
      hostDirectives: [CustomTooltip],
    })
    export class EvenMoreCustomTooltip { }
  

class field decorators for directives and components

@HostBinding


    // marks a DOM property as a host-binding property
    // and supplies configuration metadata.
    // create a directive that sets the valid and invalid properties
    //on the DOM element that has an ngModel directive on it:

    @Directive({selector: '[ngModel]'})
    class NgModelStatus {
      constructor(public control: NgModel) {}
      @HostBinding('class.valid') get valid() { return this.control.valid; }
      @HostBinding('class.invalid') get invalid() { return this.control.invalid; }
    }

    @Component({
      selector: 'app',
      template: `<input [(ngModel)]="prop">`,
    })
    class App {
      prop;
    }
  

@HostListener


    // declares a DOM event to listen for,
    // and provides a handler method to run when that event occurs
    // - eventName - CSS event to listen for
    // - args - set of arguments to pass to the handler method when the event occurs
    // attach a click listener to a button and counts clicks:
    @Directive({selector: 'button[counting]'})
    class CountClicks {
      numberOfClicks = 0;
      @HostListener('click', ['$event.target'])
      onClick(btn) {
        console.log('button', btn, 'number of clicks:', this.numberOfClicks++);
     }
    }
    @Component({
      selector: 'app',
      template: '<button counting>Increment</button>'
    })
    class App {}
  

@ViewChildren


    // get the QueryList of elements or directives from the view DOM
    // any time a child element is added/removed/moved, the query list will be updated,
    // and the changes observable of the query list will emit a new value
    // view queries are set before the ngAfterViewInit callback is called
    // metadata properties:
    // selector - the directive type or the name used for querying
    // read - read a different token from the queried elements
    // --- ---
    import {
      AfterViewInit, Component, Directive, QueryList, ViewChildren
    } from '@angular/core';
    @Directive({selector: 'child-directive'})
    class ChildDirective { }
    @Component({selector: 'someCmp', templateUrl: 'someCmp.html'})
    class SomeCmp implements AfterViewInit {
      // TODO(issue/24571): remove '!'.
      @ViewChildren(ChildDirective) viewChildren !: QueryList<ChildDirective>;
      ngAfterViewInit() {
        // viewChildren is set
      }
    }
    // --- ---
    import {
      AfterViewInit, Component, Directive, Input, QueryList, ViewChildren
    } from '@angular/core';
    @Directive({selector: 'pane'})
    export class Pane {
      // TODO(issue/24571): remove '!'.
      @Input() id !: string;
    }
    @Component({
      selector: 'example-app',
      template: `
        <pane id="1"></pane>
        <pane id="2"></pane>
        <pane id="3" *ngIf="shouldShow"></pane>
        <button (click)="show()">Show 3</button>
        <div>panes: {{serializedPanes}}</div>`,
    })
    export class ViewChildrenComp implements AfterViewInit {
      // TODO(issue/24571): remove '!'.
      @ViewChildren(Pane) panes !: QueryList<Pane>;
      serializedPanes: string = '';
      shouldShow = false;
      show() { this.shouldShow = true; }
      ngAfterViewInit() {
        this.calculateSerializedPanes();
        this.panes.changes.subscribe((r) => { this.calculateSerializedPanes(); });
      }
      calculateSerializedPanes() {
        setTimeout(() => { this.serializedPanes = this.panes.map(p => p.id).join(', '); }, 0);
      }
    }
  

@ViewChild


    // looks for the first element or the directive matching the selector in the view DOM
    // updated on changes
    // view queries are set before the ngAfterViewInit callback is called
    // metadata properties:
    // selector - the directive type or the name used for querying
    // read - read a different token from the queried elements
    // static - whether or not to resolve query results before change detection runs
    //   default is false (i.e. return static results only)
    //   uses query results to determine the timing of query resolution
    //   if any query results are inside a nested view (e.g. *ngIf),
    //   the query will be resolved after change detection runs
    //   otherwise, it will be resolved before change detection runs
    //
    // supported selectors include:
    // - any class with the @Component or @Directive decorator
    // - template reference variable as a string
    // (e.g. query <my-component #cmp></my-component> with @ViewChild('cmp'))
    // - any provider defined in the child component tree of the current component
    // (e.g. @ViewChild(SomeService) someService: SomeService)
    // - any provider defined through a string token
    // (e.g. @ViewChild('someToken') someTokenVal: any)
    // - TemplateRef (e.g. query <ng-template></ng-template> with @ViewChild(TemplateRef) template;)
    // --- ---
    import {Component, Directive, Input, ViewChild} from '@angular/core';
    @Directive({selector: 'pane'})
    export class Pane {
      // TODO(issue/24571): remove '!'.
      @Input() id !: string;
    }
    @Component({
      selector: 'example-app',
      template: `
        <pane id="1" *ngIf="shouldShow"></pane>
        <pane id="2" *ngIf="!shouldShow"></pane>
        <button (click)="toggle()">Toggle</button>
        <div>Selected: {{selectedPane}}</div>
      `,
    })
    export class ViewChildComp {
      @ViewChild(Pane, {static: false})
      set pane(v: Pane) {
        setTimeout(() => { this.selectedPane = v.id; }, 0);
      }
      selectedPane: string = '';
      shouldShow = true;
      toggle() { this.shouldShow = !this.shouldShow; }
    }
    // --- ---
    import {AfterViewInit, Component, Directive, ViewChild} from '@angular/core';
    @Directive({selector: 'child-directive'})
    class ChildDirective { }
    @Component({selector: 'someCmp', templateUrl: 'someCmp.html'})
    class SomeCmp implements AfterViewInit {
      // TODO(issue/24571): remove '!'.
      @ViewChild(ChildDirective, {static: false}) child !: ChildDirective;
      ngAfterViewInit() {
        // child is set
      }
    }
    // --- ---
    import {Component, Directive, Input, ViewChild} from '@angular/core';
    @Directive({selector: 'pane'})
    export class Pane {
      // TODO(issue/24571): remove '!'.
      @Input() id !: string;
    }
    @Component({
      selector: 'example-app',
      template: `
        <pane id="1" *ngIf="shouldShow"></pane>
        <pane id="2" *ngIf="!shouldShow"></pane>
        <button (click)="toggle()">Toggle</button>
        <div>Selected: {{selectedPane}}</div>
      `,
    })
    export class ViewChildComp {
      @ViewChild(Pane, {static: false})
      set pane(v: Pane) {
        setTimeout(() => { this.selectedPane = v.id; }, 0);
      }
      selectedPane: string = '';
      shouldShow = true;
      toggle() { this.shouldShow = !this.shouldShow; }
    }
  

@ContentChildren


    // get the QueryList of elements/directives from the view DOM, updated on changes
    // content queries are set before the ngAfterContentInit callback is called
    // metadata properties:
    // selector - the directive type or the name used for querying
    // descendants - include only direct children or all descendants
    // read - read a different token from the queried elements
    // --- ---
    import {AfterContentInit, ContentChildren, Directive, QueryList} from '@angular/core';
    @Directive({selector: 'child-directive'})
    class ChildDirective { }
    @Directive({selector: 'someDir'})
    class SomeDir implements AfterContentInit {
      // TODO(issue/24571): remove '!'.
      @ContentChildren(ChildDirective) contentChildren !: QueryList<ChildDirective>;
      ngAfterContentInit() {
        // contentChildren is set
      }
    }
    // --- --- implement a tab pane component
    import {Component, ContentChildren, Directive, Input, QueryList} from '@angular/core';
    @Directive({selector: 'pane'})
    export class Pane {
      // TODO(issue/24571): remove '!'.
      @Input() id !: string;
    }
    @Component({
      selector: 'tab',
      template: `
        <div class="top-level">Top level panes: {{serializedPanes}}</div>
        <div class="nested">Arbitrary nested panes: {{serializedNestedPanes}}</div>`
    })
    export class Tab {
      @ContentChildren(Pane) topLevelPanes !: QueryList<Pane>;
      @ContentChildren(Pane, {descendants: true}) arbitraryNestedPanes !: QueryList<Pane>;

      get serializedPanes(): string {
        return this.topLevelPanes ? this.topLevelPanes.map(p => p.id).join(', ') : '';
      }
      get serializedNestedPanes(): string {
        return this.arbitraryNestedPanes ?
          this.arbitraryNestedPanes.map(p => p.id).join(', ') : '';
      }
    }
    @Component({
      selector: 'example-app',
      template: `
        <tab>
          <pane id="1"></pane>
          <pane id="2"></pane>
          <pane id="3" *ngIf="shouldShow">
            <tab>
              <pane id="3_1"></pane>
              <pane id="3_2"></pane>
            </tab>
          </pane>
        </tab>
        <button (click)="show()">Show 3</button>`,
    })
    export class ContentChildrenComp {
      shouldShow = false;
      show() { this.shouldShow = true; }
    }
  

@ContentChild


    // first element/directive matching the selector, updated on changes
    // metadata properties:
    // selector - the directive type or the name used for querying
    // read - read a different token from the queried elements
    // --- ---
    import {AfterContentInit, ContentChild, Directive} from '@angular/core';
    @Directive({selector: 'child-directive'})
    class ChildDirective { }
    @Directive({selector: 'someDir'})
    class SomeDir implements AfterContentInit {
      @ContentChild(ChildDirective) contentChild !: ChildDirective;
      ngAfterContentInit() {
        // contentChild is set
      }
    }
    // --- ---
    import {Component, ContentChild, Directive, Input} from '@angular/core';
    @Directive({selector: 'pane'})
    export class Pane {
      // TODO(issue/24571): remove '!'.
      @Input() id !: string;
    }
    @Component({
      selector: 'tab',
      template: `
        <div>pane: {{pane?.id}}</div>`
    })
    export class Tab {
      // TODO(issue/24571): remove '!'.
      @ContentChild(Pane) pane !: Pane;
    }
    @Component({
      selector: 'example-app',
      template: `
        <tab>
          <pane id="1" *ngIf="shouldShow"></pane>
          <pane id="2" *ngIf="!shouldShow"></pane>
        </tab>
        <button (click)="toggle()">Toggle</button>`
    })
    export class ContentChildComp {
      shouldShow = true;
      toggle() { this.shouldShow = !this.shouldShow; }
    }
  

Interaction + tests

pass data from parent to child with input binding

    @Component({
      selector: 'app-hero-child',
      template: `
        <h3>{{hero.name}} says:</h3>
        <p>I, {{hero.name}}, am at your service, {{masterName}}.</p>
      `
    })
    export class HeroChildComponent {
      @Input() hero: Hero;
      @Input('master') masterName: string;
    }
    // -----------------------------
    @Component({
      selector: 'app-hero-parent',
      template: `
        <h2>{{master}} controls {{heroes.length}} heroes</h2>
        <app-hero-child *ngFor="let hero of heroes"
          [hero]="hero"
          [master]="master">
        </app-hero-child>
      `
    })
    export class HeroParentComponent {
      heroes = HEROES;
      master = 'Master';
    }
    //--------------------------------
    // ...
    let _heroNames = ['Mr. IQ', 'Magneta', 'Bombasto'];
    let _masterName = 'Master';

    it('should pass properties to children properly', function () {
      let parent = element.all(by.tagName('app-hero-parent')).get(0);
      let heroes = parent.all(by.tagName('app-hero-child'));

      for (let i = 0; i < _heroNames.length; i++) {
        let childTitle = heroes.get(i).element(by.tagName('h3')).getText();
        let childDetail = heroes.get(i).element(by.tagName('p')).getText();
        expect(childTitle).toEqual(_heroNames[i] + ' says:');
        expect(childDetail).toContain(_masterName);
      }
    });
    // ...
  
intercept input property changes with a setter

    @Component({
      selector: 'app-name-child',
      template: '<h3>"{{name}}"</h3>'
    })
    export class NameChildComponent {
      private _name = '';
      @Input()
      set name(name: string) {
        this._name = (name && name.trim()) || '<no name set>';
      }
      get name(): string { return this._name; }
    }
    //--------------------------------
    @Component({
      selector: 'app-name-parent',
      template: `
      <h2>Master controls {{names.length}} names</h2>
      <app-name-child *ngFor="let name of names" [name]="name"></app-name-child>
      `
    })
    export class NameParentComponent {
      // Displays 'Mr. IQ', '<no name set>', 'Bombasto'
      names = ['Mr. IQ', '   ', '  Bombasto  '];
    }
    //--------------------------------
    // ...
    it('should display trimmed, non-empty names', function () {
      let _nonEmptyNameIndex = 0;
      let _nonEmptyName = '"Mr. IQ"';
      let parent = element.all(by.tagName('app-name-parent')).get(0);
      let hero = parent.all(by.tagName('app-name-child')).get(_nonEmptyNameIndex);

      let displayName = hero.element(by.tagName('h3')).getText();
      expect(displayName).toEqual(_nonEmptyName);
    });
    it('should replace empty name with default name', function () {
      let _emptyNameIndex = 1;
      let _defaultName = '"<no name set>"';
      let parent = element.all(by.tagName('app-name-parent')).get(0);
      let hero = parent.all(by.tagName('app-name-child')).get(_emptyNameIndex);

      let displayName = hero.element(by.tagName('h3')).getText();
      expect(displayName).toEqual(_defaultName);
    });
    // ...
  
intercept input property changes with ngOnChanges()

    //--------------------------------
    // prefer this approach to the property setter
    // when watching multiple, interacting input properties
    //--------------------------------
    import { Component, Input, OnChanges, SimpleChange } from '@angular/core';
    @Component({
      selector: 'app-version-child',
      template: `
        <h3>Version {{major}}.{{minor}}</h3>
        <h4>Change log:</h4>
        <ul>
          <li *ngFor="let change of changeLog">{{change}}</li>
        </ul>
      `
    })
    export class VersionChildComponent implements OnChanges {
      @Input() major: number;
      @Input() minor: number;
      changeLog: string[] = [];
      ngOnChanges(changes: {[propKey: string]: SimpleChange}) {
        let log: string[] = [];
        for (let propName in changes) {
          let changedProp = changes[propName];
          let to = JSON.stringify(changedProp.currentValue);
          if (changedProp.isFirstChange()) {
            log.push(`Initial value of ${propName} set to ${to}`);
          } else {
            let from = JSON.stringify(changedProp.previousValue);
            log.push(`${propName} changed from ${from} to ${to}`);
          }
        }
        this.changeLog.push(log.join(', '));
      }
    }
    //--------------------------------
    import { Component } from '@angular/core';
    @Component({
      selector: 'app-version-parent',
      template: `
        <h2>Source code version</h2>
        <button (click)="newMinor()">New minor version</button>
        <button (click)="newMajor()">New major version</button>
        <app-version-child [major]="major" [minor]="minor"></app-version-child>
      `
    })
    export class VersionParentComponent {
      major = 1;
      minor = 23;
      newMinor() { this.minor++; }
      newMajor() { this.major++; this.minor = 0; }
    }
    //--------------------------------
    // ...
    // Test must all execute in this exact order
    it('should set expected initial values', function () {
      let actual = getActual();
      let initialLabel = 'Version 1.23';
      let initialLog = 'Initial value of major set to 1, Initial value of minor set to 23';
      expect(actual.label).toBe(initialLabel);
      expect(actual.count).toBe(1);
      expect(actual.logs.get(0).getText()).toBe(initialLog);
    });
    it('should set expected values after clicking \'Minor\' twice', function () {
      let repoTag = element(by.tagName('app-version-parent'));
      let newMinorButton = repoTag.all(by.tagName('button')).get(0);
      newMinorButton.click().then(function() {
        newMinorButton.click().then(function() {
          let actual = getActual();
          let labelAfter2Minor = 'Version 1.25';
          let logAfter2Minor = 'minor changed from 24 to 25';
          expect(actual.label).toBe(labelAfter2Minor);
          expect(actual.count).toBe(3);
          expect(actual.logs.get(2).getText()).toBe(logAfter2Minor);
        });
      });
    });
    it('should set expected values after clicking \'Major\' once', function () {
      let repoTag = element(by.tagName('app-version-parent'));
      let newMajorButton = repoTag.all(by.tagName('button')).get(1);
      newMajorButton.click().then(function() {
        let actual = getActual();
        let labelAfterMajor = 'Version 2.0';
        let logAfterMajor = 'major changed from 1 to 2, minor changed from 25 to 0';
        expect(actual.label).toBe(labelAfterMajor);
        expect(actual.count).toBe(4);
        expect(actual.logs.get(3).getText()).toBe(logAfterMajor);
      });
    });
    function getActual() {
      let versionTag = element(by.tagName('app-version-child'));
      let label = versionTag.element(by.tagName('h3')).getText();
      let ul = versionTag.element((by.tagName('ul')));
      let logs = ul.all(by.tagName('li'));
      return {
        label: label,
        logs: logs,
        count: logs.count()
      };
    }
    // ...
  
parent listens for child event

    import { Component, EventEmitter, Input, Output } from '@angular/core';
    @Component({
      selector: 'app-voter',
      template: `
        <h4>{{name}}</h4>
        <button (click)="vote(true)"  [disabled]="didVote">Agree</button>
        <button (click)="vote(false)" [disabled]="didVote">Disagree</button>
      `
    })
    export class VoterComponent {
      @Input()  name: string;
      @Output() voted = new EventEmitter<boolean>();
      didVote = false;
      vote(agreed: boolean) {
        this.voted.emit(agreed);
        this.didVote = true;
      }
    }
    //--------------------------------
    import { Component }      from '@angular/core';
    @Component({
      selector: 'app-vote-taker',
      template: `
        <h2>Should mankind colonize the Universe?</h2>
        <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3>
        <app-voter *ngFor="let voter of voters"
          [name]="voter"
          (voted)="onVoted($event)">
        </app-voter>
      `
    })
    export class VoteTakerComponent {
      agreed = 0;
      disagreed = 0;
      voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto'];
      onVoted(agreed: boolean) {
        agreed ? this.agreed++ : this.disagreed++;
      }
    }
    //--------------------------------
    // ...
    it('should not emit the event initially', function () {
      let voteLabel = element(by.tagName('app-vote-taker'))
        .element(by.tagName('h3')).getText();
      expect(voteLabel).toBe('Agree: 0, Disagree: 0');
    });
    it('should process Agree vote', function () {
      let agreeButton1 = element.all(by.tagName('app-voter')).get(0)
        .all(by.tagName('button')).get(0);
      agreeButton1.click().then(function() {
        let voteLabel = element(by.tagName('app-vote-taker'))
          .element(by.tagName('h3')).getText();
        expect(voteLabel).toBe('Agree: 1, Disagree: 0');
      });
    });
    it('should process Disagree vote', function () {
      let agreeButton1 = element.all(by.tagName('app-voter')).get(1)
        .all(by.tagName('button')).get(1);
      agreeButton1.click().then(function() {
        let voteLabel = element(by.tagName('app-vote-taker'))
          .element(by.tagName('h3')).getText();
        expect(voteLabel).toBe('Agree: 1, Disagree: 1');
      });
    });
    // ...
  
parent interacts with child via local variable

    //--------------------------------
    // read child properties or invoke child methods
    // by creating a template reference variable
    // for the child element and then reference that variable within the parent template
    //
    // parent component cannot data bind to the child start and stop methods
    // nor to its seconds property
    //
    // wire parent buttons to the child start and stop
    // and use interpolation to display the child seconds property
    //--------------------------------
    import { Component, OnDestroy, OnInit } from '@angular/core';
    @Component({
      selector: 'app-countdown-timer',
      template: '<p>{{message}}</p>'
    })
    export class CountdownTimerComponent implements OnInit, OnDestroy {
      intervalId = 0;
      message = '';
      seconds = 11;
      clearTimer() { clearInterval(this.intervalId); }
      ngOnInit()    { this.start(); }
      ngOnDestroy() { this.clearTimer(); }
      start() { this.countDown(); }
      stop()  {
        this.clearTimer();
        this.message = `Holding at T-${this.seconds} seconds`;
      }
      private countDown() {
        this.clearTimer();
        this.intervalId = window.setInterval(() => {
          this.seconds -= 1;
          if (this.seconds === 0) {
            this.message = 'Blast off!';
          } else {
            if (this.seconds < 0) { this.seconds = 10; } // reset
            this.message = `T-${this.seconds} seconds and counting`;
          }
        }, 1000);
      }
    }
    //--------------------------------
    import { Component }                from '@angular/core';
    import { CountdownTimerComponent }  from './countdown-timer.component';
    @Component({
      selector: 'app-countdown-parent-lv',
      template: `
      <h3>Countdown to Liftoff (via local variable)</h3>
      <button (click)="timer.start()">Start</button>
      <button (click)="timer.stop()">Stop</button>
      <div class="seconds">{{timer.seconds}}</div>
      <app-countdown-timer #timer></app-countdown-timer>
      `,
      styleUrls: ['../assets/demo.css']
    })
    export class CountdownLocalVarParentComponent { }
    //--------------------------------
    // ...
    it('timer and parent seconds should match', function () {
      let parent = element(by.tagName(parentTag));
      let message = parent.element(by.tagName('app-countdown-timer')).getText();
      browser.sleep(10); // give `seconds` a chance to catchup with `message`
      let seconds = parent.element(by.className('seconds')).getText();
      expect(message).toContain(seconds);
    });
    it('should stop the countdown', function () {
      let parent = element(by.tagName(parentTag));
      let stopButton = parent.all(by.tagName('button')).get(1);
      stopButton.click().then(function() {
        let message = parent.element(by.tagName('app-countdown-timer')).getText();
        expect(message).toContain('Holding');
      });
    });
    // ...
  
parent calls an @ViewChild()

    // if an instance of the parent component class must
    // read or write child component values or must call child component methods
    // inject the child component into the parent as a ViewChild
    //--------------------------------
    import { AfterViewInit, ViewChild } from '@angular/core';
    import { Component }                from '@angular/core';
    import { CountdownTimerComponent }  from './countdown-timer.component';
    @Component({
      selector: 'app-countdown-parent-vc',
      template: `
      <h3>Countdown to Liftoff (via ViewChild)</h3>
      <button (click)="start()">Start</button>
      <button (click)="stop()">Stop</button>
      <div class="seconds">{{ seconds() }}</div>
      <app-countdown-timer></app-countdown-timer>
      `,
      styleUrls: ['../assets/demo.css']
    })
    export class CountdownViewChildParentComponent implements AfterViewInit {
      @ViewChild(CountdownTimerComponent)
      private timerComponent: CountdownTimerComponent;
      seconds() { return 0; }
      ngAfterViewInit() {
        // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ...
        // but wait a tick first to avoid one-time devMode
        // unidirectional-data-flow-violation error
        setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0);
      }
      start() { this.timerComponent.start(); }
      stop() { this.timerComponent.stop(); }
    }
    //--------------------------------
    // same E2E countdown timer tests as before
  
parent and children communicate via a service

    //--------------------------------
    // share a service whose interface enables bi-directional communication
    //--------------------------------
    import { Injectable } from '@angular/core';
    import { Subject }    from 'rxjs';
    @Injectable()
    export class MissionService {
      // Observable string sources
      private missionAnnouncedSource = new Subject<string>();
      private missionConfirmedSource = new Subject<string>();
      // Observable string streams
      missionAnnounced$ = this.missionAnnouncedSource.asObservable();
      missionConfirmed$ = this.missionConfirmedSource.asObservable();
      // Service message commands
      announceMission(mission: string) {
        this.missionAnnouncedSource.next(mission);
      }
      confirmMission(astronaut: string) {
        this.missionConfirmedSource.next(astronaut);
      }
    }
    //--------------------------------
    import { Component }          from '@angular/core';
    import { MissionService }     from './mission.service';
    @Component({
      selector: 'app-mission-control',
      template: `
        <h2>Mission Control</h2>
        <button (click)="announce()">Announce mission</button>
        <app-astronaut *ngFor="let astronaut of astronauts"
          [astronaut]="astronaut">
        </app-astronaut>
        <h3>History</h3>
        <ul>
          <li *ngFor="let event of history">{{event}}</li>
        </ul>
      `,
      providers: [MissionService]
    })
    export class MissionControlComponent {
      astronauts = ['Lovell', 'Swigert', 'Haise'];
      history: string[] = [];
      missions = ['Fly to the moon!',
                  'Fly to mars!',
                  'Fly to Vegas!'];
      nextMission = 0;
      constructor(private missionService: MissionService) {
        missionService.missionConfirmed$.subscribe(
          astronaut => {
            this.history.push(`${astronaut} confirmed the mission`);
          });
      }
      announce() {
        let mission = this.missions[this.nextMission++];
        this.missionService.announceMission(mission);
        this.history.push(`Mission "${mission}" announced`);
        if (this.nextMission >= this.missions.length) { this.nextMission = 0; }
      }
    }
    //--------------------------------
    import { Component, Input, OnDestroy } from '@angular/core';
    import { MissionService } from './mission.service';
    import { Subscription }   from 'rxjs';
    @Component({
      selector: 'app-astronaut',
      template: `
        <p>
          {{astronaut}}: <strong>{{mission}}</strong>
          <button
            (click)="confirm()"
            [disabled]="!announced || confirmed">
            Confirm
          </button>
        </p>
      `
    })
    export class AstronautComponent implements OnDestroy {
      @Input() astronaut: string;
      mission = '<no mission announced>';
      confirmed = false;
      announced = false;
      subscription: Subscription;
      constructor(private missionService: MissionService) {
        this.subscription = missionService.missionAnnounced$.subscribe(
          mission => {
            this.mission = mission;
            this.announced = true;
            this.confirmed = false;
        });
      }
      confirm() {
        this.confirmed = true;
        this.missionService.confirmMission(this.astronaut);
      }
      ngOnDestroy() {
        // prevent memory leak when component destroyed
        this.subscription.unsubscribe();
      }
    }
    //--------------------------------
    // ...
    it('should announce a mission', function () {
      let missionControl = element(by.tagName('app-mission-control'));
      let announceButton = missionControl.all(by.tagName('button')).get(0);
      announceButton.click().then(function () {
        let history = missionControl.all(by.tagName('li'));
        expect(history.count()).toBe(1);
        expect(history.get(0).getText()).toMatch(/Mission.* announced/);
      });
    });
    it('should confirm the mission by Lovell', function () {
      testConfirmMission(1, 2, 'Lovell');
    });
    it('should confirm the mission by Haise', function () {
      testConfirmMission(3, 3, 'Haise');
    });
    it('should confirm the mission by Swigert', function () {
      testConfirmMission(2, 4, 'Swigert');
    });
    function testConfirmMission(buttonIndex: number, expectedLogCount: number, astronaut: string) {
      let _confirmedLog = ' confirmed the mission';
      let missionControl = element(by.tagName('app-mission-control'));
      let confirmButton = missionControl.all(by.tagName('button')).get(buttonIndex);
      confirmButton.click().then(function () {
        let history = missionControl.all(by.tagName('li'));
        expect(history.count()).toBe(expectedLogCount);
        expect(history.get(expectedLogCount - 1).getText()).toBe(astronaut + _confirmedLog);
      });
    }
    // ...
  

Template

binding syntax

Data direction Syntax Type
One-way
from data source
to view target
{{expression}}
[target]="expression"
bind-target="expression"
Interpolation
Property
Attribute
Class
Style
One-way
from view target
to data source
(target)="statement"
on-target="statement"
Event
Two-way [(target)]="expression"
bindon-target="expression"
Two-way

binding target

Type Target Examples
Property Element | Component | Directive property
    <img [src]="heroImageUrl">
<button [disabled]="isUnchanged">Cancel</button>
<app-hero-detail [hero]="currentHero">
... </app-hero-detail>
<div [ngClass]="{'special': isSpecial}">
... </div>
Event Element | Component | Directive event
    <button (click)="onSave()">Save</button>
<app-hero-detail (deleteRequest)="deleteHero()">
... </app-hero-detail>
<div (myClick)="clicked=$event" clickable>click me</div>
Two-way Event and property
<input [(ngModel)]="name">
Attribute Attribute (the exception)
<button [attr.aria-label]="help">help</button>
Class class property
<div [class.special]="isSpecial">Special</div>
Style style property
<button [style.color]="isSpecial ? 'red' : 'green'">

ATTRIBUTE binding - the only binding that creates and sets an attribute


    <td colspan="{{1 + 1}}">Three-Four</td> <!-- WRONG for interpolation -->
    <td [attr.colspan]="1 + 1">One-Two</td>
    <button [attr.aria-label]="actionName">{{actionName}} with Aria</button>
  

CLASS binding - add or remove a single class, NgClass is usually preferred


    <!-- reset/override all class names with a binding  -->
    <div [class]="badCurly">Bad curly</div>
    <!-- toggle -->
    <div [class.special]="isSpecial">binding is special</div>
    <div class="special" [class.special]="!isSpecial">not so special</div>
    NgClass
    <div [ngClass]="currentClasses">
      div is initially saveable, unchanged, and special
    </div>
    <!--
      currentClasses: {};
      setCurrentClasses() {
        // CSS classes: added/removed per current state of component properties
        this.currentClasses =  {
          'saveable': this.canSave,
          'modified': !this.isUnchanged,
          'special':  this.isSpecial
        };
      }
    -->
  

STYLE binding - set a single style value, NgStyle is generally preferred


    <!-- dash-case OR camelCase -->
    <button [style.color]="isSpecial ? 'red': 'green'">Red</button>
    <button [style.background-color]="canSave ? 'cyan': 'grey'" >Save</button>
    <button [style.font-size.em]="isSpecial ? 3 : 1" >Big</button>
    <button [style.font-size.%]="!isSpecial ? 150 : 50" >Small</button>
    NgStyle
    <div [ngStyle]="currentStyles">
      div is initially italic, normal weight, and extra large (24px)
    </div>
    <!--
      currentStyles: {};
      setCurrentStyles() {
        // CSS styles: set per current state of component properties
        this.currentStyles = {
          'font-style':  this.canSave      ? 'italic' : 'normal',
          'font-weight': !this.isUnchanged ? 'bold'   : 'normal',
          'font-size':   this.isSpecial    ? '24px'   : '12px'
        };
      }
    -->
  

TWO-WAY binding - [(...)], NgModel


    <!--  -->
    <app-sizer [(size)]="fontSizePx"></app-sizer>
    <div [style.font-size.px]="fontSizePx">Resizable Text</div>
    <app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer>
    <!-- NgModel, FormsModule import into component is required to use it-->
    <input [(ngModel)]="currentHero.name">
    <input
        [ngModel]="currentHero.name"
        (ngModelChange)="currentHero.name=$event">
  

TEMPLATE REFERENCE VARIABLES - #var


    <!-- template becomed completely self contained, component does nothing: -->
    <!-- passing value directly -->
    <!-- reference #phone from any sibling or child of element -->
    <!-- user presses Enter + blur event = UX -->
    <input #box
      (keyup.enter)="add_OR_update(box.value)"
      (blur)="add_OR_update(box.value); newHero.value=''">
    <button (click)="add_OR_update(box.value)">Add</button>
    <ul><li *ngFor="let box of boxes">{{box}}</li></ul>

    <!-- -->
    <form (ngSubmit)="onSubmit(heroForm)" #heroForm="ngForm">
      <div class="form-group">
        <label for="name">Name
          <input class="form-control" name="name" required [(ngModel)]="hero.name">
        </label>
      </div>
      <button type="submit" [disabled]="!heroForm.form.valid">Submit</button>
    </form>
    <div [hidden]="!heroForm.form.valid">{{submitMessage}}</div>
    <!-- ref- prefix alternative to # -->
    <input ref-fax placeholder="fax number">
    <button (click)="callFax(fax.value)">Fax</button>
  

EVENT binding - from an element to a component, any DOM event


    <button (click)="onSave()">Save</button>
    <input [value]="currentHero.name"
      (input)="currentHero.name=$event.target.value" > <!-- event object -->
    <input (keyup)="onKey($event)">
    onKey(event: any) { ... < without type info >
    onKey(event: KeyboardEvent) { ... < with type info >

    <!-- custom events with EventEmitter -->
    <app-hero-detail (deleteRequest)="deleteHero($event)" [hero]="currentHero">
    </app-hero-detail>
  

STRUCTURAL DIRECTIVES


    // YOU MAY APPLY ONLY ONE STRUCTURAL DIRECTIVE TO AN ELEMENT !!!
    // use <ng-container> for that cases

    <!-- *ngIf - remove DOM element completely -->
    <app-hero-detail *ngIf="isActive"></app-hero-detail>
    <!-- ... hiding ONLY ! -->
    <div [class.hidden]="!isSpecial">Show with class</div>
    <div [class.hidden]="isSpecial">Hide with class</div>
    <div [style.display]="isSpecial ? 'block' : 'none'">Show with style</div>
    <!-- HeroDetail is in the DOM but hidden -->
    <app-hero-detail [class.hidden]="isSpecial"></app-hero-detail>
    <!-- -->
    <ng-template [ngIf]="hero">
      <div class="name">{{hero.name}}</div>
    </ng-template>

    <div class="course-detail" *ngIf="courseObs | async as course; else loading">
      <div class="course-field">
          {{course.shortDescription}}
      </div>
      ...
    </div>
    <ng-template #loading>
      <div>Loading ...</div>
    </ng-template>

    <div *ngIf="user$ | async; let user">
      <h3> {{user.name}}
    </div>
    <ng-container *ngIf="user$ | async; let user">...</ng-container>
    <div *ngIf="(user$ | async) || {}; let user">
      <h3> {{user?.name}}
    </div>
    <div *ngIf="(primitive$ | async) || ' '; let primitive">
      <h3> {{primitive}}
    </div>

    <!-- *NgFor...of -->
    <div *ngFor="let hero of heroes">{{hero.name}}</div>
    <app-hero-detail
      *ngFor="let hero of heroes; let odd=odd"
      [class.odd]="odd"
      [hero]="hero">
    </app-hero-detail>
    <!--
      track changes only for specific property, avoid rerender
      trackBy(index: number, hero: Hero): number { return hero.id; }
    -->
    <div *ngFor="let hero of heroes; let i=index; trackBy: trackByFn">
      {{i + 1}} - {{hero.name}}
    </div>
    <li *ngFor="let user of userObservable | async as users; index as i; first as isFirst">
      {{i}}/{{users.length}}. {{user}} <span *ngIf="isFirst">default</span>
    </li>
    <!--
      $implicit: T - value of the individual items in the iterable (ngForOf)
      ngForOf: NgIterable<T> - value of the iterable expression,
        when the expression is more complex then a property access,
        for example when using the async pipe (userStreams | async)
      index: number - index of the current item in the iterable
      first: boolean - True when the item is the first item in the iterable
      last: boolean - True when the item is the last item in the iterable
      even: boolean - True when the item has an even index in the iterable
      odd: boolean - True when the item has an odd index in the iterable
    -->
    <div
      *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById"
      [class.odd]="odd">
      ({{i}}) {{hero.name}}
    </div>
    <ng-template
      ngFor let-hero [ngForOf]="heroes"
      let-i="index" let-odd="odd"
      [ngForTrackBy]="trackById">
      <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
    </ng-template>

    <!-- NgSwitch, work as well with native elements and web components too -->
    <div [ngSwitch]="currentHero.emotion">
      <app-happy-hero    *ngSwitchCase="'happy'"    [hero]="currentHero">
      </app-happy-hero>
      <app-sad-hero      *ngSwitchCase="'sad'"      [hero]="currentHero">
      </app-sad-hero>
      <app-confused-hero *ngSwitchCase="'confused'" [hero]="currentHero">
      </app-confused-hero>
      <app-unknown-hero  *ngSwitchDefault           [hero]="currentHero">
      </app-unknown-hero>
    </div>
    <div [ngSwitch]="hero?.emotion">
      <ng-template [ngSwitchCase]="'happy'">
        <app-happy-hero [hero]="hero"></app-happy-hero>
      </ng-template>
      <ng-template [ngSwitchCase]="'sad'">
        <app-sad-hero [hero]="hero"></app-sad-hero>
      </ng-template>
      <ng-template [ngSwitchCase]="'confused'">
        <app-confused-hero [hero]="hero"></app-confused-hero>
      </ng-template >
      <ng-template ngSwitchDefault>
        <app-unknown-hero [hero]="hero"></app-unknown-hero>
      </ng-template>
    </div>

    <!-- <ng-container> -->
    <select [(ngModel)]="hero">
      <ng-container *ngFor="let h of heroes">
        <ng-container *ngIf="showSad || h.emotion !== 'sad'">
          <option [ngValue]="h">{{h.name}} ({{h.emotion}})</option>
        </ng-container>
      </ng-container>
    </select>
  

INPUT AND OUTPUT PROPERTIES, binding to a different component


    <!-- other component property is to the left of the (=) -->
    <app-hero-detail [hero]="currentHero" (deleteRequest)="deleteHero($event)">
    </app-hero-detail>
    <!--
      @Input()  hero: Hero;
      @Output() deleteRequest = new EventEmitter<Hero>();
      ---
      @Component({
        inputs: ['hero'],
        outputs: ['deleteRequest'],
      })
      --- @Output(alias) propertyName = ...
      @Output('myClick') clicks = new EventEmitter<string>();
      ---
      @Directive({
        outputs: ['clicks:myClick']  // propertyName:alias
      })
    -->
  

TEMPLATE EXPRESSION OPERATORS


    <!-- pipe operator - | -->
    <div>Title through uppercase pipe: {{title | uppercase}}</div>
    <!-- chaining -->
    <div>Title through a pipe chain: {{title | uppercase | lowercase}}</div>
    <!-- apply parameters, pipe with configuration argument => "February 25, 1970" -->
    <div>Birthdate: {{currentHero?.birthdate | date:'longDate'}}</div>
    <div>{{currentHero | json}}</div>

    <!-- safe navigation operator ( ?. ) guards against null and undefined, a?.b?.c?.d -->
    <span>The current hero name is {{currentHero?.name}}</span>

    <!-- isabling type checking -->
    {{$any(person).addresss.street}}

    <span *ngIf="person && address"> {{person.name}} lives on {{address.street}} </span>

    <!-- non-null assertion operator ( ! ) -->
    <!--
      suspend strict null checks for a specific property expression
      required when -strictNullChecks
    -->
    <div *ngIf="hero">The hero's name is {{hero!.name}}</div>

    <!-- $any type cast function, silence the error - $any(<expression>) -->
    <!-- prevents TS reporting that 'marker' is not a member of the Hero interface -->
    <div>The hero marker is {{$any(hero).marker}}</div>
    <!-- access to undeclared members of the component -->
    <div>Undeclared members is {{$any(this).member}}</div>
  

    <!-- set constant and dynamic property -->
    <app-hero-detail prefix="You are my" [hero]="currentHero"></app-hero-detail>

    <!-- same things -->
    <img src="{{heroImageUrl}}">
    <img [src]="heroImageUrl">
    <img bind-src="heroImageUrl">
    <!-- OR -->
    <span>"{{title}}" is the interpolated title</span>
    <span [innerHTML]="title"></span>" is the property bound title
  












Pipes

UpperCasePipe {{ value | uppercase }}
LowerCasePipe {{ value | lowercase }}
TitleCasePipe {{ value | titlecase }}
DatePipe {{ value | date [ : format [ : timezone [ : locale ] ] ] }}
{{ dateObj | date }} // output is 'Jun 15, 2015'
{{ dateObj | date:'medium' }} // output is 'Jun 15, 2015, 9:43:11 PM'
{{ dateObj | date:'shortTime' }} // output is '9:43 PM'
{{ dateObj | date:'mmss' }} // output is '43:11'
'short': equivalent to 'M/d/yy, h:mm a' (6/15/15, 9:03 AM)
'medium': equivalent to 'MMM d, y, h:mm:ss a' (Jun 15, 2015, 9:03:01 AM)
'long': equivalent to 'MMMM d, y, h:mm:ss a z' (June 15, 2015 at 9:03:01 AM GMT+1)
'full': equivalent to 'EEEE, MMMM d, y, h:mm:ss a zzzz' (Monday, June 15, 2015 at 9:03:01 AM GMT+01:00)
'shortDate': equivalent to 'M/d/yy' (6/15/15)
'mediumDate': equivalent to 'MMM d, y' (Jun 15, 2015)
'longDate': equivalent to 'MMMM d, y' (June 15, 2015)
'fullDate': equivalent to 'EEEE, MMMM d, y' (Monday, June 15, 2015)
'shortTime': equivalent to 'h:mm a' (9:03 AM)
'mediumTime': equivalent to 'h:mm:ss a' (9:03:01 AM)
'longTime': equivalent to 'h:mm:ss a z' (9:03:01 AM GMT+1)
'fullTime': equivalent to 'h:mm:ss a zzzz' (9:03:01 AM GMT+01:00)
DecimalPipe {{ value | number [ : digitsInfo [ : locale ] ] }}
{{epsilon | number}}
{{pi | number:'3.1-5'}}
{{epsilon | number:'4.5-5':'fr'}}
{{epsilon | number}}
{{epsilon | number}}
{{epsilon | number}}
{{epsilon | number}}
PercentPipe {{ value | percent [ : digitsInfo [ : locale ] ] }}
{{a | percent}}
{{b | percent:'4.3-5':'fr'}}
CurrencyPipe {{ value | currency [ : currencyCode [ : display [ : digitsInfo [ : locale ] ] ] ] }}
{{a | currency}}
{{a | currency:'CAD'}}
{{a | currency:'CAD':'code'}}
{{b | currency:'CAD':'symbol':'4.2-2'}}
{{b | currency:'CAD':'symbol-narrow':'4.2-2'}}
{{b | currency:'CAD':'symbol':'4.2-2':'fr'}}
SlicePipe {{ value | slice : start [ : end ] }}
<li *ngFor="let i of collection | slice:1:3">{{i}}</li>

{{str | slice:0:4}}
{{str | slice:4:0}}
{{str | slice:-4}}
{{str | slice:-4:-2}}
KeyValuePipe {{ input_expression | keyvalue [ : compareFn ] }}
<div *ngFor="let item of object | keyvalue"> {{item.key}}:{{item.value}} </div>
JsonPipe {{ value | json }} , <pre>{{object | json}}</pre>
AsyncPipe {{ message$ | async }}
// ---
message$: Observable<string>;
I18nPluralPipe {{ value | i18nPlural : pluralMap [ : locale ] }}
        @Component({
          selector: "i18n-select-pipe",
          template: "{{gender | i18nSelect: inviteMap}}"
        })
        export class I18nSelectPipeComponent {
          gender: string = 'male';
          inviteMap: any = {
            'male': 'Invite him.',
            'female': 'Invite her.',
            'other': 'Invite them.'
          };
        }
        
I18nSelectPipe {{ value | i18nSelect : mapping }}
        @Component({
          selector: "i18n-plural-pipe",
          template: "{{ messages.length | i18nPlural: messageMapping }}"
        })
        export class I18nPluralPipeComponent {
          messages: any[] = ['Message 1'];
          messageMapping:
            {[k: string]: string} = {
              '=0': 'No messages.',
              '=1': 'One message.',
              'other': '# messages.'
            };
        }
        

    // ---
    // pipe calls the server when the requested URL changes
    // and it caches the server response
    // ---
    import { HttpClient }          from '@angular/common/http';
    import { Pipe, PipeTransform } from '@angular/core';
    @Pipe({
      name: 'fetch',
      pure: false
    })
    export class FetchJsonPipe implements PipeTransform {
      private cachedData: any = null;
      private cachedUrl = '';
      constructor(private http: HttpClient) { }
      transform(url: string): any {
        if (url !== this.cachedUrl) {
          this.cachedData = null;
          this.cachedUrl = url;
          this.http.get(url).subscribe(result => this.cachedData = result);
        }
        return this.cachedData;
      }
    }
    // ---
    // requesting the heroes from the heroes.json file
    // ---
    import { Component } from '@angular/core';
    @Component({
      selector: 'app-hero-list',
      template: `
        <h2>Heroes from JSON File</h2>
        <div *ngFor="let hero of ('assets/heroes.json' | fetch) ">
          {{hero.name}}
        </div>
        <p>Heroes as JSON:
          {{'assets/heroes.json' | fetch | json}}
        </p>`
    })
    export class HeroListComponent { }
  

DI and services


    import { Injectable } from '@angular/core';
    import { UserModule } from './user.module';

    @Injectable({ // Injectable-level configuration
      providedIn: 'root',
    })
    export class UserService { }

    //--------------------------------

    @NgModule({ // NgModule-level injectors
      providers: [
        UserService,
        Logger
        // { provide: Logger, useClass: Logger }
      ],
      ...
    })

    //--------------------------------

    @Component({ // component-level injectors
      selector:    'app-hero-list',
      templateUrl: './hero-list.component.html',
      providers:  [ HeroService ]
    })
    export someCompnent {
      constructor(private logger: Logger) {
        this.logger.log(some_message);
      }
    }
  
hierachical class inheritance

    // --- car.component.ts
    import { Component } from '@angular/core';
    import { Car, Engine, Tires }  from './car';
    import { Car as CarNoDi }      from './car-no-di';
    import { CarFactory }          from './car-factory';
    import { testCar, simpleCar, superCar } from './car-creations';
    import { useInjector } from './car-injector';
    @Component({
      selector: 'app-car',
      template: `
      <h2>Cars</h2>
      <div id="di">{{car.drive()}}</div>
      <div id="nodi">{{noDiCar.drive()}}</div>
      <div id="injector">{{injectorCar.drive()}}</div>
      <div id="factory">{{factoryCar.drive()}}</div>
      <div id="simple">{{simpleCar.drive()}}</div>
      <div id="super">{{superCar.drive()}}</div>
      <div id="test">{{testCar.drive()}}</div>`,
      providers: [Car, Engine, Tires]
    })
    export class CarComponent {
      factoryCar  = (new CarFactory).createCar();
      injectorCar = useInjector();
      noDiCar     = new CarNoDi;
      simpleCar   = simpleCar();
      superCar    = superCar();
      testCar     = testCar();
      constructor(public car: Car) {}
    }

    // --- car.ts
    import { Injectable } from '@angular/core';
    export class Engine { public cylinders = 4; }
    export class Tires {
      public make  = 'Flintstone';
      public model = 'Square';
    }
    @Injectable()
    export class Car {
      public description = 'DI';
      constructor(public engine: Engine, public tires: Tires) { }
      // Method using the engine and tires
      drive() {
        return `${this.description} car with ` +
          `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
      }
    }

    // --- car-factory.ts
    import { Engine, Tires, Car } from './car';
    // BAD pattern!
    export class CarFactory {
      createCar() {
        let car = new Car(this.createEngine(), this.createTires());
        car.description = 'Factory';
        return car;
      }
      createEngine() {
        return new Engine();
      }
      createTires() {
        return new Tires();
      }
    }

    // --- car-creations.ts
    import { Car, Engine, Tires } from './car';
    // - 1
    export function simpleCar() {
      // Simple car with 4 cylinders and Flintstone tires.
      let car = new Car(new Engine(), new Tires());
      car.description = 'Simple';
      return car;
    }
    // - 2
    class Engine2 { constructor(public cylinders: number) { } }
    export function superCar() {
      // Super car with 12 cylinders and Flintstone tires.
      let bigCylinders = 12;
      let car = new Car(new Engine2(bigCylinders), new Tires());
      car.description = 'Super';
      return car;
    }
    // - 3
    class MockEngine extends Engine { cylinders = 8; }
    class MockTires  extends Tires  { make = 'YokoGoodStone'; }
    export function testCar() {
      // Test car with 8 cylinders and YokoGoodStone tires.
      let car = new Car(new MockEngine(), new MockTires());
      car.description = 'Test';
      return car;
    }

    // --- car-injector.ts
    import { Injector } from '@angular/core';
    import { Car, Engine, Tires } from './car';
    import { Logger }             from '../logger.service';
    export function useInjector() {
      let injector: Injector;
      /*
      // Cannot instantiate an Injector like this!
      let injector = new Injector([
        { provide: Car, deps: [Engine, Tires] },
        { provide: Engine, deps: [] },
        { provide: Tires, deps: [] }
      ]);
      */
      injector = Injector.create({
        providers: [
          { provide: Car, deps: [Engine, Tires] },
          { provide: Engine, deps: [] },
          { provide: Tires, deps: [] }
        ]
      });
      let car = injector.get(Car);
      car.description = 'Injector';
      injector = Injector.create({
        providers: [{ provide: Logger, deps: [] }]
      });
      let logger = injector.get(Logger);
      logger.log('Injector car.drive() said: ' + car.drive());
      return car;
    }

    // --- car-no-di.ts - car without DI
    import { Engine, Tires } from './car';
    export class Car {
      public engine: Engine;
      public tires: Tires;
      public description = 'No DI';
      constructor() {
        this.engine = new Engine();
        this.tires = new Tires();
      }
      // Method using the engine and tires
      drive() {
        return `${this.description} car with ` +
          `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
      }
    }
  
dependency provider - configures an injector with a DI token

    // --- ALTERNATIVE CLASS PROVIDERS
    [{ provide: Logger, useClass: BetterLogger }]

    // --- CLASS PROVIDERS WITH DEPENDENCIES
    [ UserService,
      {
        provide: Logger,
        useClass: EvenBetterLogger
      }
    ]
    // ...
    @Injectable()
    export class EvenBetterLogger extends Logger {
      constructor(private userService: UserService) { super(); }
      log(message: string) {
        let name = this.userService.user.name;
        super.log(`Message to ${name}: ${message}`);
      }
    }

    // --- ALIASED CLASS PROVIDERS
    // useExisting - lets you map one token to another
    // first token is an alias for the service associated with the second token,
    // creating two ways to access the same service object.
    // avoid two instances creation:
    [ NewLogger,
      {
        provide: OldLogger,
        useExisting: NewLogger
    }]
    // token for a provider:
    { provide: MinimalLogger, useExisting: LoggerService },...
    // minimal-logger.service.ts
    // class used as a "narrowing" interface that exposes a minimal logger
    // other members of the actual implementation are invisible
    export abstract class MinimalLogger {
      logs: string[];
      logInfo: (msg: string) => void;
    }

    // --- PROVIDE A READY-MADE OBJECT
    [{ provide: Logger, useValue: silentLogger }]
    export function SilentLoggerFn() {}
    const silentLogger = { // object in the shape of the logger service
      logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
      log: SilentLoggerFn
    };
    // InjectionToken objects
    { provide: TITLE, useValue: 'Hero of the Month' },
    // ...
    import { InjectionToken } from '@angular/core';
    export const TITLE = new InjectionToken<string>('title');

    // --- NON-CLASS DEPENDENCIES
    // src/app/app.config.ts
    export const HERO_DI_CONFIG: AppConfig = {
      apiEndpoint: 'api.heroes.com',
      title: 'Dependency Injection'
    };
    // TypeScript interfaces are not valid tokens !
    // use an InjectionToken whenever the type you are injecting
    // is not reified (does not have a runtime representation)
    // interface, callable type, array or parameterized type
    import { InjectionToken } from '@angular/core';
    export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
    // src/app/app.module.ts (providers)
    providers: [
      UserService,
      { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
    ],...
    // and
    constructor(@Inject(APP_CONFIG) config: AppConfig) {
      this.title = config.title;
    }

    // --- FACTORY PROVIDER
    // create a dependency object with a factory function
    // whose inputs are a combination of injected services and local state
    // user.service.ts
    export class User {
      constructor( public name: string, public isAuthorized = false) { }
    }
    let alice = new User('Alice', true);
    let bob = new User('Bob', false);
    @Injectable({ providedIn: 'root' })
    export class UserService {
      user = bob;  // initial user is Bob
      getNewUser() { // swap users, // TODO: get the user; don't 'new' it
        return this.user = this.user === bob ? alice : bob;
      }
    }
    // hero.service.provider.ts
    let heroServiceFactory = (logger: Logger, userService: UserService) => {
      return new HeroService(logger, userService.user.isAuthorized);
    };
    export let heroServiceProvider =
      {
        provide: HeroService,
        useFactory: heroServiceFactory,
        deps: [Logger, UserService]
      };
    // hero.service.ts
    @Injectable({
      providedIn: 'root',
      useFactory: (logger: Logger, userService: UserService) =>
          new HeroService(logger, userService.user.isAuthorized),
      deps: [Logger, UserService],
    })
    export class HeroService {
      constructor( private logger: Logger, private isAuthorized: boolean) { }
      getHeroes() {
        let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
        this.logger.log(`Getting heroes for ${auth} user.`);
        return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
      }
    }
    // ...
    constructor(heroService: HeroService) {
      this.heroes = heroService.getHeroes();
    }
    // ...
    {{hero.isSecret ? 'secret' : 'public'}})

    // --- PREDEFINED TOKENS AND MULTIPLE PROVIDERS
    export const APP_TOKENS = [
      { provide: PLATFORM_INITIALIZER,
        useFactory: platformInitialized, multi: true },
      { provide: APP_INITIALIZER,
        useFactory: delayBootstrapping, multi: true },
      { provide: APP_BOOTSTRAP_LISTENER,
        useFactory: appBootstrapped, multi: true },
    ];
  
multiple service instances

    // separate instance of the service for each component
    // hero-bios.component.ts
    @Component({
      selector: 'app-hero-bios',
      template: `
      <app-hero-bio [heroId]="1"></app-hero-bio>
      <app-hero-bio [heroId]="2"></app-hero-bio>
      <app-hero-bio [heroId]="3"></app-hero-bio>`,
      providers: [HeroService]
    })
    export class HeroBiosComponent { }
    // hero-cache.service.ts
    @Injectable()
    export class HeroCacheService {
      hero: Hero;
      constructor(private heroService: HeroService) {}
      fetchCachedHero(id: number) {
        if (!this.hero) {
          this.hero = this.heroService.getHeroById(id);
        }
        return this.hero;
      }
    }
    // hero-bio.component.ts
    @Component({
      selector: 'app-hero-bio',
      template: `
        <h4>{{hero.name}}</h4>
        <ng-content></ng-content>
        <textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
      providers: [HeroCacheService]
    })
    export class HeroBioComponent implements OnInit  {
      @Input() heroId: number;
      constructor(private heroCache: HeroCacheService) { }
      ngOnInit() { this.heroCache.fetchCachedHero(this.heroId); }
      get hero() { return this.heroCache.hero; }
    }
  
parameter decorators

    // @Host() , @Optional()
    @Component({
      selector: 'app-hero-contact',
      template: `
      Phone #: {{phoneNumber}}
      <span *ngIf="hasLogger">!!!</span>`
    })
    export class HeroContactComponent {
      hasLogger = false;
      constructor(
          @Host() // limit to the host components instance of the HeroCacheService
          private heroCache: HeroCacheService,
          @Host()     // limit search for logger; hides the application-wide logger
          @Optional() // ok if the logger doesnt exist
          private loggerService: LoggerService
      ) {
        if (loggerService) {
          this.hasLogger = true;
          loggerService.logInfo('HeroContactComponent can log!');
        }
      }
      get phoneNumber() { return this.heroCache.hero.phone; }
    }

    // @Inject - specify a custom provider of a dependency
    import { Inject, Injectable, InjectionToken } from '@angular/core';
    export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
      providedIn: 'root',
      factory: () => localStorage
    });
    @Injectable({ providedIn: 'root' })
    export class BrowserStorageService {
      constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
      get(key: string) { this.storage.getItem(key); }
      set(key: string, value: string) { this.storage.setItem(key, value); }
      remove(key: string) { this.storage.removeItem(key); }
      clear() { this.storage.clear(); }
    }

    // @Self - injector only looks at the component injector for its providers
    // @SkipSelf - skip local injector and look in hierarchy
    // override BROWSER_STORAGE in Component class providers with sessionStorage
    import { Component, OnInit, Self, SkipSelf } from '@angular/core';
    import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';
    @Component({
      selector: 'app-storage',
      template: `
        Open the inspector to see the local/session storage keys:
        <h3>Session Storage</h3>
        <button (click)="setSession()">Set Session Storage</button>
        <h3>Local Storage</h3>
        <button (click)="setLocal()">Set Local Storage</button>
      `,
      providers: [
        BrowserStorageService,
        { provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
      ]
    })
    export class StorageComponent implements OnInit {
      constructor(
        @Self() private sessionStorageService: BrowserStorageService,
        @SkipSelf() private localStorageService: BrowserStorageService,
      ) { }
      ngOnInit() { }
      setSession() {
        this.sessionStorageService.set('hero', 'Mr. Nice - Session');
      }
      setLocal() {
        this.localStorageService.set('hero', 'Mr. Nice - Local');
      }
    }
  
inject the component DOM element

    import { Directive, ElementRef, HostListener, Input } from '@angular/core';
    @Directive({
      selector: '[appHighlight]'
    })
    export class HighlightDirective {
      @Input('appHighlight') highlightColor: string;
      private el: HTMLElement;
      constructor(el: ElementRef) {
        this.el = el.nativeElement;
      }
      @HostListener('mouseenter')
      onMouseEnter() {
        this.highlight(this.highlightColor || 'cyan');
      }
      @HostListener('mouseleave')
      onMouseLeave() {
        this.highlight(null);
      }
      private highlight(color: string) {
        this.el.style.backgroundColor = color;
      }
    }
  

Forms

REACTIVE

      // --- src/app/app.module.ts
      import { ReactiveFormsModule } from '@angular/forms';
      @NgModule({
        imports: [
          // other imports ...
          ReactiveFormsModule
        ],
      })
      export class AppModule { }
      // ---  <app-profile-editor> </app-profile-editor>
      import { Component } from '@angular/core';
      import { FormGroup, FormControl } from '@angular/forms';
      import { FormBuilder } from '@angular/forms';
      import { Validators } from '@angular/forms';
      import { FormArray } from '@angular/forms';
      @Component({
        selector: 'app-profile-editor',
        template: `
        // --- --- ---
        Name:  <input type="text" [formControl]="name">
        <button (click)="updateName()">Update Name </button>
        <p>Value: {{ name.value }} </p>
        // --- --- ---
        <form [formGroup]="profileForm" (ngSubmit)="onSubmit()">
          First Name:  <input type="text" formControlName="firstName" required >
            <div *ngIf="profileForm.get('firstName').invalid && (profileForm.get('firstName').dirty || profileForm.get('firstName').touched)"">
              <div *ngIf="profileForm.get('firstName').errors.required"">Name is required</div">
              <div *ngIf="profileForm.get('firstName').errors.minlength"">
                Name must be at least 4 characters long
              </div">
              <div *ngIf="firstName.errors.forbiddenName"">Name cannot be Bob</div">
            </div">
          Last Name:  <input type="text" formControlName="lastName">
          <div formGroupName="address">
            <h3>Address </h3>
            Street:  <input type="text" formControlName="street">
            City:  <input type="text" formControlName="city">
            State:  <input type="text" formControlName="state">
            Zip Code:  <input type="text" formControlName="zip">
            <p>Value: {{ profileForm.get('address').street.value }} </p>
          </div>
          <button (click)="updateProfile()">Update Profile </button>
          // --- --- ---
          <div formArrayName="cities">
            <div *ngFor="let city of cities.controls; index as i">
              <input [formControlName]="i" placeholder="City">
            </div>
          </div>
          <button (click)="addCity()">Add City </button>
          // --- --- ---
          <div formArrayName="aliases">
            <h3>Aliases </h3>  <button (click)="addAlias()">Add Alias </button>
            <div *ngFor="let address of aliases.controls; let i=index">
              <!-- The repeated alias template -->
              Alias:  <input type="text" [formControlName]="i">
            </div>
          </div>
          // --- --- ---
          <button type="submit" [disabled]="!profileForm.valid">
            Submit
          </button>
          Form Status: {{ profileForm.status }}
        </form>
        // --- --- ---
        `
      })
      export class ProfileEditorComponent {
        constructor(private fb: FormBuilder) { }

        name = new FormControl('');
        updateName() {
          this.name.setValue(
            'Nancy',
            // delay updating form validity: change(default)|submit|blur
            {updateOn: 'blur'}
          );
        }

        // --- WITH INSTANCES

        profileForm = new FormGroup({
          firstName: new FormControl('Andrei',[
            Validators.required,
            Validators.minLength(4),
            forbiddenNameValidator(/bob/i) // how you pass in the custom validator
          ]),
          lastName: new FormControl('T.', Validators.required),
          address: new FormGroup({
            street: new FormControl(''),
            city: new FormControl(''),
            state: new FormControl(''),
            zip: new FormControl('')
          }),
          cities: new FormArray([
            new FormControl('SF'),
            new FormControl('NY'),
          ]),
        });
        get cities(): FormArray { return this.profileForm.get('cities') as FormArray; }
        addCity() { this.cities.push(new FormControl()); }
        onSubmit() {
          console.log(this.cities.value);  // ['SF', 'NY']
          console.log(this.form.value);    // { cities: ['SF', 'NY'] }
        }
        setPreset() { this.cities.patchValue(['LA', 'MTV']); }

        // --- WITH FORM BUILDER

        profileForm = this.fb.group({
          firstName: ['', Validators.required],
          lastName: [''],
          address: this.fb.group({
            street: [''],
            city: [''],
            state: [''],
            zip: ['']
          }),
          // undefined number of controls in an array
          aliases: this.fb.array([
            this.fb.control('')
          ])
        });
        get aliases() {
          return this.profileForm.get('aliases') as FormArray;
        }
        addAlias() {
          this.aliases.push(this.fb.control(''));
        }
        // ---
        updateProfile() {
          this.profileForm.patchValue({
            firstName: 'Andrei',
            address: {
              street: '123 Here Street'
            }
          });
        }
        // setValue() { this.form.setValue({first: 'Carson', last: 'Drew'}); }
        onSubmit() {
          // TODO: Use EventEmitter with form value
          console.warn(this.profileForm.value);
        }
      }
    
TEMPLATE-DRIVEN

      // --- src/app/app.module.ts
      import { FormsModule } from '@angular/forms';
      @NgModule({
        imports: [
          // other imports ...
          FormsModule
        ],
      })
      export class AppModule { }
      // ---  <app-hero-form></app-hero-form>
      import { Component } from '@angular/core';
      import { Hero }    from '../hero';
      @Component({
        selector: 'app-hero-form',
        template: `
        <div>
          <div [hidden]="submitted">
            <h1>Hero Form</h1>
            <form (ngSubmit)="onSubmit()" #heroForm="ngForm">
              <div>
                <label for="name">Name</label>
                <input type="text" id="name"
                        required minlength="4" appForbiddenName="bob"
                        [(ngModel)]="model.name" name="name"
                        // delay updating the form validity
                        [ngModelOptions]="{updateOn: 'blur'}"
                        #name="ngModel">
                <div *ngIf="name.invalid && (name.dirty || name.touched)"
                    class="alert alert-danger">
                  <div *ngIf="name.errors.required">
                    Name is required.
                  </div>
                  <div *ngIf="name.errors.minlength">
                    Name must be at least 4 characters long.
                  </div>
                  <div *ngIf="name.errors.forbiddenName">
                    Name cannot be Bob.
                  </div>
                </div>
              </div>
              <div>
                <label for="alterEgo">Alter Ego</label>
                <input type="text" id="alterEgo"
                        [(ngModel)]="model.alterEgo" name="alterEgo">
              </div>
              <div>
                <label for="power">Hero Power</label>
                <select id="power" required
                        [(ngModel)]="model.power" name="power"
                        #power="ngModel">
                  <option *ngFor="let pow of powers" [value]="pow">{{pow}}</option>
                </select>
                <div [hidden]="power.valid || power.pristine">
                  Power is required
                </div>
              </div>
              <button type="submit"
                      [disabled]="!heroForm.form.valid">Submit</button>
              <button type="button"
                      (click)="newHero(); heroForm.reset()">New Hero</button>
            </form>
          </div>
          <div [hidden]="!submitted">
            <h2>You submitted the following:</h2>
            Name: {{ model.name }}<br>
            Alter Ego: {{ model.alterEgo }}<br>
            Power: {{ model.power }}<br>
            <button  (click)="submitted=false">Edit</button>
          </div>
        </div>
        `,
        style: `
          .ng-valid[required], .ng-valid.required  {
            border-left: 5px solid #42A948; /* green */
          }
          .ng-invalid:not(form)  {
            border-left: 5px solid #a94442; /* red */
          }
        `
      })
      export class HeroFormComponent {
        powers = ['Really Smart', 'Super Flexible',
                  'Super Hot', 'Weather Changer'];
        model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');
        submitted = false;
        onSubmit() { this.submitted = true; }
        newHero() {
          this.model = new Hero(42, '', '');
        }
      }
    
custom validators

      //--- validator for reactive form
      // forbidden-name.directive.ts (forbiddenNameValidator)
      // name cant match the given regular expression
      export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
        return (control: AbstractControl): {[key: string]: any} | null => {
          const forbidden = nameRe.test(control.value);
          return forbidden ? {'forbiddenName': {value: control.value}} : null;
        };
      }
      //--- extending for template driven form as directive
      @Directive({
        selector: '[appForbiddenName]',
        providers: [{
          provide: NG_VALIDATORS,
          useExisting: ForbiddenValidatorDirective,
          multi: true
        }]
      })
      export class ForbiddenValidatorDirective implements Validator {
        @Input('appForbiddenName') forbiddenName: string;
        validate(control: AbstractControl): {[key: string]: any} | null {
          return this.forbiddenName ?
            forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control) : null;
        }
      }
    
cross-field validation

      // REACTIVE
      const heroForm = new FormGroup({
        'name': new FormControl(),
        'alterEgo': new FormControl(),
        'power': new FormControl()
      }, { validators: identityRevealedValidator });
      // //--- shared/identity-revealed.directive.ts
      // // name cant match the hero alter ego
      // export const identityRevealedValidator: ValidatorFn =
      //   (control: FormGroup): ValidationErrors | null => {
      //     const name = control.get('name');
      //     const alterEgo = control.get('alterEgo');
      //     return name && alterEgo &&
      //       name.value === alterEgo.value ? { 'identityRevealed': true } : null;
      // };
      // --- --- ---
      // <div
      // *ngIf="heroForm.errors?.identityRevealed && (heroForm.touched || heroForm.dirty)"
      // class="cross-validation-error-message alert alert-danger">
      //     Name cannot match alter ego.
      // </div>
      // for TEMPLATE-DRIVEN
      @Directive({
        selector: '[appIdentityRevealed]',
        providers: [{
          provide: NG_VALIDATORS,
          useExisting: IdentityRevealedValidatorDirective,
          multi: true
        }]
      })
      export class IdentityRevealedValidatorDirective implements Validator {
        validate(control: AbstractControl): ValidationErrors {
          return identityRevealedValidator(control)
        }
      }
      // <form #heroForm="ngForm" appIdentityRevealed>
      //   <div *ngIf="heroForm.errors?.identityRevealed &&
      //      (heroForm.touched || heroForm.dirty)"
      //     class="cross-validation-error-message alert alert-danger">
      //       Name cannot match alter ego.
      //   </div>
    
custom async validator

      @Injectable({ providedIn: 'root' })
      export class UniqueAlterEgoValidator implements AsyncValidator {
        constructor(private heroesService: HeroesService) {}
        validate(
          ctrl: AbstractControl
        ): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> {
          return this.heroesService.isAlterEgoTaken(ctrl.value).pipe(
            map(isTaken => (isTaken ? { uniqueAlterEgo: true } : null)),
            catchError(() => null)
          );
        }
      }
      //--------------------------------
      // responsible for check if the alter ego is available
      interface HeroesService {
        isAlterEgoTaken: (alterEgo: string) => Observable<boolean>;
      }
    
tests

      // reactive form - view to model test
      it('should update the value of the input field', () => {
        // query the view for the form input element
        const input = fixture.nativeElement.querySelector('input');
        // custom "input" event for the test
        const event = createNewEvent('input');
        input.value = 'Red';
        input.dispatchEvent(event);
        // assert component favoriteColorControl value
        expect(
          fixture.componentInstance.favoriteColorControl.value
        ).toEqual('Red');
      });

      // template-driven form
      // requires a detailed knowledge of the change detection process
      // and an understanding of how directives run on each cycle
      // to ensure that elements are queried, tested, or changed at the correct time
      // view to model test
      it('should update the favorite color in the component', fakeAsync(() => {
        const input = fixture.nativeElement.querySelector('input');
        const event = createNewEvent('input');
        input.value = 'Red';
        input.dispatchEvent(event);
        fixture.detectChanges();
        expect(component.favoriteColor).toEqual('Red');
      }));
      // model to view
      it('should update the favorite color on the input field', fakeAsync(() => {
        component.favoriteColor = 'Blue';
        fixture.detectChanges();
        tick();
        const input = fixture.nativeElement.querySelector('input');
        expect(input.value).toBe('Blue');
      }));
    
Reactive Template-driven
Setup (form model) More explicit, created in component class Less explicit, created by directives
Data model Structured Unstructured
Predictability Synchronous Asynchronous
Form validation Functions Directives
Mutability Immutable Mutable
Scalability Low-level API access Abstraction on top of APIs

Observables


    // create simple observable that emits three values
    const myObservable = of(1, 2);
    // create observer object
    const myObserver = {
      next: x => console.log('Observer got a next value: ' + x),
      error: err => console.error('Observer got an error: ' + err),
      complete: () => console.log('Observer got a complete notification'),
    };
    // execute with the observer object
    myObservable.subscribe(myObserver);
    // myObservable.subscribe(
    //   x => console.log(x),
    //   err => console.error('Observer got an error: ' + err),
    //   () => console.log('Observer got a complete notification')
    // );
    // logs: 1 , 2 , Observer got a complete notification
    // --- SAME AS
    function sequenceSubscriber(observer) { // runs when subscribe() is called
      // synchronously deliver 1 and 2 then complete
      observer.next(1);
      observer.next(2);
      observer.complete();
      return {unsubscribe() {}}; // no scenario here, values delivered synchronously
    }
    const sequence = new Observable(sequenceSubscriber); // deliver the above sequence
    sequence.subscribe({
      next(num) { console.log(num); },
      complete() { console.log('Finished sequence'); }
    });
    // Logs: 1 , 2 , Finished sequence
  

EXAMPLES


    // --- UNSUBSCRIBING
    @Component({...})
    export class LineChartComponent implements OnInit {
      private subscription: Subscription = new Subscription();
      everySecond$: Observable = timer(1, 1000);
      everyMinute$: Observable = timer(1, 60000);
      ngOnInit() {
        this.subscription.add(this.everySecond$.subscribe(
          second => console.log(second)
        ));
        ngOnInit() {
          this.subscription.add(this.everyMinute$.subscribe(
            minute => console.log(minute)
          ));
      }
      ngOnDestroy() {
        this.subscription.unsubscribe();
      }
    }

    // --- MULTICASTING
    function multicastSequenceSubscriber() {
      const seq = [1, 2, 3];
      const observers = [];
      let timeoutId;
      return (observer) => {
        observers.push(observer);
        // start the sequence on first subscription
        if (observers.length === 1) {
          timeoutId = doSequence({
            next(val) { observers.forEach(obs => obs.next(val)); },
            complete() { observers.slice(0).forEach(obs => obs.complete()); }
          }, seq, 0);
        }
        return {
          unsubscribe() {
            // remove from the observers array
            observers.splice(observers.indexOf(observer), 1);
            // cleanup if there is no more listeners
            if (observers.length === 0) { clearTimeout(timeoutId); }
          }
        };
      };
    }
    // Run through an array of numbers, emitting one value
    // per second until it gets to the end of the array.
    function doSequence(observer, arr, idx) {
      return setTimeout(() => {
        observer.next(arr[idx]);
        if (idx === arr.length - 1) {
          observer.complete();
        } else {
          doSequence(observer, arr, ++idx);
        }
      }, 1000);
    }
    // Create a new Observable that will deliver the above sequence
    const multicastSequence = new Observable(multicastSequenceSubscriber());
    // Subscribe starts the clock, and begins to emit after 1 second
    multicastSequence.subscribe({
      next(num) { console.log('1st subscribe: ' + num); },
      complete() { console.log('1st sequence finished.'); }
    });
    // After 1 1/2 seconds, subscribe again (should "miss" the first value).
    setTimeout(() => {
      multicastSequence.subscribe({
        next(num) { console.log('2nd subscribe: ' + num); },
        complete() { console.log('2nd sequence finished.'); }
      });
    }, 1500);
    // Logs:
    // (at 1 second): 1st subscribe: 1
    // (at 2 seconds): 1st subscribe: 2
    // (at 2 seconds): 2nd subscribe: 2
    // (at 3 seconds): 1st subscribe: 3
    // (at 3 seconds): 1st sequence finished
    // (at 3 seconds): 2nd subscribe: 3
    // (at 3 seconds): 2nd sequence finished

    // --- PIPE
    import { filter, map } from 'rxjs/operators';
    const squareOdd = of(1, 2, 3, 4, 5)
      .pipe(
        filter(n => n % 2 !== 0),
        map(n => n * n)
      );
    // Subscribe to get values
    squareOdd.subscribe(x => console.log(x));

    // ---  EventEmitter
    // component that listens for open and close events
    // <zippy (open)="onOpen($event)" (close)="onClose($event)"></zippy>
    @Component({
      selector: 'zippy',
      template: `
      <div class="zippy">
        <div (click)="toggle()">Toggle</div>
        <div [hidden]="!visible">
          <ng-content></ng-content>
        </div>
      </div>`})
    export class ZippyComponent {
      visible = true;
      @Output() open = new EventEmitter<any>();
      @Output() close = new EventEmitter<any>();
      toggle() {
        this.visible = !this.visible;
        if (this.visible) {
          this.open.emit(null);
        } else {
          this.close.emit(null);
        }
      }
    }

    // --- AsyncPipe
    @Component({
      selector: 'async-observable-pipe',
      template: `Time: {{ time | async }}`
    })
    export class AsyncObservablePipeComponent {
      time = new Observable(observer =>
        setInterval(() => observer.next(new Date().toString()), 1000)
      );
    }

    // --- Router
    import { Router, ActivatedRoute, NavigationStart } from '@angular/router';
    import { filter } from 'rxjs/operators';
    @Component({ selector: 'app-routable', ... })
    export class Routable1Component implements OnInit {
      navStart: Observable<NavigationStart>;
      constructor(
        private router: Router,
        private activatedRoute: ActivatedRoute
      ) {
        // look for events of interest, only the NavigationStart event
        this.navStart = router.events.pipe(
          filter(evt => evt instanceof NavigationStart)
        ) as Observable<NavigationStart>;
      }
      ngOnInit() {
        this.navStart.subscribe(evt => console.log('Navigation Started!'));
        // report the route path or paths
        this.activatedRoute.url
          .subscribe(url => console.log('The URL changed to: ' + url));
      }
    }

    // --- Reactive forms
    import { FormGroup } from '@angular/forms';
    @Component({
      selector: 'my-component', ...
    })
    export class MyComponent implements OnInit {
      nameChangeLog: string[] = [];
      heroForm: FormGroup;
      ngOnInit() {
        this.logNameChange();
      }
      logNameChange() {
        // valueChanges and statusChanges contain change events observables
        const nameControl = this.heroForm.get('name');
        nameControl.valueChanges.forEach(
          (value: string) => this.nameChangeLog.push(value)
        );
      }
    }

    // --- Typeahead
    import { fromEvent } from 'rxjs';
    import { ajax } from 'rxjs/ajax';
    import {
      map, filter, debounceTime, distinctUntilChanged, switchMap
    } from 'rxjs/operators';
    const searchBox = document.getElementById('search-box');
    const typeahead = fromEvent(searchBox, 'input').pipe(
      map((e: KeyboardEvent) => e.target.value),
      filter(text => text.length > 2),
      debounceTime(10),
      distinctUntilChanged(),
      switchMap(() => ajax('/api/endpoint'))
    );
    typeahead.subscribe(data => {
     // Handle the data from the API
    });


    import { pipe, range, timer, zip } from 'rxjs';
    import { ajax } from 'rxjs/ajax';
    import { retryWhen, map, mergeMap } from 'rxjs/operators';
    function backoff(maxTries, ms) {
     return pipe(
       retryWhen(attempts => zip(range(1, maxTries), attempts)
         .pipe(
           map(([i]) => i * i),
           mergeMap(i =>  timer(i * ms))
    )))}
    ajax('/api/endpoint')
      .pipe(backoff(3, 250)).subscribe(data => handleData(data));
    function handleData(data) { /*...*/ }

    // start listening to geolocation updates when a consumer subscribes
    const locations = new Observable((observer) => {
      // next and error callbacks, passed when the consumer subscribes
      const {next, error} = observer;
      let watchId;
      // geolocation API check provides values to publish
      if ('geolocation' in navigator) {
        watchId = navigator.geolocation.watchPosition(next, error);
      } else { error('Geolocation not available'); }
      // consumer unsubscribes, clean up data ready for next subscription.
      return {unsubscribe() { navigator.geolocation.clearWatch(watchId); }};
    });
    // call subscribe() to start listening for updates.
    const locationsSubscription = locations.subscribe({
      next(position) { console.log('Current Position: ', position); },
      error(msg) { console.log('Error Getting Location: ', msg); }
    });
    // Stop listening for location after 10 seconds
    setTimeout(() => { locationsSubscription.unsubscribe(); }, 10000);
  

Observables VS Promises

Operation Observable Promise
Creation  new Observable((observer) => {
  observer.next(123);
 });
 new Promise((resolve, reject) => {
  resolve(123);
 });
Transform obs.map((value) => value * 2 ); promise.then((value) => value * 2);
Subscribe  sub = obs.subscribe((value) => {
  console.log(value)
 });
 promise.then((value) => {
  console.log(value);
 });
Unsubscribe sub.unsubscribe(); Implied by promise resolution.

Observables VS Events API

Observable Events API
Creation & cancellation  // Setup
 let clicks$ = fromEvent(buttonEl, ‘click’);
 // Begin listening
 let subscription = clicks$
  .subscribe(e => console.log(‘Clicked’, e))
 // Stop listening
 subscription.unsubscribe();
 function handler(e) {
  console.log(‘Clicked’, e);
 }
 // Setup & begin listening
 button.addEventListener(‘click’, handler);
 // Stop listening
 button.removeEventListener(‘click’, handler);
Subscription  observable.subscribe(() => {
  // notification handlers here
 });
 element.addEventListener(
  eventName,
  (event) => {
  // notification handler here
 });
Configuration Listen for keystrokes, but provide a stream representing the value in the input
 fromEvent(inputEl, 'keydown').pipe(
  map(e => e.target.value)
 );
Does not support configuration
 element.addEventListener(
  eventName,
  (event) => {
  // Cannot change the passed Event into another
  // value before it gets to the handler
 });

Observables VS arrays

Observable Array
Given
obs: ➞1➞2➞3➞5➞7
obsB: ➞'a'➞'b'➞'c'
arr: [1, 2, 3, 5, 7]
arrB: ['a', 'b', 'c']
concat()
obs.concat(obsB)
➞1➞2➞3➞5➞7➞'a'➞'b'➞'c'
arr.concat(arrB)
[1,2,3,5,7,'a','b','c']
filter()
obs.filter((v) => v>3)
➞5➞7
arr.filter((v) => v>3)
[5, 7]
find()
obs.find((v) => v>3)
➞5
arr.find((v) => v>3)
5
findIndex()
obs.findIndex((v) => v>3)
➞3
arr.findIndex((v) => v>3)
3
forEach()
obs.forEach((v) => { console.log(v); })
          1 2 3 5 7
        
arr.forEach((v) => { console.log(v); })
          1 2 3 5 7
        
map()
obs.map((v) => -v)
➞-1➞-2➞-3➞-5➞-7
arr.map((v) => -v)
[-1, -2, -3, -5, -7]
reduce()
obs.scan((s,v)=> s+v, 0)
➞1➞3➞6➞11➞18
arr.reduce((s,v) => s+v, 0)
18

Http/requests


    // app.module.ts
    import { NgModule }         from '@angular/core';
    import { BrowserModule }    from '@angular/platform-browser';
    import { HttpClientModule } from '@angular/common/http';
    import { HttpClientXsrfModule } from '@angular/common/http';
    import { httpInterceptorProviders } from './http-interceptors/index';
    @NgModule({
      imports: [
        BrowserModule,
        // import HttpClientModule after BrowserModule
        HttpClientModule,
        // configuring custom cookie/header names
        HttpClientXsrfModule.withOptions({
          cookieName: 'My-Xsrf-Cookie',
          headerName: 'My-Xsrf-Header',
        }),
      ],
      declarations: [
        AppComponent,
      ],
      providers: [
        httpInterceptorProviders
      ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule {}

    // http-interceptors/index.ts
    // "Barrel" of Http Interceptors
    import { HTTP_INTERCEPTORS } from '@angular/common/http';
    import { AuthInterceptor } from './auth-interceptor';
    import { CachingInterceptor } from './caching-interceptor';
    import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
    import { LoggingInterceptor } from './logging-interceptor';
    import { TrimNameInterceptor } from './trim-name-interceptor';
    import { UploadInterceptor } from './upload-interceptor';
    // Http interceptor providers in outside-in order
    export const httpInterceptorProviders = [
      { provide: HTTP_INTERCEPTORS, useClass: EnsureHttpsInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: UploadInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true },
      // multi: true - multiprovider, inject this array of values, rather than a single value in root
    ];
  
note.service.ts

    import { Injectable } from '@angular/core';
    import { Observable, of } from 'rxjs';
    import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
    import { catchError, map, tap } from 'rxjs/operators';
    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type': 'application/json',
        'x-refresh':  'true'
      })
    };

    function createHttpOptions(packageName: string, refresh = false) {
      // npm package name search api: http://npmsearch.com/query?q=dom
      const params = new HttpParams({ fromObject: { q: packageName } });
      const headerMap = refresh ? {'x-refresh': 'true'} : {};
      const headers = new HttpHeaders(headerMap) ;
      return { headers, params };
    }

    // export interface NpmPackageInfo {
    //   name: string;
    //   version: string;
    //   description: string;
    // }

    import { Note } from './note';
    // import { NOTES } from './mock-notes';
    import { MessageService } from './message.service';

    export const notesUrl = 'api/notes';

    @Injectable({
      providedIn: 'root' // injects into any class that asks for it
    })
    export class NoteService {// URL to web api
      constructor(
        private http: HttpClient,
        private messageService: MessageService // injecting other service
      ) { }
      // getNotes(): Note[] {
      //   return NOTES;
      // }
      getNotes(): Observable<Note[]> {
        // TODO: send the message _after_ fetching the heroes
        // this.log('fetched notes');
        // return of(NOTES);
        return this.http.get<Note[]>(notesUrl).pipe(
          tap(_ => this.log('fetched notes')),
          catchError(this.handleError('getNotes', []))
        );
      }
      getNoteNo404<Data>(id: number): Observable<Note> {
        const url = `${notesUrl}/?id=${id}`;
        return this.http.get<Note[]>(url)
          .pipe(
            map(notes => notes[0]), // returns a {0|1} element array
            tap(h => {
              const outcome = h ? `fetched` : `did not find`;
              this.log(`${outcome} note id=${id}`);
            }),
            catchError(this.handleError<Note>(`getNote id=${id}`))
          );
      }
      getNote(id: number): Observable<Note> {
        // this.log(`fetched note id=${id}`); // send _after_ fetching the heroes
        // return of(NOTES.find(note => note.id === id));
        const url = `${notesUrl}/${id}`;
        return this.http.get<Note>(url).pipe(
          tap(_ => this.log(`fetched note id=${id}`)),
          catchError(this.handleError<Note>(`getNote id=${id}`))
        );
      }
      updateNote (note: Note): Observable<any> {
        return this.http.put(notesUrl, note, httpOptions).pipe(
          tap(_ => this.log(`updated note id=${note.id}`)),
          catchError(this.handleError<any>('updateNote'))
        );
      }
      addNote (note: Note): Observable<Note> {
        return this.http.post<Note>(notesUrl, note, httpOptions).pipe(
          tap((note: Note) => this.log(`added note w/ id=${note.id}`)),
          catchError(this.handleError<Note>('addNote'))
        );
      }
      deleteNote (note: Note | number): Observable<Note> {
        const id = typeof note === 'number' ? note : note.id;
        const url = `${notesUrl}/${id}`;
        return this.http.delete<Note>(url, httpOptions).pipe(
          tap(_ => this.log(`deleted note id=${id}`)),
          catchError(this.handleError<Note>('deleteNote'))
        );
      }
      searchNotes(
        term: string,
        refresh = false
      ): Observable<Note[]> {
        if (!term.trim()) { return of([]); }
        // const options = term ?
        // { params: new HttpParams().set('title', term) } : {}; // URL encoded search parameter
        const options = createHttpOptions(term, refresh);
        console.log(options)
        return this.http.get<Note[]>(notesUrl, options).pipe(
          tap(_ => this.log(`found notes matching "${term}"`)),
          // map((data: any) => {
          //   return data.results.map(entry => ({
          //       name: entry.name[0],
          //       version: entry.version[0],
          //       description: entry.description[0]
          //     } as NpmPackageInfo )
          //   );
          // }),
          catchError(this.handleError<Note[]>('searchNotes', []))
        );
        // return this.http.get<Note[]>(`${notesUrl}/?title=${term}`).pipe(...
      }

      // --------------------------------------
      private log(message: string) {
        this.messageService.add(`NoteService: ${message}`);
      }
      /**
        * Handle Http operation that failed. Let the app continue.
        * @param operation - name of the operation that failed
        * @param result - optional value to return as the observable result
        */
      private handleError<T> (operation = 'operation', result?: T) {
        return (error: any): Observable<T> => {
          // TODO: send the error to remote logging infrastructure
          console.error(error); // log to console instead
          // TODO: better job of transforming error for user consumption
          this.log(`${operation} failed: ${error.message}`);
          // Let the app keep running by returning an empty result.
          return of(result as T);
        };
      }
    }
  
note-search.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Observable, Subject } from 'rxjs';
    import {
        debounceTime, distinctUntilChanged, switchMap
      } from 'rxjs/operators';
    import { Note } from '../note';
    import { NoteService /*,NpmPackageInfo*/ } from '../note.service';

    @Component({
      selector: 'app-note-search',
      template: `
        <div id="search-component">
          <h4>Note Search</h4>
          <input #searchBox id="search-box" (input)="search(searchBox.value)" />
          <input type="checkbox" id="refresh" [checked]="withRefresh" (click)="toggleRefresh()">
          <label for="refresh">with refresh</label>
          <ul class="search-result">
            <li *ngFor="let note of notes$ | async" >
              <a routerLink="/note/{{note.id}}">
                {{note.title}}
              </a>
            </li>
          </ul>
        </div>
      `,
      styleUrls: ['./note-search.component.css']
    })
    export class NoteSearchComponent implements OnInit {
      withRefresh = false;
      // packages$: Observable<NpmPackageInfo[]>;
      notes$: Observable<Note[]>; // $ - as an Observable
      private searchTerms$ = new Subject<string>();
      constructor(private noteService: NoteService) { }
      ngOnInit(): void {
        this.notes$ = this.searchTerms$.pipe(
          // wait 300ms after each keystroke before considering the term
          debounceTime(300),
          // ignore new term if same as previous term
          distinctUntilChanged(),
          // switch to new search observable each time the term changes
          switchMap(
            (term: string) => this.noteService.searchNotes(term, this.withRefresh)
          ),
        );
      }
      // Push a search term into the observable stream.
      search(term: string): void {
        this.searchTerms$.next(term);
      }
      toggleRefresh() { this.withRefresh = ! this.withRefresh; }
    }
  
notes.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Note } from '../note';
    // import { NOTES } from '../mock-notes';
    import { NoteService } from '../note.service';

    @Component({
      selector: 'app-notes',
      template: `
        <h2>My Notes</h2>
        <div>
          <label>Note title:
            <input #noteTitle (keyup.enter)="add(noteTitle.value); noteTitle.value=''" />
          </label>
          <!-- (click) passes input value to add() and then clears the input -->
          <button (click)="add(noteTitle.value); noteTitle.value=''">
            add
          </button>
        </div>

        <ul class="notes">
          <li *ngFor="let note of notes" >
            <!-- [class.selected]="note === selectedNote" -->
            <!-- (click)="onSelect(note)" -->
            <a routerLink="/note/{{note.id}}">
              <span class="id">{{note.id}}</span> {{note.title}}
            </a>
            <button class="delete" title="delete note"
              (click)="delete(note)">x</button>
          </li>
        </ul>
        <!-- <app-note-details [note]="selectedNote"></app-note-details> -->
      `,
      styleUrls: ['./notes.component.css']
    })
    export class NotesComponent implements OnInit {
      // note: Note = {
      //   id: 1,
      //   title: 'Note 1 title',
      //   content: 'this is content sample'
      // };
      // notes = NOTES;
      notes: Note[];
      // selectedNote: Note;
      constructor(private noteService: NoteService) {}
      ngOnInit() {
        this.getNotes();
      }
      // onSelect(note: Note): void {
      //   this.selectedNote = note;
      // }
      getNotes(): void {
        this.noteService.getNotes()
          .subscribe(notes => this.notes = notes);
      }
      add(title: string): void {
        title = title.trim();
        if (!title) { return; }
        this.noteService.addNote({ title } as Note)
          .subscribe(note => {
            this.notes.push(note);
          });
      }
      delete(note: Note): void {
        this.notes = this.notes.filter(h => h !== note);
        this.noteService.deleteNote(note).subscribe();
      }
    }
  
http-examples.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Config, HttpExamplesService } from './http-examples.service';
    import { UploaderService } from './uploader.service';
    @Component({
      selector: 'app-http-examples',
      templateUrl: './http-examples.component.html',
      styles:[`
        textarea.error {
          width:50%;
          height:20em;
        }
      `],
      providers: [HttpExamplesService, UploaderService]
    })
    export class HttpExamplesComponent implements OnInit {
      error: any;
      headers: string[];
      config: Config;
      result_obj: Object;
      contents: string;
      configUrl = 'assets/config.json';
      textfileUrl = 'assets/textfile.txt';
      objectKeys = Object.keys;
      constructor(
        private httpExamplesService: HttpExamplesService,
        private uploaderService: UploaderService
      ) {}
      ngOnInit() { }
      clear() {
        this.error = undefined;
        this.headers = undefined;
        this.config = undefined;
        this.result_obj = undefined;
        this.contents = undefined;
      }
      showConfig() {
        this.httpExamplesService.getData(this.configUrl)
          .subscribe(
            (data: Config) => {
              this.config = { ...data }; // typed response object container
              this.result_obj = this.config; // assign to local object
            }, // success path
            error => this.error = error // error path
          );
          // .subscribe((data: Config) => this.config = {
          //   apiUrl: data['heroesUrl'],
          //   textfile:  data['textfile']
          // });
      }
      showConfigResponse() {
        this.httpExamplesService.getConfigDataResponse(
          this.configUrl
        ).subscribe(resp => { // resp is of type `HttpResponse<Config>`
            // display its headers
            const keys = resp.headers.keys();
            this.headers = keys.map(key =>
              `${key}: ${resp.headers.get(key)}`);
            // access the body directly, which is typed as `Config`.
            this.config = { ... resp.body }; // typed response object container
            this.result_obj = this.config; // assign here to local object
          });
      }
      onPicked(input: HTMLInputElement) {
        const file = input.files[0];
        if (file) {
          this.uploaderService.upload(file).subscribe(
            msg => {
              input.value = null;
              this.contents = msg;
            }
          );
        }
      }
      showTextfileContent() {
        this.httpExamplesService.getData(
          this.textfileUrl,
          {responseType: 'text'}
        ).subscribe(
          results => this.contents = results.toString(),
          error => this.error = error // error path
        );
        // .subscribe((data: Config) => this.config = {
        //   apiUrl: data['heroesUrl'],
        //   textfile:  data['textfile']
        // });
      }
      makeError() {
        this.httpExamplesService.makeIntentionalError()
          .subscribe(null, error => this.error = error );
      }
    }
  
http-examples.service.ts

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { HttpErrorResponse, HttpResponse } from '@angular/common/http';
    import { Observable, throwError } from 'rxjs';
    import { catchError, retry, tap } from 'rxjs/operators';
    import { MessageService } from '../message.service';

    export interface Config {
      apiUrl: string;
      textfile: string;
    }

    @Injectable()
    export class HttpExamplesService {
      constructor(
        private http: HttpClient,
        private messageService: MessageService
      ) { }
      getData(url: string, options_obj = {}) {
        return this.http.get(url, options_obj)
          .pipe(
            retry(3),
            tap(
              data => data,
              error => this.handleError(error)
            )
          );
        // Observable of Config
        // configUrl = 'assets/config.json';
        // return this.http.get<Config>(this.configUrl)
        // .pipe(
        //   retry(3), // retry a failed request up to 3 times
        //   catchError(this.handleError) // then handle the error
        // );
        // return this.http.get(this.configUrl);
      }
      getConfigDataResponse(url: string): Observable<HttpResponse<Config>> {
        return this.http.get<Config>(
          url,
          { observe: 'response' }
        );
      }
      makeIntentionalError() {
        return this.http.get('not/a/real/url')
          .pipe(
            tap(
              data => data,
              error => this.handleError(error)
            )
          );
      }
      private handleError(error: HttpErrorResponse) {
        if (error.error instanceof ErrorEvent) {
          // client-side or network error occurred
          this.log('An error occurred:'+ error.error.message);
        } else {
          // backend returned an unsuccessful response
          this.log(
            `Backend returned code ${error.status}`);
            // , body was: ${error.error}
        }
        // return an observable with a user-facing error message
        return throwError(
          'Something bad happened; please try again later.');
      };
      private log(data: string) {
        this.messageService.add(data);
      }
    }
  
uploader.service.ts

    import { Injectable } from '@angular/core';
    import {
      HttpClient, HttpEvent, HttpEventType, HttpProgressEvent,
      HttpRequest, HttpResponse, HttpErrorResponse
    } from '@angular/common/http';
    import { of } from 'rxjs';
    import { catchError, last, map, tap } from 'rxjs/operators';
    import { MessageService } from '../message.service';
    @Injectable()
    export class UploaderService {
      constructor(
        private http: HttpClient,
        private messenger: MessageService) {}
      // If uploading multiple files, change to:
      // upload(files: FileList) {
      //   const formData = new FormData();
      //   files.forEach(f => formData.append(f.name, f));
      //   new HttpRequest('POST', '/upload/file', formData, {reportProgress: true});
      //   ...
      // }
      upload(file: File) {
        if (!file) { return; }

        // COULD HAVE WRITTEN:
        // return this.http.post('/upload/file', file, {
        //   reportProgress: true,
        //   observe: 'events'
        // }).pipe(
        const req = new HttpRequest('POST', '/upload/file', file, {
          reportProgress: true
        });
        // The `HttpClient.request` API produces a raw event stream
        // which includes start (sent), progress, and response events.
        return this.http.request(req).pipe(
          map(event => this.getEventMessage(event, file)),
          tap(message => this.showProgress(message)),
          last(), // return last (completed) message to caller
          catchError(this.handleError(file))
        );
      }
      /** Return distinct message for sent, upload progress, & response events */
      private getEventMessage(event: HttpEvent<any>, file: File) {
        switch (event.type) {
          case HttpEventType.Sent:
            return `Uploading file "${file.name}" of size ${file.size}.`;
          case HttpEventType.UploadProgress:
            // Compute and show the % done:
            const percentDone = Math.round(100 * event.loaded / event.total);
            return `File "${file.name}" is ${percentDone}% uploaded.`;
          case HttpEventType.Response:
            return `File "${file.name}" was completely uploaded!`;
          default:
            return `File "${file.name}" surprising upload event: ${event.type}.`;
        }
      }
      /**
        * Returns a function that handles Http upload failures.
        * @param file - File object for file being uploaded
        *
        * When no `UploadInterceptor` and no server,
        * you'll end up here in the error handler.
        */
      private handleError(file: File) {
        const userMessage = `${file.name} upload failed.`;
        return (error: HttpErrorResponse) => {
          // TODO: send the error to remote logging infrastructure
          console.error(error); // log to console instead
          const message = (error.error instanceof Error) ?
            error.error.message :
            `server returned code ${error.status} with body "${error.error}"`;
          this.messenger.add(`${userMessage} ${message}`);
          // Let app keep running but indicate failure.
          return of(userMessage);
        };
      }
      private showProgress(message: string) {
        this.messenger.add(message);
      }
    }
  
auth-interceptor.ts | auth.service.ts

    import { Injectable } from '@angular/core';
    import {
      HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
    } from '@angular/common/http';
    import { AuthService } from '../auth.service';
    @Injectable()
    export class AuthInterceptor implements HttpInterceptor {
      constructor(private auth: AuthService) {}
      intercept(req: HttpRequest<any>, next: HttpHandler) {
        const authToken = this.auth.getAuthorizationToken();
        // // Verbose way: clone the request and replace the original headers
        // // with cloned headers, updated with the authorization
        // const authReq = req.clone({
        //   headers: req.headers.set('Authorization', authToken)
        // });
        // clone the request and set the new header in one step
        const authReq = req.clone({ setHeaders: { Authorization: authToken } });
        return next.handle(authReq); // send cloned request with header to the next handler
      }
    }

    // auth.service.ts
    import { Injectable } from '@angular/core';
    @Injectable()
    export class AuthService {
      getAuthorizationToken() {
        return 'some-auth-token';
      }
    }
  
caching-interceptor.ts | request-cache.service.ts

    import { Injectable } from '@angular/core';
    import {
      HttpEvent, HttpHeaders, HttpRequest, HttpResponse,
      HttpInterceptor, HttpHandler
    } from '@angular/common/http';
    import { Observable, of } from 'rxjs';
    import { startWith, tap } from 'rxjs/operators';
    import { RequestCache } from '../request-cache.service';

    import { searchUrl } from '../package-search/package-search.service';
    // searchUrl = 'https://npmsearch.com/query';

    // return cachable (e.g., search) response as observable,
    // also re-run search if has 'x-refresh' header that is true,
    // using response from next(),
    // returning an observable that emits the cached response first.
    // if not in cache or not cachable, pass request through to next()
    @Injectable()
    export class CachingInterceptor implements HttpInterceptor {
      constructor(private cache: RequestCache) {}
      intercept(req: HttpRequest<any>, next: HttpHandler) {
        // continue if not cachable.
        if (!isCachable(req)) { return next.handle(req); }
        const cachedResponse = this.cache.get(req);
        // cache-then-refresh
        if (req.headers.get('x-refresh')) {
          const results$ = sendRequest(req, next, this.cache);
          return cachedResponse ?
            results$.pipe( startWith(cachedResponse) ) :
            results$;
        }
        // cache-or-fetch
        return cachedResponse ?
          of(cachedResponse) : sendRequest(req, next, this.cache);
      }
    }
    // Only GET requests and npm package search are cachable
    function isCachable(req: HttpRequest<any>) {
      return req.method === 'GET' && -1 < req.url.indexOf(searchUrl);
    }
    // get server response observable by sending request to `next()`
    // adds the response to the cache on the way out
    function sendRequest(
      req: HttpRequest<any>,
      next: HttpHandler,
      cache: RequestCache
    ): Observable<HttpEvent<any>> {
      // No headers allowed in npm search request
      const noHeaderReq = req.clone({ headers: new HttpHeaders() });
      return next.handle(noHeaderReq).pipe(
        tap(event => {
          // There may be other events besides the response.
          if (event instanceof HttpResponse) {
            cache.put(req, event); // Update the cache.
          }
        })
      );
    }

    // request-cache.service.ts
    import { Injectable } from '@angular/core';
    import { HttpRequest, HttpResponse } from '@angular/common/http';
    import { MessageService } from './message.service';
    export interface RequestCacheEntry {
      url: string;
      response: HttpResponse<any>;
      lastRead: number;
    }
    export abstract class RequestCache {
      abstract get(req: HttpRequest<any>): HttpResponse<any> | undefined;
      abstract put(req: HttpRequest<any>, response: HttpResponse<any>): void
    }
    const maxAge = 30000; // maximum cache age (ms)
    @Injectable()
    export class RequestCacheWithMap implements RequestCache {
      cache = new Map<string, RequestCacheEntry>();
      constructor(private messenger: MessageService) { }
      get(req: HttpRequest<any>): HttpResponse<any> | undefined {
        const url = req.urlWithParams;
        const cached = this.cache.get(url);
        if (!cached) { return undefined; }
        const isExpired = cached.lastRead < (Date.now() - maxAge);
        const expired = isExpired ? 'expired ' : '';
        this.messenger.add(`Found ${expired}cached response for "${url}".`);
        return isExpired ? undefined : cached.response;
      }
      put(req: HttpRequest<any>, response: HttpResponse<any>): void {
        const url = req.urlWithParams;
        this.messenger.add(`Caching response from "${url}".`);
        const entry = { url, response, lastRead: Date.now() };
        this.cache.set(url, entry);
        // remove expired cache entries
        const expired = Date.now() - maxAge;
        this.cache.forEach(entry => {
          if (entry.lastRead < expired) {
            this.cache.delete(entry.url);
          }
        });
        this.messenger.add(`Request cache size: ${this.cache.size}.`);
      }
    }
  
ensure-https-interceptor.ts

    import { Injectable } from '@angular/core';
    import {
      HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
    } from '@angular/common/http';
    import { Observable } from 'rxjs';
    @Injectable()
    export class EnsureHttpsInterceptor implements HttpInterceptor {
      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        // clone request and replace 'http://' with 'https://' at the same time
        const secureReq = req.clone({
          url: req.url.replace('http://', 'https://')
        });
        return next.handle(secureReq); // send cloned, "secure" request to the next handler
      }
    }
  
logging-interceptor.ts

    import { Injectable } from '@angular/core';
    import {
      HttpEvent, HttpInterceptor, HttpHandler,
      HttpRequest, HttpResponse
    } from '@angular/common/http';
    import { finalize, tap } from 'rxjs/operators';
    import { MessageService } from '../message.service';
    @Injectable()
    export class LoggingInterceptor implements HttpInterceptor {
      constructor(private messenger: MessageService) {}
      intercept(req: HttpRequest<any>, next: HttpHandler) {
        const started = Date.now();
        let ok: string;
        // extend server response observable with logging
        return next.handle(req)
          .pipe(
            tap(
              // Succeeds when there is a response; ignore other events
              event => ok = event instanceof HttpResponse ? 'succeeded' : '',
              // Operation failed; error is an HttpErrorResponse
              error => ok = 'failed'
            ),
            // Log when response observable either completes or errors
            finalize(() => {
              const elapsed = Date.now() - started;
              const msg = `${req.method} "${req.urlWithParams}"
                  ${ok} in ${elapsed} ms.`;
              this.messenger.add(msg);
            })
          );
      }
    }
  
trim-name-interceptor.ts

    import { Injectable } from '@angular/core';
    import {
      HttpEvent, HttpInterceptor, HttpHandler, HttpRequest
    } from '@angular/common/http';
    import { Observable } from 'rxjs';
    @Injectable()
    export class TrimNameInterceptor implements HttpInterceptor {
      intercept(
        req: HttpRequest<any>,
        next: HttpHandler
      ): Observable<HttpEvent<any>> {
        const body = req.body;
        if (!body || !body.name ) { return next.handle(req); }
        // copy the body and trim whitespace from the name property
        const newBody = { ...body, name: body.name.trim() };
        // clone request and set its body
        const newReq = req.clone({ body: newBody });
        // send the cloned request to the next handler.
        return next.handle(newReq);
      }
    }
  
upload-interceptor.ts - simulate server replying to file upload request

    import { Injectable } from '@angular/core';
    import {
      HttpEvent, HttpInterceptor, HttpHandler,
      HttpRequest, HttpResponse,
      HttpEventType, HttpProgressEvent
    } from '@angular/common/http';
    import { Observable } from 'rxjs';
    @Injectable()
    export class UploadInterceptor implements HttpInterceptor {
      intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (req.url.indexOf('/upload/file') === -1) {
          return next.handle(req);
        }
        const delay = 300; // TODO: inject delay?
        return createUploadEvents(delay);
      }
    }
    // simulation of upload event stream
    function createUploadEvents(delay: number) {
      // Simulate XHR behavior which would provide this information in a ProgressEvent
      const chunks = 5;
      const total = 12345678;
      const chunkSize = Math.ceil(total / chunks);
      return new Observable<HttpEvent<any>>(observer => {
        // notify the event stream that the request was sent.
        observer.next({type: HttpEventType.Sent});
        uploadLoop(0);
        function uploadLoop(loaded: number) {
          // N.B.: Cannot use setInterval or rxjs delay (which uses setInterval)
          // because e2e test won't complete. A zone thing?
          // Use setTimeout and tail recursion instead.
            setTimeout(() => {
              loaded += chunkSize;
              if (loaded >= total) {
                const doneResponse = new HttpResponse({
                  status: 201, // OK but no body;
                });
                observer.next(doneResponse);
                observer.complete();
                return;
              }
              const progressEvent: HttpProgressEvent = {
                type: HttpEventType.UploadProgress,
                loaded,
                total
              };
              observer.next(progressEvent);
              uploadLoop(loaded);
            }, delay);
        }
      });
    }
  
HttpClient

    get(
      url: string,
      options: {
        headers?: HttpHeaders | { [header: string]: string | string[]; };
        observe?: HttpObserve;
        params?: HttpParams | { [param: string]: string | string[]; };
        reportProgress?: boolean;
        responseType?: "arraybuffer" | "blob" | "text" | "json";
        withCredentials?: boolean;
      } = {}
    ): Observable<any>

    post(
      url: string,
      body: any,
      options: { /* ...same */ } = {}
    ): Observable<any>

    put(
      url: string,
      body: any,
      options: { /* ...same */ } = {}
    ): Observable<any>

    patch(
      url: string,
      body: any,
      options: { /* ...same */ } = {}
    ): Observable<any>
    /*
    patchHero (id: number, heroName: string): Observable<{}> {
      const url = `${this.heroesUrl}/${id}`;   // PATCH api/heroes/42
      return this.httpClient.patch(url, {name: heroName}, httpOptions)
        .pipe(catchError(this.handleError('patchHero')));
    }
    */

    delete(
      url: string,
      options: { /* ...same */ } = {}
    ): Observable<any>

    request(
      method | req: string | HttpRequest<any>,
      url?: string,
      options: { body?: any; /* ...same */ } = {}
    ): Observable<any>
    /*
    searchHeroes(term: string): observable<Hero[]>{
      const params = new HttpParams({fromString: 'name=term'});
      return this.httpClient.request(
        'GET',
        this.heroesUrl,
        {responseType:'json', params}
      );
    }
    */

    options(
      url: string,
      options: { /* ...same */ } = {}
    ): Observable<any>

    head(
      url: string,
      options: { /* ...same */ } = {}
    ): Observable<any>

    jsonp<T>(
      url: string,
      callbackParam: string
    ): Observable<T>
    /*
    requestJsonp(url, callback = 'callback') {
      return this.httpClient.jsonp(this.heroesURL, callback);
    }
    */
  
HttpParams, class is immutable - all mutation operations return a new instance

    //constructor(options: HttpParamsOptions = {} as HttpParamsOptions)

    // whether the body has one or more values for the given parameter name
    has(param: string): boolean
    // first value for the given parameter name, or null if its not present
    get(param: string): string | null
    // all values for the given parameter name, or null if its not present
    getAll(param: string): string[] | null
    // all the parameter names for this body
    keys(): string[]
    // construct a new body with an appended value for the given parameter name
    append(param: string, value: string): HttpParams
    // construct a new body with a new value for the given parameter name
    set(param: string, value: string): HttpParams
    // construct a new body with either the given value for the given parameter removed,
    // if a value is given, or all values for the given parameter removed if not
    delete(param: string, value?: string): HttpParams
    // serialize the body to an encoded string,
    // where key-value pairs (separated by =) are separated by &s
    toString(): string
  
HttpHeaders

    // constructor(headers?: string | { [name: string]: string | string[]; })

    // checks for existence of a header by a given name
    has(name: string): boolean
    // returns the first header value that matches a given name
    get(name: string): string | null
    // returns the names of the headers
    keys(): string[]
    // returns a list of header values for a given header name
    getAll(name: string): string[] | null
    // appends a new header value to the existing set of header values
    append(name: string, value: string | string[]): HttpHeaders
    // sets a header value for a given name
    // value is replaced with the given value if the header name already exists
    set(name: string, value: string | string[]): HttpHeaders
    // deletes all header values for a given name
    delete(name: string, value?: string | string[]): HttpHeaders
  
HttpResponseBase, HttpResponse and HttpHeaderResponse

    /*
    constructor(
      init: {
        headers?: HttpHeaders;
        status?: number;
        statusText?: string;
        url?: string;
      },
      defaultStatus: number = 200,
      defaultStatusText: string = 'OK'
    )
    */
    headers: HttpHeaders
    status: number
    statusText: string
    url: string | null
    ok: boolean
    type: HttpEventType.Response | HttpEventType.ResponseHeader

    // HttpResponse
    /*
    constructor(
      init: {
        body?: T;
        headers?: HttpHeaders;
        status?: number;
        statusText?: string;
        url?: string;
      } = {}
    )
    */
    body: T | null
    type: HttpEventType.Response
    clone(
      update: {
        body?: any;
        headers?: HttpHeaders;
        status?: number;
        statusText?: string;
        url?: string;
      } = {}
    ): HttpResponse<any>
    // then, inherited from common/http/HttpResponseBase...

    // HttpHeaderResponse
    /*
    constructor(
      init: {
        headers?: HttpHeaders;
        status?: number;
        statusText?: string;
        url?: string;
      } = {}
    )
    */
    type: HttpEventType.ResponseHeader
    // copy this HttpHeaderResponse, overriding its contents with the given parameter hash
    clone(
      update: {
        headers?: HttpHeaders;
        status?: number;
        statusText?: string;
        url?: string;
      } = {}
    ): HttpHeaderResponse
    // then, inherited from common/http/HttpResponseBase...
  

Routing


    const routes: Routes = [
      { path: '', component: HomeComponent },
      { path: 'path/:routeParam', component: MyComponent },
      { path: 'staticPath', component: ... },
      { path: '**', component: ... },
      { path: 'oldPath', redirectTo: '/staticPath' },
      { path: ..., component: ..., data: { message: 'Custom' } }
    ];

    const crisisCenterRoutes: Routes = [{
      path: 'crisis-center',
      title: 'Crisis Center',
      component: CrisisCenterComponent,
      // display following in component outlet not in root
      children: [
        {
          path: '',
          component: CrisisListComponent,
          children: [
            ...
      ]}]
    }];

    this.router.navigate([
      '../',
      { id: crisisId, foo: 'foo' }
    ], { relativeTo: this.route });

    // --- lazy loading a standalone component
    export const ROUTES: Route[] = [
      {
        path: 'admin',
        loadComponent: () => import('./admin/panel.component').then(mod => mod.AdminPanelComponent)},
        // omit ".then" for "default" exports
        loadComponent: () => import('./admin/panel.component')},
        // ...
    ];
    // many routes at once, main application:
    export const ROUTES: Route[] = [
      {path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
      // omit ".then" for "default" exports
      {path: 'admin', loadChildren: () => import('./admin/routes')},
      // ...
    ];
    // admin/routes.ts:
    export const ADMIN_ROUTES: Route[] = [
      {path: 'home', component: AdminHomeComponent},
      {path: 'users', component: AdminUsersComponent},
      // ...
    ];
    // lazy loading and default exports, main application:
    export const ROUTES: Route[] = [
      {path: 'admin', loadChildren: () => import('./admin/routes')},
      // ...
    ];
    // admin/routes.ts:
    export default [
      {path: 'home', component: AdminHomeComponent},
      {path: 'users', component: AdminUsersComponent},
      // ...
    ] as Route[];

    // --- specifying additional providers on a Route
    // allows this same scoping without the need for either lazy loading or NgModule
    export const ROUTES: Route[] = [
      {
        path: 'admin',
        providers: [
          AdminService,
          {provide: ADMIN_API_KEY, useValue: '12345'},
        ],
        children: [
          path: 'users', component: AdminUsersComponent,
          path: 'teams', component: AdminTeamsComponent,
        ],
      },
      // ... other application routes that don't
      //     have access to ADMIN_API_KEY or AdminService.
    ];
    // combine providers with loadChildren of additional routing configuration
    // same effect of lazy loading an NgModule with additional routes and route-level providers
    // configure the same providers/child routes as above, but behind a lazy loaded boundary:
    // main application:
    export const ROUTES: Route[] = {
      // Lazy-load the admin routes.
      {path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)},
      // ... rest of the routes
    }
    // admin/routes.ts:
    export const ADMIN_ROUTES: Route[] = [{
      path: '',
      pathMatch: 'prefix',
      providers: [
        AdminService,
        {provide: ADMIN_API_KEY, useValue: 12345},
      ],
      children: [
        {path: 'users', component: AdminUsersCmp},
        {path: 'teams', component: AdminTeamsCmp},
      ],
    }];
  

Router


    interface Route {
      // path to match against, default is "/" (the root path)
      path?: string
      // path-matching strategy
      // "prefix"(default) OR "full" - important when redirecting empty-path routes
      pathMatch?: 'prefix' | 'full'
      // URL-matching function as a custom strategy for path matching
      // supersedes "path" and "pathMatch"
      matcher?: UrlMatcher
      // component to instantiate when the path matches
      // can be empty if child routes specify components
      component?: Type<any>
      // URL to which to redirect when a the path matches
      // absolute if the URL begins with a slash (/),
      // otherwise relative to the path URL
      redirectTo?: string
      // RouterOutlet object where the component can be placed
      outlet?: string
      // developer-defined data provided to the component via ActivatedRoute
      data?: Data
      // map of DI tokens used to look up data resolvers
      resolve?: ResolveData
      // array of child Route objects that specifies a nested route configuration
      children?: Routes
      // lazy-loaded child routes
      loadChildren?: LoadChildren
      // defines when guards and resolvers will be run
      // by default, run only when the matrix parameters of the route change
      // paramsOrQueryParamsChange - run when query parameters change
      // always - run on every execution
      runGuardsAndResolvers?: RunGuardsAndResolvers
      // handlers tokens
      canActivate?: any[]
      canActivateChild?: any[]
      canDeactivate?: any[]
      canLoad?: any[]
    }

    router.resetConfig([
      { path: 'team/:id', component: TeamCmp, children: [
        { path: 'simple', component: SimpleCmp },
        { path: 'user/:name', component: UserCmp }
      ]}
    ]);

    // create /team/33/user/11
    router.createUrlTree(['/team', 33, 'user', 11]);
    // create /team/33;expand=true/user/11
    router.createUrlTree(['/team', 33, {expand: true}, 'user', 11]);
    // you can collapse static segments like this
    // works only with the first passed-in value:
    router.createUrlTree(['/team/33/user', userId]);
    // If the first segment can contain slashes,
    // and you do not want the router to split it, you
    // can do the following:
    router.createUrlTree([{segmentPath: '/one/two'}]);
    // create /team/33/(user/11//right:chat)
    router.createUrlTree(['/team', 33, {outlets: {primary: 'user/11', right: 'chat'}}]);
    // remove the right secondary node
    router.createUrlTree(['/team', 33, {outlets: {primary: 'user/11', right: null}}]);
    // assuming the current url is `/team/33/user/11` and the route points to `user/11`
    // navigate to /team/33/user/11/details
    router.createUrlTree(['details'], {relativeTo: route});
    // navigate to /team/33/user/22
    router.createUrlTree(['../22'], {relativeTo: route});
    // navigate to /team/44/user/22
    router.createUrlTree(['../../team/44/user/22'], {relativeTo: route});

    router.navigateByUrl("/team/33/user/11");
    // Navigate without updating the URL
    router.navigateByUrl("/team/33/user/11", { skipLocationChange: true });

    router.navigate(['team', 33, 'user', 11], {relativeTo: route});
    // Navigate without updating the URL
    router.navigate(['team', 33, 'user', 11], {relativeTo: route, skipLocationChange: true});
  

Routes examples


    // Simple Configuration
    // for /team/11/user/bob
    // create the team component with the user component in it
    [{
      path: 'team/:id',
      component: Team,
      children: [{
        path: 'user/:name',
        component: User
      }]
    }]

    // Multiple Outlets
    // for /team/11(aux:chat/jim)
    // create the team component next to the chat component, placed into the aux outlet
    [{
      path: 'team/:id',
      component: Team
    }, {
      path: 'chat/:user',
      component: Chat
      outlet: 'aux'
    }]

    // Wild Cards
    [{
      path: '**',
      component: Sink
    }]

    // Redirects
    // for '/team/11/legacy/user/jim'
    // change the url to '/team/11/user/jim'
    // and then instantiate the team component with the user component in it.
    // if the redirectTo value starts with a '/', then it is an absolute redirect:
    // if we change the redirectTo to /user/:name, the result url will be '/user/jim'
    [{
      path: 'team/:id',
      component: Team,
      children: [{
        path: 'legacy/user/:name',
        redirectTo: 'user/:name'
      }, {
        path: 'user/:name',
        component: User
      }]
    }]

    // Empty Path
    // for team/11, instantiate the AllUsers component
    [{
      path: 'team/:id',
      component: Team,
      children: [{
        path: '',
        component: AllUsers
      }, {
        path: 'user/:name',
        component: User
      }]
    }]
    // for /team/11/user/jim
    // instantiate the wrapper component with the user component in it
    [{
      path: 'team/:id',
      component: Team,
      children: [{
        path: '',
        component: WrapperCmp,
        children: [{
          path: 'user/:name',
          component: User
        }]
      }]
    }]

    // Matching Strategy
    // change matching strategy to make sure that path covers the whole unconsumed url
    // even when navigating to '/main', the router will still apply the redirect
    [{
      path: '',
      pathMatch: 'prefix', //default
      redirectTo: 'main'
    }, {
      path: 'main',
      component: Main
    }]
    // if pathMatch: full is provided
    // router will apply the redirect if and only if navigating to '/'
    [{
      path: '',
      pathMatch: 'full',
      redirectTo: 'main'
    }, {
      path: 'main',
      component: Main
    }]

    // Componentless Routes
    // two components require some id parameter
    // for parent/10/(a//aux:b)
    // instantiate the main child and aux child components next to each other
    // router will also merge the params, data, and resolve of the componentless parent
    // into the params, data, and resolve of the children
    [{
      path: 'parent/:id',
      children: [
        { path: 'a', component: MainChild },
        { path: 'b', component: AuxChild, outlet: 'aux' }
      ]
    }]
    // also merges the 'params', 'data', and 'resolve'
    // of the componentless parent into the childrens
    // done because there is no component
    // that can inject the activated route of the componentless parent.
    // '/parent/10' will create the main child and aux components
    [{
      path: 'parent/:id',
      children: [
        { path: '', component: MainChild },
        { path: '', component: AuxChild, outlet: 'aux' }
      ]
    }]

    // Lazy Loading
    // fetch an NgModule associated with 'team'
    // extract the set of routes defined in that NgModule
    // transparently add those routes to the main configuration
    // using an ES dynamic import() expression
    [{
      path: 'lazy',
      loadChildren: () => import('./lazy-route/lazy.module').then(mod => mod.LazyModule),
      // omit ".then" for "default" exports
      loadChildren: () => import('./lazy-route/lazy.module'),
    }];

    // --- REUSE COMMON LAYOUTS
    const routes: Routes = [{
        path: '',
        redirectTo: '/dashboard',
        pathMatch: 'full'
      },{
        path: '',
        component: MainLayoutComponent,
        children: [
          {
            path: 'dashboard',
            loadChildren: () => import(
              './dashboard/dashboard.module'
            ).then(mod => mod.DashboardModule)
          },{
            path: 'users',
            loadChildren: () => import(
              './users/users.module'
            ).then(mod => mod.UsersModule)
          },{
            path: 'account-settings',
            loadChildren: () => import(
              './account-settings/account-settings.module'
            ).then(mod => mod.AccountSettingsModule)
          },
        ]
      },{
        path: '',
        component: FooterOnlyLayoutComponent,
        children: [
          {
            path: 'login',
            loadChildren: () => import(
              './login/login.module'
            ).then(mod => mod.LoginModule)
          },{
            path: 'registration',
            loadChildren: () => import(
              './registration/registration.module'
            ).then(mod => mod.RegistrationModule)
          }
        ]
    }];
    // --- main-layout
    <app-header></app-header>
    <div>
      <app-sidebar></app-sidebar>
      <div class="content">
        <router-outlet></router-outlet>
      </div>
    </div>
    <app-footer></app-footer>
    // --- footer-only-layout
    <div class="content">
      <router-outlet></router-outlet>
    </div>
    <app-footer></app-footer>
  

RouterLink examples


    <a routerLink="/path">
    <a [routerLink]="[ '/path', routeParam ]">
    <a [routerLink]="[ '/path', { matrixParam: 'value' } ]">
    <a [routerLink]="[ '/path' ]" [queryParams]="{ page: 1 }">
    <a [routerLink]="[ '/path' ]" fragment="anchor">

    <!-- /team/11/user/bob;details=true -->
    <a routerLink="['/team', teamId, 'user', userName, {details: true}]"></a>
    <a routerLink="['/team/11/user', userName, {details: true}]"></a>

    <!-- /user/bob#education?debug=true -->
    <a
      [routerLink]="['/user/bob']"
      [queryParams]="{debug: true}"
      fragment="education"
    ></a>

    <!--
      preserve the current query params(?*=*&...) and fragment(#)
      queryParamsHandling:
        merge - merge the queryParams into the current queryParams
        preserve - preserve the current queryParams
        default/'' - use the queryParams only
    -->
    <a
      [routerLink]="['/user/bob']"
      queryParamsHandling='preserve'
      preserveFragment
    ></a>

    <!--
      provide a state value
      to be persisted to the browser History.state property
      later the value can be read
      from the router through router.getCurrentNavigation:
      // Get NavigationStart events
      router.events.pipe(
        filter(e => e instanceof NavigationStart)
      ).subscribe(e => {
        const navigation = router.getCurrentNavigation();
        tracingService.trace({id: navigation.extras.state.tracingId});
      });
    -->
    <a
      [routerLink]="['/user/bob']"
      [state]="{tracingId: 123}"
    ></a>
  

Guards examples


    @Injectable({ providedIn: 'root' })
    export class MyGuardWithDependency implements CanActivate {
      constructor(private loginService: LoginService) {}
      canActivate() {
        return this.loginService.isLoggedIn();
      }
    }
    const route = {
      path: 'somePath',
      canActivate: [MyGuardWithDependency]
    };
    // refactored to:
    const route = {
      path: 'admin',
      canActivate: [() => inject(LoginService).isLoggedIn()]
    };
    // function-based guard:
    export const authGuard: CanActivateFn {
      const loginService = inject(LoginService);
      const router = inject(Router);
      if (loginService.isLoggedIn) {
        return true;
      }
      return router.parseUrl('/login'); // redirect
    }

    // ---

    it('can run functional guards serially', fakeAsync(() => {
      function runSerially(guards: CanActivateFn[]|CanActivateChildFn[]): CanActivateFn|CanActivateChildFn {
        return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
          const injector = coreInject(EnvironmentInjector);
          const observables = guards.map(guard => {
            const guardResult = injector.runInContext(() => guard(route, state));
            return wrapIntoObservable(guardResult).pipe(first());
          });
          return concat(...observables).pipe(takeWhile(v => v === true), last());
        };
      }

      const guardDone: string[] = [];

      const guard1: CanActivateFn = () =>
          of(true).pipe(delay(100), tap(() => guardDone.push('guard1')));
      const guard2: CanActivateFn = () => of(true).pipe(tap(() => guardDone.push('guard2')));
      const guard3: CanActivateFn = () =>
          of(true).pipe(delay(50), tap(() => guardDone.push('guard3')));
      const guard4: CanActivateFn = () =>
          of(true).pipe(delay(200), tap(() => guardDone.push('guard4')));
      const router = TestBed.inject(Router);
      router.resetConfig([{
        path: '**',
        component: BlankCmp,
        canActivate: [runSerially([guard1, guard2, guard3, guard4])]
      }]);
      router.navigateByUrl('');
      tick(100);
      expect(guardDone).toEqual(['guard1', 'guard2']);
      tick(50);
      expect(guardDone).toEqual(['guard1', 'guard2', 'guard3']);
      tick(200);
      expect(guardDone).toEqual(['guard1', 'guard2', 'guard3', 'guard4']);
    }));
  
Router Part Meaning
Router Displays the application component for the active URL. Manages navigation from one component to the next.
RouterModule A separate NgModule that provides the necessary service providers and directives for navigating through application views.
Routes Defines an array of Routes, each mapping a URL path to a component.
Route Defines how the router should navigate to a component based on a URL pattern. Most routes consist of a path and a component type.
RouterOutlet The directive (<router-outlet>) that marks where the router displays a view.
RouterLink The directive for binding a clickable HTML element to a route, clicking an element with a routerLink directive that is bound to a string or a link parameters array triggers a navigation.
RouterLinkActive The directive for adding/removing classes from an HTML element when an associated routerLink contained on or inside the element becomes active/inactive.
ActivatedRoute A service that is provided to each route component that contains route specific information such as route parameters, static data, resolve data, global query params, and the global fragment.
RouterState The current state of the router including a tree of the currently activated routes together with convenience methods for traversing the route tree.
Link parameters array An array that the router interprets as a routing instruction, you can bind that array to a RouterLink or pass the array as an argument to the Router.navigate method.
Routing component An Angular component with a RouterOutlet that displays views based on router navigations.

Animations


    // 1 - enable animations modules
    import { BrowserModule } from '@angular/platform-browser';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    // ...
    @NgModule({
      imports: [
        BrowserModule,
        BrowserAnimationsModule
      ],
      declarations: [ ],
      bootstrap: [ ]
    })
    export class AppModule { }

    // 2 - importing animation functions into component files
    import { Component, HostBinding } from '@angular/core';
    import {
      trigger,
      state,
      style,
      animate,
      transition,
      // ...
    } from '@angular/animations';

    // 3 - adding the animation metadata property
    @Component({
      selector: 'app-root',
      templateUrl: 'app.component.html',
      styleUrls: ['app.component.css'],
      animations: [
        // animation triggers go here
      ]
    })
  

app/animations/animations.component.ts


    import {
      Component, OnInit, HostBinding, EventEmitter
    } from '@angular/core';
    import {
      state, style, trigger,
      animate, transition, group,
      query, stagger, keyframes,
      useAnimation,
      AnimationEvent
    } from '@angular/animations';
    import {
      transOnAnimation, transOffAnimation
    } from '../animations';
    import { NOTES } from '../notes/mock-notes';

    @Component({
      selector: 'app-animations',
      templateUrl: './animations.component.html',
      styleUrls: ['./animations.component.css'],
      animations: [

        trigger('openClose', [
          state('true', style({
            height: '8em',
            opacity: 1,
            backgroundColor: 'yellow'
          })),
          state('false', style({
            height: '4em',
            opacity: 0.5,
            backgroundColor: 'green'
          })),
          transition('false <=> true', [
            animate('1s'
              // , keyframes ( [
              //   style({ opacity: 0.1, offset: 0.1 }),
              //   style({ opacity: 0.6, offset: 0.2 }),
              //   style({ opacity: 1,   offset: 0.5 }),
              //   style({ opacity: 0.2, offset: 0.7 })
              // ])
            )
          ]),
        ]),

        trigger('importedOpenClose', [
          transition('false => true', [
            useAnimation(transOnAnimation)
          ]),
          transition('true => false', [
            useAnimation(transOffAnimation, {
              params: {
                time: '.2s'
              }
            })
          ])
        ]),

        trigger('openClose2', [
          state('open2', style({
            height: '200px',
            // opacity: 1,
            backgroundColor: 'yellow'
          })),
          state('closed2', style({
            height: '100px',
            // opacity: 0.5,
            backgroundColor: 'green'
          })),
          transition('open2 => closed2', [
            animate('1s')
          ]),
          transition('closed2 => open2', [
            animate('0.5s')
          ]),

          transition('* => closed2', [
            animate('1s')
          ]),
          transition('* => open2', [
            animate('0.5s')
          ]),

          transition('open2 <=> closed2', [
            animate('0.5s')
          ]),
          transition ('* => open2', [
            animate ('1s',
              style ({ opacity: '*' }),
            ),
          ]),
          transition('* => *', [
            animate('1s')
          ]),
        ]),

        trigger('enterLeaveTrigger', [
          transition(':enter', [
            style({ opacity: 0 }),
            animate('0.5s', style({ opacity: 1 })),
          ]),
          transition(':leave', [
            animate('0.5s', style({ opacity: 0 }))
          ])
        ]),

        trigger('shrinkOut', [
          state('in', style({ height: '*' })),
          transition('* => void', [
            style({ height: '*' }),
            animate(250, style({ height: 0 }))
          ])
        ]),

        trigger('kfSample', [
          state('inactive', style({ backgroundColor: 'blue' })),
          state('active', style({ backgroundColor: 'orange' })),
          transition('* => active', [
            animate('2s', keyframes([
              style({ backgroundColor: 'blue', offset: 0}),
              style({ backgroundColor: 'red', offset: 0.8}),
              style({ backgroundColor: 'orange', offset: 1.0})
            ])),
          ]),
          transition('* => inactive', [
            animate('2s', keyframes([
              style({ backgroundColor: 'orange', offset: 0}),
              style({ backgroundColor: 'red', offset: 0.2}),
              style({ backgroundColor: 'blue', offset: 1.0})
            ]))
          ]),
          transition('* => active', [
            animate('2s', keyframes([
              style({ backgroundColor: 'blue' }),
              style({ backgroundColor: 'red' }),
              style({ backgroundColor: 'orange' })
            ]))
          ]),
        ]),

        trigger('enterPageAnimations', [
          transition(':enter', [
            query('.note, form', [
              // invisible and use transform to move it out of position
              style({opacity: 0, transform: 'translateY(-100px)'}),
              // delay each animation by 30 milliseconds
              stagger(-30, [
                // animate each element on screen for 0.5 seconds
                // using a custom-defined easing curve,
                // simultaneously fading it in and un-transforming it
                animate(
                  '500ms cubic-bezier(0.35, 0, 0.25, 1)',
                  style({ opacity: 1, transform: 'none' })
                )
              ])
            ])
          ])
        ]),
        trigger('filterAnimation', [
          transition(':enter, * => 0, * => -1', []),
          transition(':increment', [
            query(':enter', [
              style({ opacity: 0, width: '0px' }),
              stagger(50, [
                animate(
                  '300ms ease-out',
                  style({ opacity: 1, width: '*' })
                ),
              ]),
            ], { optional: true })
          ]),
          transition(':decrement', [
            query(':leave', [
              stagger(50, [
                animate(
                  '300ms ease-out',
                  style({ opacity: 0, width: '0px' })
                ),
              ]),
            ]/*, { optional: true } */)
          ]),
        ]),

      ],
    })
    export class AnimationsComponent implements OnInit {
      constructor() { }
      isDisabled = false;
      toggleAnimations() { this.isDisabled = !this.isDisabled; }

      isOpen = false;
      toggle() { this.isOpen = !this.isOpen; }

      isImportedOpenClose = false;
      toggleImportedOpenClose() {
        this.isImportedOpenClose = !this.isImportedOpenClose;
      }

      isOpen2 = false;
      onAnimationEvent ( event: AnimationEvent ) {
        // // openClose is trigger name in this example
        // console.warn(`Animation Trigger: ${event.triggerName}`);
        // // phaseName is start or done
        // console.warn(`Phase: ${event.phaseName}`);
        // // in our example, totalTime is 1000 or 1 second
        // console.warn(`Total time: ${event.totalTime}`);
        // // in our example, fromState is either open or closed
        // console.warn(`From: ${event.fromState}`);
        // // in our example, toState either open or closed
        // console.warn(`To: ${event.toState}`);
        // // the HTML element itself, the button in this case
        // console.warn(`Element: ${event.element}`);
      }
      toggle2() { this.isOpen2 = !this.isOpen2; }

      notes_1 = NOTES.slice();
      removeNote(id: number) {
        this.notes_1 = this.notes_1.filter(note => note.id !== id);
      }

      // insert/remove
      enterLeave = false;
      toggleEnterLeave() { this.enterLeave = !this.enterLeave; }

      // keyframes
      kfStatus: 'active' | 'inactive' = 'inactive';
      kfToggle() {
        if (this.kfStatus === 'active') { this.kfStatus = 'inactive'; }
        else { this.kfStatus = 'active'; }
      }

      // animate multiple elements filter/stagger
      @HostBinding('@enterPageAnimations')
      ngOnInit() { this.notes_2_2 = NOTES; }
      public animatePage = true;
      notes_2_2 = [];
      noteTotal = -1;
      get notes_2_1() { return this.notes_2_2; }
      updateCriteria(criteria: string) {
        criteria = criteria ? criteria.trim() : '';
        this.notes_2_2 = NOTES.filter(
          note => note.title.toLowerCase().includes(criteria.toLowerCase())
        );
        const newTotal = this.notes_2_1.length;
        if (this.noteTotal !== newTotal) {
          this.noteTotal = newTotal;
        } else if (!criteria) {
          this.noteTotal = -1;
        }
      }

    }
  

app/animations/animations.component.html


    <nav>
      <button (click)="toggleAnimations()">
        Toggle Animations
      </button>
    </nav>

    <br>
    <nav>
      <button (click)="toggle()">
        Toggle Open/Closed
      </button>
    </nav>
    <div [@.disabled]="isDisabled">
      <div [@openClose]="isOpen ? true : false"
        class="open-close-container">
        <p>openClose box is {{ isOpen ? 'Open' : 'Closed' }}!</p>
        <p *ngIf="isOpen" >additional content</p>
      </div>
    </div>

    <br>
    <nav>
      <button (click)="toggleImportedOpenClose()">
        Toggle Imported Open/Closed
      </button>
    </nav>
    <div [@.disabled]="isDisabled">
      <div [@importedOpenClose]="isImportedOpenClose ? true : false"
        class="open-close-container">
        importedOpenClose box is {{ isImportedOpenClose ? 'Open' : 'Closed' }}!
      </div>
    </div>

    <br>
    <nav>
      <button (click)="toggle2()">Toggle Open/Close 2</button>
    </nav>
    <div [@.disabled]="isDisabled">
      <div [@openClose2]="isOpen2 ? 'open2' : 'closed2'"
        (@openClose2.start)="onAnimationEvent($event)"
        (@openClose2.done)="onAnimationEvent($event)"
        class="open-close-container">
        <p>openClose2 box is {{ isOpen2 ? 'Open' : 'Closed' }}!</p>
      </div>
    </div>

    <br>
    <nav>
      <button (click)="toggleEnterLeave()">
        toggleEnterLeave()
      </button>
    </nav>
    <div
      @enterLeaveTrigger
      *ngIf="enterLeave"
      class="insert-remove-container"
    >
      <p>The box is inserted</p>
    </div>

    <br>
    <nav>
      <button (click)="kfToggle()">kfToggle()</button>
    </nav>
    <div [@kfSample]="kfStatus" class="kfBox">
      {{ kfStatus == 'active' ? 'Active' : 'Inactive' }}
    </div>

    <br>
    <div class="w_100pc">
      <div class="w_32pc">
        <ul class="notes">
          <li
            *ngFor="let note of notes_1"
            [@shrinkOut]="'in'"
            (click)="removeNote(note.id)">
              <div class="inner">
                <span class="id">{{ note.id }}</span>
                <span>{{ note.title }}</span>
              </div>
          </li>
        </ul>
      </div>
      <div class="w_32pc">
        <ul
          class="notes"
          [@filterAnimation]="noteTotal">
            <li *ngFor="let note of notes_2_2" class="note">
              <div class="inner">
                <span class="id">{{ note.id }}</span>
                <span>{{ note.title }}</span>
              </div>
            </li>
        </ul>
        <form>
        <input
          #criteria (input)="updateCriteria(criteria.value)"
          placeholder="Search Notes"
        /></form>
      </div>
    </div>
  

app/animations.ts


    import {
      trigger, animateChild, group,
      transition, animate, style, query,
      animation
    } from '@angular/animations';

    // reusable animations
    export const transOnAnimation = animation([
      style({
        height: '{{ height }}',
        opacity: '{{ opacity }}',
        backgroundColor: '{{ backgroundColor }}'
      }), animate('{{ time }}') ],
      // defaults
      {
        params: {
          height: '6em',
          opacity: 1,
          backgroundColor: 'green',
          time: '2s'
        }
      }
    );
    export const transOffAnimation = animation([
      style({
        height: '3em',
        opacity: 0.2,
        backgroundColor: 'red'
      }), animate('{{ time }}') ],
      // defaults
      {
        params: {
          time: '.5s'
        }
      }
    );

    // routable animations
    export const slideInAnimation = trigger('routeAnimation', [
      transition('notes <=> note, * <=> dashboard', [
        // host view must use relative positioning
        style({ position: 'relative' }),
        // child views must use absolute positioning
        query(':enter, :leave', [
          style({
            position: 'absolute',
            top: 0,
            left: 0,
            width: '100%',
            backgroundColor: 'white'
          })
        ]),
        query(':enter', [
          style({ left: '-100%'})
        ]),
        query(':leave', animateChild()),
        group([
          query(':leave', [
            animate(
              '300ms ease-out',
              style({ left: '100%', opacity: 0})
            )
          ]),
          query(':enter', [
            animate(
              '300ms ease-out',
              style({ left: '0%', opacity: 1})
            )
          ])
        ]),
        query(':enter', animateChild()),
      ])
    ]);
  

Back to Main Page