Security


    import { Component } from '@angular/core';
    import {
      DomSanitizer, SafeResourceUrl, SafeUrl
    } from '@angular/platform-browser';

    @Component({
      selector: 'app-security',
      templateUrl: './security.component.html',
    })
    export class SecurityComponent {
      dangerousUrl: string;
      trustedUrl: SafeUrl;
      dangerousVideoUrl: string;
      videoUrl: SafeResourceUrl;

      constructor(private sanitizer: DomSanitizer) {
        // explicitly tell Angular to trust this value:
        this.dangerousUrl = 'javascript:alert("Hi there")';
        this.trustedUrl = sanitizer.bypassSecurityTrustUrl(this.dangerousUrl);
        this.updateVideoUrl('PUBnlbjZFAI');
      }

      updateVideoUrl(id: string) {
        this.dangerousVideoUrl = 'https://www.youtube.com/embed/' + id;
        this.videoUrl =
            this.sanitizer.bypassSecurityTrustResourceUrl(this.dangerousVideoUrl);
      }

      // sanitize script tag out of content automatically
      htmlSnippet = 'Template <script>alert("0wned")</script> <b>Syntax</b>';
    }
  

    <h3>Bypass Security Component</h3>
    <h4>An untrusted URL:</h4>
    <p><a [href]="dangerousUrl">Click me</a></p>
    <h4>A trusted URL:</h4>
    <p><a [href]="trustedUrl">Click me</a></p>

    <h4>Resource URL:</h4>
    <p>Showing: {{dangerousVideoUrl}}</p>
    <p>Trusted:</p>
    <iframe width="640" height="390" [src]="videoUrl"></iframe>
    <p>Untrusted:</p>
    <iframe width="640" height="390" [src]="dangerousVideoUrl"></iframe>

    <h3>Binding innerHTML</h3>
    <p>Bound value:</p>
    <p>{{htmlSnippet}}</p>
    <p>Result of binding to innerHTML:</p>
    <p [innerHTML]="htmlSnippet"></p>
  

    abstract class DomSanitizer implements Sanitizer {
      abstract sanitize(
        // enum SecurityContext {
        //   NONE: 0
        //   HTML: 1
        //   STYLE: 2
        //   SCRIPT: 3
        //   URL: 4
        //   RESOURCE_URL: 5
        // }
        context: SecurityContext,
        value: string | SafeValue
      ): string | null
      // calling any of the bypassSecurityTrust... APIs
      // disables Angular built-in sanitization for the value passed in
      abstract bypassSecurityTrustHtml(value: string): SafeHtml
      abstract bypassSecurityTrustStyle(value: string): SafeStyle
      abstract bypassSecurityTrustScript(value: string): SafeScript
      abstract bypassSecurityTrustUrl(value: string): SafeUrl
      abstract bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl
    }
  

Internationalization (i18n)


    <h1 i18n>Hello i18n!</h1>
    <!-- description of the text message -->
    <h1 i18n="An introduction header for this sample">Hello i18n!</h1>
    <!-- <meaning>|<description> -->
    <h1 i18n="site header|An introduction header for this sample">Hello i18n!</h1>
    <!-- custom id is persistent -->
    <!-- <trans-unit id="introductionHeader" datatype="html"> -->
    <h1 i18n="@@introductionHeader">Hello i18n!</h1>
    <!-- custom id with a description -->
    <h1
      i18n="An introduction header for this sample@@introductionHeader"
    >Hello i18n!</h1>
    <!-- ... with meaning -->
    <h1
      i18n="site header|An introduction header for this sample@@introductionHeader"
    >Hello i18n!</h1>
    <!-- non-displayed HTML -->
    <ng-container i18n>I don't output any element</ng-container>

    <!-- mark an attribute for translation -->
    <!-- i18n-x="<meaning>|<description>@@<id> -->
    <img [src]="logo" i18n-title title="Angular logo" />

    <!-- plural -->
    <span i18n>
      Updated {
        minutes,  <!-- key, component property (minutes) -->
        plural,   <!-- translation type -->
        <!-- pluralization pattern consisting of -->
        <!-- pluralization categories and their matching values -->
        =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}
      }
    </span>
    <!-- select -->
    <span i18nn>
      The author is {
        gender,   <!-- key, component property (minutes) -->
        select,   <!-- translation type -->
        male {male} female {female} other {other}
      }
    </span>
    <!-- nesting plural and select -->
    <span i18nn>
      Updated: {
        minutes,
        plural,
        =0 {just now}
        =1 {one minute ago}
        other {
          {{minutes}} minutes ago by {
            gender, select, male {male} female {female} other {other}
          }
        }
      }
    </span>
  

    // --- specific build options for just one locale

    // and use "ng serve --configuration=fr" for:
    "build": {
      ...
      "configurations": {
        ...
        "fr": {
          "localize": ["fr"],
          "main": "src/main.fr.ts",
          ...
        }
      }
    },
    "serve": {
      ...
      "configurations": {
        ...
        "fr": {
          "browserTarget": "*project-name*:build:fr"
        }
      }
    }
    // OR "ng build --configuration=production,fr", to execute both configurations for:
    "architect": {
      "build": {
        "builder": "@angular-devkit/build-angular:browser",
        "options": { ... },
        "configurations": {
          "fr": {
            "localize": ["fr"],
          }
        }
      },
      ...
      "serve": {
        "builder": "@angular-devkit/build-angular:dev-server",
        "options": {
          "browserTarget": "my-project:build"
        },
        "configurations": {
          "production": {
            "browserTarget": "my-project:build:production"
          },
          "fr": {
            "browserTarget": "my-project:build:fr"
          }
        }
      }
    }


    // --- specify missing translations warning level
    angular.json
    content_copy
    "options": {
      ...
      "i18nMissingTranslation": "error" // warning | ignore
    }
  
EXAMPLE APP
   angular.json

    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "angular.io-example": {
          "projectType": "application",
          "schematics": {
            "@schematics/angular:application": {
              "strict": true
            }
          },
          "root": "",
          "sourceRoot": "src",
          "prefix": "app",
          "i18n": {
            "sourceLocale": "en-US",
            "locales": {
              "fr": "src/locale/messages.fr.xlf"
            }
          },
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "localize": true,
                "outputPath": "dist",
                "index": "src/index.html",
                "main": "src/main.ts",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "tsconfig.app.json",
                "aot": true,
                "assets": [
                  "src/favicon.ico",
                  "src/assets"
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": []
              },
              "configurations": {
                "production": {
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ],
                  "optimization": true,
                  "outputHashing": "all",
                  "sourceMap": false,
                  "namedChunks": false,
                  "extractLicenses": true,
                  "vendorChunk": false,
                  "buildOptimizer": true,
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "500kb",
                      "maximumError": "1mb"
                    },
                    {
                      "type": "anyComponentStyle",
                      "maximumWarning": "2kb",
                      "maximumError": "4kb"
                    }
                  ]
                },
                "fr": {
                  "localize": [
                    "fr"
                  ]
                }
              }
            },
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "options": {
                "browserTarget": "angular.io-example:build"
              },
              "configurations": {
                "production": {
                  "browserTarget": "angular.io-example:build:production"
                },
                "fr": {
                  "browserTarget": "angular.io-example:build:fr"
                }
              }
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n",
              "options": {
                "browserTarget": "angular.io-example:build"
              }
            },
            "test": {
              "builder": "@angular-devkit/build-angular:karma",
              "options": {
                "main": "src/test.ts",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "tsconfig.spec.json",
                "karmaConfig": "karma.conf.js",
                "assets": [
                  "src/favicon.ico",
                  "src/assets"
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": []
              }
            },
            "lint": {
              "builder": "@angular-devkit/build-angular:tslint",
              "options": {
                "tsConfig": [
                  "tsconfig.app.json",
                  "tsconfig.spec.json",
                  "e2e/tsconfig.json"
                ],
                "exclude": [
                  "**/node_modules/**"
                ]
              }
            },
            "e2e": {
              "builder": "@angular-devkit/build-angular:protractor",
              "options": {
                "protractorConfig": "e2e/protractor.conf.js",
                "devServerTarget": "angular.io-example:serve:fr"
              },
              "configurations": {
                "production": {
                  "devServerTarget": "angular.io-example:serve:production"
                }
              }
            }
          }
        }
      },
      "defaultProject": "angular.io-example"
    }
    
   e2e
   +---- protractor.conf.js

    // @ts-check
    // Protractor configuration file, see link for more information
    // https://github.com/angular/protractor/blob/master/lib/config.ts

    const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter');

    /**
    * @type { import("protractor").Config }
    */
    exports.config = {
      allScriptsTimeout: 11000,
      specs: [
        './src/**/*.e2e-spec.ts'
      ],
      capabilities: {
        browserName: 'chrome'
      },
      directConnect: true,
      SELENIUM_PROMISE_MANAGER: false,
      baseUrl: 'http://localhost:4200/',
      framework: 'jasmine',
      jasmineNodeOpts: {
        showColors: true,
        defaultTimeoutInterval: 30000,
        print: function() {}
      },
      onPrepare() {
        require('ts-node').register({
          project: require('path').join(__dirname, './tsconfig.json')
        });
        jasmine.getEnv().addReporter(new SpecReporter({
          spec: {
            displayStacktrace: StacktraceOption.PRETTY
          }
        }));
      }
    };
    
   +---- src
   |      +---- app.e2e-spec.ts

    import { browser, element, by } from 'protractor';

    describe('i18n E2E Tests', () => {

      beforeEach(() => browser.get(''));

      it('should display i18n translated welcome: Bonjour !', async () => {
        expect(await element(by.css('h1')).getText()).toEqual('Bonjour i18n !');
      });

      it('should display the node texts without elements', async () => {
        expect(await element(by.css('app-root')).getText()).toContain(`Je n'affiche aucun élément`);
      });

      it('should display the translated title attribute', async () => {
        const title = await element(by.css('img')).getAttribute('title');
        expect(title).toBe(`Logo d'Angular`);
      });

      it('should display the ICU plural expression', async () => {
        expect(await element.all(by.css('span')).get(0).getText()).toBe(`Mis à jour à l'instant`);
      });

      it('should display the ICU select expression', async () => {
        const selectIcuExp = element.all(by.css('span')).get(1);
        expect(await selectIcuExp.getText()).toBe(`L'auteur est une femme`);
        await element.all(by.css('button')).get(2).click();
        expect(await selectIcuExp.getText()).toBe(`L'auteur est un homme`);
      });

      it('should display the nested expression', async () => {
        const nestedExp = element.all(by.css('span')).get(2);
        const incBtn = element.all(by.css('button')).get(0);
        expect(await nestedExp.getText()).toBe(`Mis à jour: à l'instant`);
        await incBtn.click();
        expect(await nestedExp.getText()).toBe(`Mis à jour: il y a une minute`);
        await incBtn.click();
        await incBtn.click();
        await element.all(by.css('button')).get(4).click();
        expect(await nestedExp.getText()).toBe(`Mis à jour: il y a 3 minutes par autre`);
      });

    });
    
   |      +---- app.po.ts

    import { browser, by, element } from 'protractor';

    export class AppPage {
      async navigateTo(): Promise<unknown> {
        return browser.get(browser.baseUrl);
      }

      async getTitleText(): Promise<string> {
        return element(by.css('app-root h1')).getText();
      }
    }
    
   +---- tsconfig.json

    /* To learn more about this file see: https://angular.io/config/tsconfig. */
    {
      "extends": "../tsconfig.json",
      "compilerOptions": {
        "outDir": "../out-tsc/e2e",
        "module": "commonjs",
        "target": "es2018",
        "types": [
          "jasmine",
          "node"
        ]
      }
    }
    
   karma.conf.js

    // Karma configuration file, see link for more information
    // https://karma-runner.github.io/1.0/config/configuration-file.html

    module.exports = function (config) {
      config.set({
        basePath: '',
        frameworks: ['jasmine', '@angular-devkit/build-angular'],
        plugins: [
          require('karma-jasmine'),
          require('karma-chrome-launcher'),
          require('karma-jasmine-html-reporter'),
          require('karma-coverage'),
          require('@angular-devkit/build-angular/plugins/karma')
        ],
        client: {
          jasmine: {
            // you can add configuration options for Jasmine here
            // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
            // for example, you can disable the random execution with `random: false`
            // or set a specific seed with `seed: 4321`
          },
          clearContext: false // leave Jasmine Spec Runner output visible in browser
        },
        jasmineHtmlReporter: {
          suppressAll: true // removes the duplicated traces
        },
        coverageReporter: {
          dir: require('path').join(__dirname, './coverage/angular.io-example'),
          subdir: '.',
          reporters: [
            { type: 'html' },
            { type: 'text-summary' }
          ]
        },
        reporters: ['progress', 'kjhtml'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true,
        browsers: ['Chrome'],
        singleRun: false,
        restartOnFileChange: true
      });
    };
    
   package.json

    {
      "name": "angular.io-example",
      "version": "0.0.0",
      "description": "Example project from an angular.io guide.",
      "license": "MIT",
      "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "start:fr": "ng serve --configuration=fr",
        "build": "ng build",
        "build:fr": "ng build --configuration=production-fr",
        "test": "ng test",
        "lint": "ng lint",
        "e2e": "ng e2e",
        "extract": "ng extract-i18n --output-path=locale"
      },
      "private": true,
      "dependencies": {
        "@angular/animations": "~12.0.0",
        "@angular/common": "~12.0.0",
        "@angular/compiler": "~12.0.0",
        "@angular/core": "~12.0.0",
        "@angular/forms": "~12.0.0",
        "@angular/localize": "~12.0.0",
        "@angular/platform-browser": "~12.0.0",
        "@angular/platform-browser-dynamic": "~12.0.0",
        "@angular/router": "~12.0.0",
        "angular-in-memory-web-api": "~0.11.0",
        "rxjs": "~6.6.0",
        "tslib": "^2.0.0",
        "zone.js": "~0.11.4"
      },
      "devDependencies": {
        "@angular-devkit/build-angular": "~12.0.0",
        "@angular/cli": "~12.0.0",
        "@angular/compiler-cli": "~12.0.0",
        "@types/jasmine": "~3.6.0",
        "@types/node": "^12.11.1",
        "codelyzer": "^6.0.0",
        "jasmine-core": "~3.7.0",
        "jasmine-marbles": "~0.6.0",
        "jasmine-spec-reporter": "~5.0.0",
        "karma": "~6.3.0",
        "karma-chrome-launcher": "~3.1.0",
        "karma-coverage": "~2.0.3",
        "karma-jasmine": "~4.0.0",
        "karma-jasmine-html-reporter": "^1.5.0",
        "protractor": "~7.0.0",
        "ts-node": "~8.3.0",
        "tslint": "~6.1.0",
        "typescript": "~4.2.3"
      }
    }
    
   src
   +---- app
   |      +---- app.component.html

    <h1 i18n="User welcome|An introduction header for this sample@@introductionHeader">
      Hello i18n!
    </h1>

    <ng-container i18n>I don't output any element</ng-container>

    <br />

    <img [src]="logo" i18n-title title="Angular logo" />
    <br>
    <button (click)="inc(1)">+</button> <button (click)="inc(-1)">-</button>
    <span i18n>Updated {minutes, plural, =0 {just now} =1 {one minute ago} other {{{minutes}} minutes ago}}</span>
    ({{minutes}})
    <br><br>
    <button (click)="male()">&#9794;</button> <button (click)="female()">&#9792;</button> <button (click)="other()">&#9895;</button>
    <span i18n>The author is {gender, select, male {male} female {female} other {other}}</span>
    <br><br>
    <span i18n>Updated: {minutes, plural,
      =0 {just now}
      =1 {one minute ago}
      other {{{minutes}} minutes ago by {gender, select, male {male} female {female} other {other}}}}
    </span>
    
   |      +---- app.component.ts

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

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html'
    })
    export class AppComponent {
      minutes = 0;
      gender = 'female';
      fly = true;
      logo = 'https://angular.io/assets/images/logos/angular/angular.png';
      inc(i: number) {
        this.minutes = Math.min(5, Math.max(0, this.minutes + i));
      }
      male() { this.gender = 'male'; }
      female() { this.gender = 'female'; }
      other() { this.gender = 'other'; }
    }

    
   |      +---- app.module.ts

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';

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

    @NgModule({
      imports: [ BrowserModule ],
      declarations: [ AppComponent ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }
    
   +---- environments
   |      +---- environment.prod.ts

    export const environment = {
      production: true
    };
    
   |      +---- environment.ts

    // This file can be replaced during build by using the `fileReplacements` array.
    // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`.
    // The list of file replacements can be found in `angular.json`.

    export const environment = {
      production: false
    };

    /*
    * For easier debugging in development mode, you can import the following file
    * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`.
    *
    * This import should be commented out in production mode because it will have a negative impact
    * on performance if an error is thrown.
    */
    // import 'zone.js/plugins/zone-error';  // Included with Angular CLI.
    
   +---- index.html

    <!DOCTYPE html>
    <html>
      <head>
        <base href="/">
        <title>Angular i18n example</title>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
      </head>
      <body>
        <app-root>Loading...</app-root>
      </body>
    </html>
    
   +---- locale
   |      +---- messages.fr.xlf

    <?xml version="1.0" encoding="UTF-8" ?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
      <file source-language="en" datatype="plaintext" original="ng2.template">
        <body>
          <trans-unit id="introductionHeader" datatype="html">
            <source>
        Hello i18n!
    </source>
            <target>
        Bonjour i18n !
    </target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">4</context>
            </context-group>
            <note priority="1" from="description">An introduction header for this sample</note>
            <note priority="1" from="meaning">User welcome</note>
          </trans-unit>
          <trans-unit id="5206857922697139278" datatype="html">
            <source>I don&apos;t output any element</source>
            <target>Je n'affiche aucun élément</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">10</context>
            </context-group>
          </trans-unit>
          <trans-unit id="392942015236586892" datatype="html">
            <source>Angular logo</source>
            <target>Logo d'Angular</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">16</context>
            </context-group>
          </trans-unit>
          <trans-unit id="4606963464835766483" datatype="html">
            <source>Updated <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
            <target>Mis à jour <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">21</context>
            </context-group>
          </trans-unit>
          <trans-unit id="2002272803511843863" datatype="html">
            <source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago} }</source>
            <target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes} }</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">21</context>
            </context-group>
          </trans-unit>
          <trans-unit id="3560311772637911677" datatype="html">
            <source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
            <target>L'auteur est <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">27</context>
            </context-group>
          </trans-unit>
          <trans-unit id="7670372064920373295" datatype="html">
            <source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
            <target>{VAR_SELECT, select, male {un homme} female {une femme} other {autre} }</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">27</context>
            </context-group>
          </trans-unit>
          <trans-unit id="3967965900462880190" datatype="html">
            <source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/>
    </source>
            <target>Mis à jour: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/>
    </target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">31</context>
            </context-group>
          </trans-unit>
          <trans-unit id="2508975984005233379" datatype="html">
            <source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago by {VAR_SELECT, select, male {male} female {female} other {other} }} }</source>
            <target>{VAR_PLURAL, plural, =0 {à l'instant} =1 {il y a une minute} other {il y a <x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes par {VAR_SELECT, select, male {un homme} female {une femme} other {autre} }} }</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app\app.component.ts</context>
              <context context-type="linenumber">31</context>
            </context-group>
          </trans-unit>
        </body>
      </file>
    </xliff>
    
   |      +---- messages.xlf

    <?xml version="1.0" encoding="UTF-8" ?>
    <xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
      <file source-language="en" datatype="plaintext" original="ng2.template">
        <body>
          <trans-unit id="introductionHeader" datatype="html">
            <source>
      Hello i18n!
    </source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">3</context>
            </context-group>
            <note priority="1" from="description">An introduction header for this sample</note>
            <note priority="1" from="meaning">User welcome</note>
          </trans-unit>
          <trans-unit id="5206857922697139278" datatype="html">
            <source>I don&apos;t output any element</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">9</context>
            </context-group>
          </trans-unit>
          <trans-unit id="392942015236586892" datatype="html">
            <source>Angular logo</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">15</context>
            </context-group>
          </trans-unit>
          <trans-unit id="4606963464835766483" datatype="html">
            <source>Updated <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">20</context>
            </context-group>
          </trans-unit>
          <trans-unit id="2002272803511843863" datatype="html">
            <source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago} }</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">20</context>
            </context-group>
          </trans-unit>
          <trans-unit id="3560311772637911677" datatype="html">
            <source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">26</context>
            </context-group>
          </trans-unit>
          <trans-unit id="7670372064920373295" datatype="html">
            <source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">26</context>
            </context-group>
          </trans-unit>
          <trans-unit id="3967965900462880190" datatype="html">
            <source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/>
    </source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">30</context>
            </context-group>
          </trans-unit>
          <trans-unit id="2508975984005233379" datatype="html">
            <source>{VAR_PLURAL, plural, =0 {just now} =1 {one minute ago} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> minutes ago by {VAR_SELECT, select, male {male} female {female} other {other} }} }</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/app.component.ts</context>
              <context context-type="linenumber">30</context>
            </context-group>
          </trans-unit>
        </body>
      </file>
    </xliff>
    
   +---- main.ts

    import { enableProdMode } from '@angular/core';
    import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

    import { AppModule } from './app/app.module';
    import { environment } from './environments/environment';

    if (environment.production) {
      enableProdMode();
    }

    platformBrowserDynamic().bootstrapModule(AppModule);
    
   +---- styles.css

    /* Global Styles */
    * {
      font-family: Arial, Helvetica, sans-serif;
    }
    h1 {
      color: #264D73;
      font-size: 2.5rem;
    }
    h2, h3 {
      color: #444;
      font-weight: lighter;
    }
    h3 {
      font-size: 1.3rem;
    }
    body {
      padding: .5rem;
      max-width: 1000px;
      margin: auto;
    }
    @media (min-width: 600px) {
      body {
        padding: 2rem;
      }
    }
    body, input[text] {
      color: #333;
      font-family: Cambria, Georgia, serif;
    }
    a {
      cursor: pointer;
    }
    button {
      background-color: #eee;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      color: black;
      font-size: 1.2rem;
      padding: 1rem;
      margin-right: 1rem;
      margin-bottom: 1rem;
      margin-top: 1rem;
    }
    button:hover {
      background-color: black;
      color: white;
    }
    button:disabled {
      background-color: #eee;
      color: #aaa;
      cursor: auto;
    }

    /* Navigation link styles */
    nav a {
      padding: 5px 10px;
      text-decoration: none;
      margin-right: 10px;
      margin-top: 10px;
      display: inline-block;
      background-color: #e8e8e8;
      color: #3d3d3d;
      border-radius: 4px;
    }

    nav a:hover {
      color: white;
      background-color:  #42545C;
    }
    nav a.active {
      background-color: black;
      color: white;
    }
    hr {
      margin: 1.5rem 0;
    }
    input[type="text"] {
      box-sizing: border-box;
      width: 100%;
      padding: .5rem;
    }
    
   +---- test.ts

    // This file is required by karma.conf.js and loads recursively all the .spec and framework files

    import 'zone.js/testing';
    import { getTestBed } from '@angular/core/testing';
    import {
      BrowserDynamicTestingModule,
      platformBrowserDynamicTesting
    } from '@angular/platform-browser-dynamic/testing';

    declare const require: {
      context(path: string, deep?: boolean, filter?: RegExp): {
        <T>(id: string): T;
        keys(): string[];
      };
    };

    // First, initialize the Angular testing environment.
    getTestBed().initTestEnvironment(
      BrowserDynamicTestingModule,
      platformBrowserDynamicTesting()
    );
    // Then we find all the tests.
    const context = require.context('./', true, /\.spec\.ts$/);
    // And load the modules.
    context.keys().map(context);
    
   tsconfig.app.json

    /* To learn more about this file see: https://angular.io/config/tsconfig. */
    {
      "extends": "./tsconfig.json",
      "compilerOptions": {
        "outDir": "./out-tsc/app",
        "types": []
      },
      "files": [
        "src/main.ts",
        "src/polyfills.ts"
      ],
      "include": [
        "src/**/*.d.ts"
      ],
      "exclude": [
        "src/test.ts",
        "src/**/*.spec.ts",
        "src/**/*-specs.ts",
        "src/**/*.avoid.ts",
        "src/**/*.0.ts",
        "src/**/*.1.ts",
        "src/**/*.1b.ts",
        "src/**/*.2.ts",
        "src/**/*.3.ts",
        "src/**/*.4.ts",
        "src/**/*.5.ts",
        "src/**/*.6.ts",
        "src/**/*.7.ts",
        "src/**/testing"
      ]
    }
    
   tsconfig.json

    /* To learn more about this file see: https://angular.io/config/tsconfig. */
    {
      "compileOnSave": false,
      "compilerOptions": {
        "baseUrl": "./",
        "outDir": "./dist/out-tsc",
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "noImplicitReturns": true,
        "noFallthroughCasesInSwitch": true,
        "sourceMap": true,
        "declaration": false,
        "downlevelIteration": true,
        "experimentalDecorators": true,
        "moduleResolution": "node",
        "importHelpers": true,
        "target": "es2015",
        "module": "es2020",
        "lib": [
          "es2018",
          "dom"
        ]
      },
      "angularCompilerOptions": {
        "enableI18nLegacyMessageIdFormat": false,
        "strictInjectionParameters": true,
        "strictInputAccessModifiers": true,
        "strictTemplates": true
      }
    }
    
   tslint.json

    {
      "extends": "tslint:recommended",
      "rulesDirectory": [
        "codelyzer"
      ],
      "rules": {
        "align": {
          "options": [
            "parameters",
            "statements"
          ]
        },
        "array-type": false,
        "arrow-return-shorthand": true,
        "curly": true,
        "deprecation": {
          "severity": "warning"
        },
        "eofline": true,
        "import-blacklist": [
          true,
          "rxjs/Rx"
        ],
        "import-spacing": true,
        "indent": {
          "options": [
            "spaces"
          ]
        },
        "max-classes-per-file": false,
        "max-line-length": [
          true,
          140
        ],
        "member-ordering": [
          true,
          {
            "order": [
              "static-field",
              "instance-field",
              "static-method",
              "instance-method"
            ]
          }
        ],
        "no-console": [
          true,
          "debug",
          "info",
          "time",
          "timeEnd",
          "trace"
        ],
        "no-empty": false,
        "no-inferrable-types": [
          true,
          "ignore-params"
        ],
        "no-non-null-assertion": false,
        "no-redundant-jsdoc": true,
        "no-switch-case-fall-through": true,
        "no-var-requires": false,
        "object-literal-key-quotes": [
          true,
          "as-needed"
        ],
        "quotemark": [
          true,
          "single"
        ],
        "semicolon": {
          "options": [
            "always"
          ]
        },
        "space-before-function-paren": {
          "options": {
            "anonymous": "never",
            "asyncArrow": "always",
            "constructor": "never",
            "method": "never",
            "named": "never"
          }
        },
        // TODO(gkalpak): Fix the code and enable this.
        // "typedef": [
        //   true,
        //   "call-signature"
        // ],
        "typedef-whitespace": {
          "options": [
            {
              "call-signature": "nospace",
              "index-signature": "nospace",
              "parameter": "nospace",
              "property-declaration": "nospace",
              "variable-declaration": "nospace"
            },
            {
              "call-signature": "onespace",
              "index-signature": "onespace",
              "parameter": "onespace",
              "property-declaration": "onespace",
              "variable-declaration": "onespace"
            }
          ]
        },
        "variable-name": {
          "options": [
            "ban-keywords",
            "check-format",
            "allow-pascal-case"
          ]
        },
        "whitespace": {
          "options": [
            "check-branch",
            "check-decl",
            "check-operator",
            "check-separator",
            "check-type",
            "check-typecast"
          ]
        },
        "component-class-suffix": true,
        "contextual-lifecycle": true,
        "directive-class-suffix": true,
        "no-conflicting-lifecycle": true,
        "no-host-metadata-property": true,
        "no-input-rename": true,
        "no-inputs-metadata-property": true,
        "no-output-native": true,
        "no-output-on-prefix": true,
        "no-output-rename": true,
        "no-outputs-metadata-property": true,
        "template-banana-in-box": true,
        "template-no-negated-async": true,
        "use-lifecycle-interface": true,
        "use-pipe-transform-interface": true,
        "directive-selector": [
          true,
          "attribute",
          ["app", "toh"],
          "camelCase"
        ],
        "component-selector": [
          true,
          "element",
          // TODO: Fix the code and change the prefix to `"app"` (or whatever makes sense).
          "",
          "kebab-case"
        ]
      }
    }
    

ServiceWorker


    # add @angular/service-worker package
    # enables SW build support in the CLI
    # imports and registers the SW in the app module
    #   updates the index.html file:
    #   includes a link to add the manifest.json file
    # adds meta tags for theme-color
    # installs icon files to support the installed PWA
    # creates the SW configuration file called ngsw-config.json
    ng add @angular/pwa --project my-project-name
    # now, build the project:
    ng build --prod

    # serve local files with npm http-server
    http-server -p 8080 -c-1 dist/angular-tour
    # try target http-server from node_modules/http-server/bin/http-server
    # and run current folder
    ../../node_modules/.bin/http-server .
    # or serve just index to serve correctly routes !
    ../../node_modules/.bin/http-server index.html
  

SW


    import { ApplicationRef, Injectable } from '@angular/core';
    import { SwUpdate } from '@angular/service-worker';
    import { concat, interval } from 'rxjs';
    import { first } from 'rxjs/operators';
    @Injectable()
    export class LogUpdateService {
      constructor(
        appRef: ApplicationRef,
        updates: SwUpdate
      ) {
        // --- available and activated updates
        updates.available.subscribe(event => {
          console.log('current version is', event.current);
          console.log('available version is', event.available);
        });
        updates.activated.subscribe(event => {
          console.log('old version was', event.previous);
          console.log('new version is', event.current);
        });
        // --- checking for updates
        // allow the app to stabilize first,
        // before starting polling for updates with interval().
        // Promise which indicates the update check has completed successfully
        // does not indicate whether an update was discovered as a result of the check
        const appIsStable$ = appRef.isStable.pipe(
          first(isStable => isStable === true)
        );
        const everySixHours$ = interval(6 * 60 * 60 * 1000);
        const everySixHoursOnceAppIsStable$ =
          concat(appIsStable$, everySixHours$);
        everySixHoursOnceAppIsStable$.subscribe(
          () => updates.checkForUpdate()
        );
        // --- forcing update activation immediately
        // doing this could break lazy-loading into currently running apps
        updates.available.subscribe(event => {
          if (promptUser(event)) {
            updates.activateUpdate().then(
              () => document.location.reload()
            );
          }
        });
      }
    }

    // deactivate the service worker:
    // npm i safety-worker
    navigator.serviceWorker.register("/safety-worker.js")
    // sconst skipWaiting = () => self.skipWaiting();
    // sconst unregister = (event) => {
    //   event.waitUntil(self.clients.claim());
    //   self.registration.unregister()
    //     .then(() => console.log('Unregistered old service worker'));
    // };
    // sself.addEventListener('install', skipWaiting);
    // self.addEventListener('activate', unregister);

    class SwUpdate {
      // true - if the SW is enabled
      // (supported by the browser and enabled via ServiceWorkerModule)
      isEnabled: boolean
      checkForUpdate(): Promise<void>
      // whenever the app has been updated to a new version
      activateUpdate(): Promise<void>
      // whenever a new app version is available
      SwUpdate#versionUpdates
    }
  

ngsw-config.json


    {
      "index": "/index.html",
      "assetGroups": [
        {
          "name": "app",
          "installMode": "prefetch",
          "resources": {
            "files": [
              "/favicon.ico",
              "/index.html",
              "/*.css",
              "/*.js"
            ]
          }
        }, {
          "name": "assets",
          "installMode": "lazy",
          "updateMode": "prefetch",
          "resources": {
            "files": [
              "/assets/**",
              "/*.(eot|svg|cur|jpg|png|webp|gif|otf|ttf|woff|woff2|ani)"
            ]
          }
        }
      ]
    }

    AssetGroup {
      // identifies group of assets between versions of the configuration
      name: string;
      // how these resources are initially cached
      // prefetch (default) - fetch every single listed resource
      // lazy - on-demand caching mode
      installMode?: 'prefetch' | 'lazy';
      // caching behavior when a new version of the app is discovered
      // prefetch - download and cache the changed resources immediately
      // lazy - dont cache resources,
      //   treats them as unrequested and waits until they
      //   are requested again before updating them
      //   updateMode of lazy is only valid if the installMode is also lazy
      updateMode?: 'prefetch' | 'lazy';
      // resources to cache
      resources: {
        files?: string[];
        // URLs and URL patterns that will be matched at runtime
        // will be cached according to their HTTP headers
        //   negative glob patterns are not supported and ? will be matched literally
        //   will not match any character other than ?
        urls?: string[];
      };
    }

    DataGroup {
      // group name which uniquely identifies
      name: string;
      // URL patterns, will be cached according to this data group policy
      urls: string[];
      // mechanism to indicate that the resources being cached
      // have been updated in a backwards-incompatible way,
      // and that the old version cache entries should be discarded
      // integer field, defaults to 1
      version?: number;
      // policy by which matching requests will be cached
      cacheConfig: {
        // maximum number of entries, or responses, in the cache
        maxSize: number;
        // how long responses are allowed to remain in the cache
        // before being considered invalid and evicted
        // duration string, using the following unit suffixes:
        //   d: days
        //   h: hours
        //   m: minutes
        //   s: seconds
        //   u: milliseconds
        // 3d12h will cache content for up to three and a half days
        maxAge: string;
        // how long the Angular service worker will wait for the network
        // to respond before using a cached response, if configured to do so
        // uses same sufixes
        timeout?: string;
        // strategies for data resources
        // performance (default) - optimizes for fast responses
        //   if resource exists in the cache, cached version is used
        //   suitable for resources that dont change often
        // freshness - optimizes for currency of data,
        //   preferentially fetching requested data from the network
        //   only if the network times out, according to timeout,
        //   does the request fall back to the cache
        //   useful for resources that change frequently
        strategy?: 'freshness' | 'performance';
      };
    }
  

SSR

http interceptor for server requests


    // interceptor will fire and replace the
    // request URL with the absolute URL provided in the Express Request object
    import {Injectable, Inject, Optional} from '@angular/core';
    import {HttpInterceptor, HttpHandler, HttpRequest, HttpHeaders} from '@angular/common/http';
    import {Request} from 'express';
    import {REQUEST} from '@nguniversal/express-engine/tokens';
    @Injectable()
    export class UniversalInterceptor implements HttpInterceptor {
      constructor(@Optional() @Inject(REQUEST) protected request: Request) {}
      intercept(req: HttpRequest, next: HttpHandler) {
        let serverReq: HttpRequest = req;
        if (this.request) {
          let newUrl = `${this.request.protocol}://${this.request.get('host')}`;
          if (!req.url.startsWith('/')) {
            newUrl += '/';
          }
          newUrl += req.url;
          serverReq = req.clone({url: newUrl});
        }
        return next.handle(serverReq);
      }
    }
    // --- provide interceptor in app.server.module.ts
    import {HTTP_INTERCEPTORS} from '@angular/common/http';
    import {UniversalInterceptor} from './universal-interceptor';
    @NgModule({
      ...
      providers: [{
        provide: HTTP_INTERCEPTORS,
        useClass: UniversalInterceptor,
        multi: true
      }],
    })
    export class AppServerModule {}
  

Libraries


    # install library and its types
    npm install d3 --save
    npm install @types/d3 --save-dev

    # update individual library versions
    ng update lib_name
  

manually adding typings


    // 1 - create a typings.d.ts file in your src/ folder
    // is automatically included as global type definition
    // 2
    declare module 'host' {
      export interface Host {
        protocol?: string;
        hostname?: string;
        pathname?: string;
      }
      export function parse(url: string, queryString?: string): Host;
    }
    // 3 - in the component or file that uses the library
    import * as host from 'host';
    const parsedUrl = host.parse('https://angular.io');
    console.log(parsedUrl.hostname);
  

adding a library to the runtime global scope, Bootstrap 4


    npm install jquery --save
    npm install popper.js --save
    npm install bootstrap --save

    // angular.json
    "scripts": [
      "node_modules/jquery/dist/jquery.slim.js",
      "node_modules/popper.js/dist/umd/popper.js",
      "node_modules/bootstrap/dist/js/bootstrap.js"
    ],
    ...
    "styles": [
      "node_modules/bootstrap/dist/css/bootstrap.css",
      "src/styles.css"
    ],

    // once you import a library using the "scripts" array,
    // you should not import it using an import statement
    // in your TS code (such as import * as $ from 'jquery';)
    // instead, download typings for your library
    // npm install @types/jquery and others

    // ... run or restart ng serve

    // if the global library you need to use
    // does not have global typings,
    // declare them manually as any in src/typings.d.ts
    declare var libraryName: any;

    // some scripts extend other libraries;
    // for instance with JQuery plugins
    $('.test').myPlugin();
    // installed @types/jquery doesn't include myPlugin,
    // so you need to add an interface in src/typings.d.ts
    interface JQuery {
      myPlugin(options?: any): any;
    }
  

creating libraries


    // list all the peer dependencies that your library uses
    // in the TS configuration file ./tsconfig.json
    // this mapping ensures that your library
    // always loads the local copies of the modules it needs.
    {
      "compilerOptions": {
        // ...
        // paths are relative to `baseUrl` path.
        "paths": {
          "@angular/*": [
            "../node_modules/@angular/*"
          ]
        }
      }
    }
  

Workspace/Project

WORKSPACE CONFIG FILES
.editorconfig configuration for code editors
.gitignore untracked files that Git should ignore
angular.json CLI configuration defaults for all projects in the workspace, including configuration options for build, serve, and test tools that the CLI uses, such as TSLint, Karma, and Protractor
node_modules provides npm packages to the entire workspace
package.json configures npm package dependencies that are available to all projects in the workspace
package-lock.json | yarn.lock provides version information for all packages installed into node_modules by the npm client
tsconfig.json default TS configuration for apps in the workspace, including TypeScript and Angular template compiler options
tslint.json default TSLint configuration for apps in the workspace
README.md introductory documentation
WORKSPACE CONFIGURATION (angular.json)

    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      // configuration-file version
      "version": 1,
      // path where new projects are created, bsolute or relative to the workspace folder
      "newProjectRoot": "projects",
      // subsection for each app, e2e app, and library individual configuration options
      "projects": {
        "angular-tour": {
          // root folder for files, relative to the workspace folder
          "root": "",
          // root folder for source files
          "sourceRoot": "src",
          // application | library
          // application can run independently in a browser, library cannot
          // both an app and its e2e test app are of type "application"
          "projectType": "application",
          // string that Angular prepends to generated selectors
          // can be customized to identify an app or feature area
          "prefix": "app",
          // object containing schematics that customize CLI commands
          "schematics": {},
          // configuration defaults for Architect builder targets
          // tool that the CLI uses to perform complex tasks
          // can be run using the "ng run" command, and define your own targets
          "architect": {
            // target, which is the npm package for the tool that Architect runs
            // defaults for options of the "ng build" command
            "build": {
              // npm package for the build tool used to create this target
              // default is @angular-devkit/build-angular:browser
              // which uses the webpack package bundler
              "builder": "@angular-devkit/build-angular:browser",
              // default options for the target
              // used when no named alternative configuration is specified
              "options": {
                "outputPath": "dist/browser",
                "index": "src/index.html",
                "main": "src/main.ts",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "src/tsconfig.app.json",
                // object containing paths to static assets to add to the global context
                // of the project, default paths point to the project icon file and its assets folder
                "assets": [ "src/favicon.ico", "src/assets", "src/manifest.webmanifest" ],
                "styles": [ "src/styles.css" ],
                // option-value pairs to pass to style preprocessors
                stylePreprocessorOptions: { },
                // JS files to add to the global context of the project
                // loaded exactly as if you had added them in a script-tag inside index.html
                "scripts": [
                  {
                    "input": "node_modules/document-register-element/build/document-register-element.js"
                  }
                ],
                // default size-budget type and threshholds for all or parts of your app
                "budgets": [ ],
              },
              // section with named configuration
              // sets the default options for that intended environment
              "configurations": {
                "production": {
                  // files and their compile-time replacements
                  // default environment with production version
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ],
                  "optimization": true,
                  "outputHashing": "all",
                  "sourceMap": false,
                  "extractCss": true,
                  "namedChunks": false,
                  "extractLicenses": true,
                  "vendorChunk": false,
                  "buildOptimizer": true,
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "2mb",
                      "maximumError": "5mb"
                    }
                  ],
                  "serviceWorker": true
                },
                // locale-specific configuration
                "ru": {
                  "outputPath": "dist/angular-tour-ru/",
                  "i18nFile": "src/locale/messages.ru.xlf",
                  "i18nFormat": "xlf",
                  "i18nLocale": "ru",
                  "i18nMissingTranslation": "error"
                }
              }
            },
            // override build defaults and supplies additional
            // serve defaults for the "ng serve" command
            "serve": {
              "builder": "@angular-devkit/build-angular:dev-server",
              "options": { "browserTarget": "angular-tour:build" },
              "configurations": {
                "production": {
                  "browserTarget": "angular-tour:build:production"
                },
                "ru": { "browserTarget": "angular-tour:build:ru" }
              }
            },
            // override build-option defaults
            // for building end-to-end testing apps using the "ng e2e" command
            "e2e" : { },
            // overrides build-option defaults for test builds and supplies
            // additional test-running defaults for the "ng test" command
            "test": {
              "builder": "@angular-devkit/build-angular:karma",
              "options": {
                "main": "src/test.ts",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "src/tsconfig.spec.json",
                "karmaConfig": "src/karma.conf.js",
                "styles": [ "src/styles.css" ],
                "scripts": [],
                "assets": [ "src/favicon.ico", "src/assets", "src/manifest.webmanifest" ]
              }
            },
            // configure defaults for options of the ng-xi18n tool used by the "ng xi18n" command
            // which extracts marked message strings from source code and outputs translation files
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n",
              "options": {
                "browserTarget": "angular-tour:build"
              }
            },
            // configure defaults for options of the "ng lint" command
            // which performs code analysis on project source files
            "lint": {
              "builder": "@angular-devkit/build-angular:tslint",
              "options": {
                "tsConfig": [ "src/tsconfig.app.json", "src/tsconfig.spec.json" ],
                "exclude": [ "**/node_modules/**" ]
              }
            },
            // defaults for creating a Universal app with server-side rendering
            // using the "ng run project-name:server" command
            "server": {
              "builder": "@angular-devkit/build-angular:server",
              "options": {
                "outputPath": "dist/server",
                "main": "src/main.server.ts",
                "tsConfig": "src/tsconfig.server.json"
              },
              "configurations": {
                "production": {
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ]
                }
              }
            },
            // defaults for creating an app shell for a PWA
            // using the "ng run project-name:app-shell" command
            "app-shell": { }
        } },
        "angular-tour-e2e": {
          "root": "e2e/",
          "projectType": "application",
          "prefix": "",
          "architect": {
            "e2e": {
              "builder": "@angular-devkit/build-angular:protractor",
              "options": {
                "protractorConfig": "e2e/protractor.conf.js",
                "devServerTarget": "angular-tour:serve"
              },
              "configurations": {
                "production": {
                  "devServerTarget": "angular-tour:serve:production"
                }
              }
            },
            "lint": {
              "builder": "@angular-devkit/build-angular:tslint",
              "options": {
                "tsConfig": "e2e/tsconfig.e2e.json",
                "exclude": [ "**/node_modules/**" ]
      } } } } },
      // default project name to use in commands
      "defaultProject": "angular-tour"
    }
  
APP SOURCE (/src) & CONFIG FILES
app/ contains the component files in which your app logic and data are defined
assets/ contains image files and other asset files to be copied as-is when you build your application
environments/ contains build configuration options for particular target environments, by default there is an unnamed standard development environment and a production ("prod") environment, you can define additional target environment configurations
browserslist configures sharing of target browsers and Node.js versions among various front-end tools
favicon.ico icon to use for this app in the bookmark bar
index.html main HTML page that is served when someone visits your site, CLI automatically adds all JavaScript and CSS files when building your app, so you typically dont need to add any <script> or<link> tags here manually
main.ts main entry point for your app, compiles the application with the JIT compiler and bootstraps the application root module (AppModule) to run in the browser
polyfills.ts polyfill scripts for browser support
styles.sass CSS files that supply styles for a project, extension reflects the style preprocessor you have configured for the project
test.ts main entry point for your unit tests, with some Angular-specific configuration, you dont typically need to edit this file
tsconfig.app.json inherits from the workspace-wide tsconfig.json file
tsconfig.spec.json inherits from the workspace-wide tsconfig.json file
tslint.json inherits from the workspace-wide tslint.json file
APP SOURCE FILES
app/app.component.ts defines the logic for the app root component, named AppComponent,view associated with this root component becomes the root of the view hierarchy as you add components and services to your app
app/app.component.html defines the HTML template associated with the root AppComponent
app/app.component.css defines the base CSS stylesheet for the root AppComponent
app/app.component.spec.ts defines a unit test for the root AppComponent
app/app.module.ts defines the root module, named AppModule, that tells Angular how to assemble the application. Initially declares only the AppComponent. As you add more components to the app, they must be declared here
assets/* contains image files and other asset files to be copied as-is when you build your application

    # project folders for additional apps and libraries
    # newly generated libraries are also added under projects/.
    my-app/
      ...
      projects/           (additional apps and libs)
        my-other-app/     (a second app)
          src/
          (config files)
        my-other-app-e2e/  (corresponding test app)
          src/
          (config files)
        my-lib/            (a generated library)
          (config files)

    # default app project end-to-end test files
    # workspace-wide node_modules dependencies are visible to this project
    my-app/
      e2e/                  (end-to-end test app for my-app)
        src/                (app source files)
        protractor.conf.js  (test-tool config)
        tsconfig.e2e.json   (TS config inherits from workspace tsconfig.json)
  

npm/packages

Compilation


    // --- compiler does not support function expressions and arrow functions
    // use exported value
    export function serverFactory() {
      return new Server();
    }
    @Component({
      ...
      providers: [{provide: server, useFactory: serverFactory}]
    })

    // --- folding
    const template = '<div>{{hero.name}}</div>';
    @Component({
      selector: 'app-hero',
      // template: template // leads to error !
      template: template + '<div>{{hero.title}}</div>' // ok
    })
    export class HeroComponent {
      @Input() hero: Hero;
    }

    // --- metadata rewriting
    // compiler converts the expression initializing
    // one of the useClass|useValue|useFactory
    // into an exported variable, which replaces the expression.
    class TypicalServer { }
    export const ɵ0 = () => new TypicalServer();
    @NgModule({
      providers: [{provide: SERVER, useFactory: ɵ0}]
    })
    export class TypicalModule {}

    // --- BAD CODE - title is private
    @Component({
      selector: 'app-root',
      template: '<h1>{{title}}</h1>'
    })
    export class AppComponent {
      private title = 'My App'; // Bad
    }

    // --- TS compiler infers that binding expression will never be undefined
    @Component({
      selector: 'my-component',
      template: '<span *ngIf="person"> {{person.addresss.street}} </span>'
    })
    class MyComponent {
      person?: Person;
    }

    // --- disabling type checking using $any()
    @Component({
      selector: 'my-component',
      template: '{{$any(person).addresss.street}}'
    })
    class MyComponent {
      person?: Person;
    }
  

metadata errors


    // --- EXPRESSION FORM NOT SUPPORTED
    // language features outside of the compiler restricted expression syntax
    export class Fooish { ... }
    ...
    const prop = typeof Fooish; // typeof is not valid in metadata
      ...
      // bracket notation is not valid in metadata
      { provide: 'token', useValue: { [prop]: 'value' } };
      ...
    // you can use typeof and bracket notation in normal application code

    // --- REFERENCE TO A LOCAL (NON-EXPORTED) SYMBOL
    // defined symbol that either wasnt exported or wasnt initialized
    let foo: number; // neither exported nor initialized
    // let foo = 42; // initialized !
    // export let foo: number; // exported !
    export let someTemplate: string; // exported but not initialized
    @Component({
      selector: 'my-component',
      template: someTemplate,
      providers: [
        { provide: Foo, useValue: foo }
      ]
    })
    export class MyComponent {}

    // --- ONLY INITIALIZED VARIABLES AND CONSTANTS
    // reference to an exported variable or static field that wasnt initialized

    // --- REFERENCE TO A NON-EXPORTED CLASS
    // --- REFERENCE TO A NON-EXPORTED FUNCTION
    // metadata referenced a class/function that wasnt exported

    // --- FUNCTION CALLS ARE NOT SUPPORTED
    // export a function from the module and refer to the function

    // --- DESTRUCTURED VARIABLE OR CONSTANT NOT SUPPORTED
    import { configuration } from './configuration';
    // destructured assignment to foo and bar
    const {foo, bar} = configuration;
      ...
      // wrong
      providers: [
        {provide: Foo, useValue: foo},
        {provide: Bar, useValue: bar},
      ]
      // right
      // providers: [
      //   {provide: Foo, useValue: configuration.foo},
      //   {provide: Bar, useValue: configuration.bar},
      // ]
      ...

    // --- COULD NOT RESOLVE TYPE
    // cant determine which module exports type.
    // do not refer to ambient types in metadata expressions
    @Component({ })
    export class MyComponent {
      constructor (private win: Window) { ... }
    }
    // inject an instance of an ambient type:
    // create an injection token for an instance of the ambient type
    // create a factory function that returns that instance
    // add a useFactory provider with that factory function
    // use @Inject to inject the instance
    import { Inject } from '@angular/core';
    import { DOCUMENT } from '@angular/platform-browser';
    export const WINDOW = new InjectionToken('Window');
    export function _window() { return window; }
    @Component({
      ...
      providers: [ { provide: WINDOW, useFactory: _window } ]
    })
    export class MyComponent {
      constructor (
        @Inject(WINDOW) private win: Window,
        @Inject(DOCUMENT) private doc: Document
      ) { ... }
    }

    // --- NAME EXPECTED
    // compiler expected a name in an expression it was evaluating
    // happens if you use a number as a property name
    provider: [{ provide: Foo, useValue: { 0: 'test' } }]
    // provider: [{ provide: Foo, useValue: { '0': 'test' } }]

    // --- UNSUPPORTED ENUM MEMBER NAME
    enum Colors {
      Red = 1,
      White,
      Blue = "Blue".length // computed = complex
    }
      ...
      providers: [
        { provide: BaseColor,   useValue: Colors.White } // ok
        { provide: DangerColor, useValue: Colors.Red }   // ok
        { provide: StrongColor, useValue: Colors.Blue }  // bad
      ]
      ...

    // --- TAGGED TEMPLATE EXPRESSIONS ARE NOT SUPPORTED
    // compiler encountered a ES6 tagged template expression
    const expression = 'funky';
    const raw = String.raw`A tagged template ${expression} string`;
     ...
     template: '<div>' + raw + '</div>'
     ...

    // --- SYMBOL REFERENCE EXPECTED
    // dont use expression in the "extends" clause of a class
  

build/serve


    // environment.ts
    export const environment = {
      production: false,
      apiUrl: 'http://my-api-url'
    };
    // environment.prod.ts
    export const environment = {
      production: true,
      apiUrl: 'http://my-prod-url'
    };

    import { Component } from '@angular/core';
    import { environment } from './../environments/environment';
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      constructor() {
        console.log(environment.production); // Logs false for default environment
      }
      title = 'app works!';
    }

    // package.json - PWA browsers list
    {
      "name": "angular-tour",
      "version": "0.0.0",
      "browserslist": [
        "last 2 versions",
        "not ie <= 10",
        "not ie_mob <= 10"
      ],
    ...
  

proxying to a backend server


    // --- proxy.conf.json
    {
      "/api": {
        "target": "http://localhost:3000",
        "secure": false,
        // remove "api" from the end of a path
        "pathRewrite": {
          "^/api": ""
        },
        // access a backend that is not on localhost
        "changeOrigin": true,
        // determine whether your proxy is working as intended
        "logLevel": "debug" // info (the default) | debug | warn | error | silent
      }
    }

    // --- proxy.conf.js
    // proxy multiple entries to the same target
    const PROXY_CONFIG = [
      {
        context: [
          "/my",
          "/many",
          "/endpoints",
          "/i",
          "/need",
          "/to",
          "/proxy"
        ],
        target: "http://localhost:3000",
        secure: false
      }
    ]
    module.exports = PROXY_CONFIG;
    // bypass the proxy, or dynamically change the request before it is sent
    const PROXY_CONFIG = {
      "/api/proxy": {
        "target": "http://localhost:3000",
        "secure": false,
        "bypass": function (req, res, proxyOptions) {
          if (req.headers.accept.indexOf("html") !== -1) {
            console.log("Skipping proxy for browser request.");
            return "/index.html";
          }
          req.headers["X-Custom-Header"] = "yes";
        }
      }
    }
    module.exports = PROXY_CONFIG;
    // using corporate proxy
    // configure the backend proxy to redirect calls through your corporate proxy using an agent:
    // npm install --save-dev https-proxy-agent
    // when you define an environment variable http_proxy or HTTP_PROXY,
    // an agent is automatically added to pass calls through your corporate proxy
    // when running npm start
    var HttpsProxyAgent = require('https-proxy-agent');
    var proxyConfig = [{
      context: '/api',
      target: 'http://your-remote-server.com:3000',
      secure: false
    }];
    function setupForCorporateProxy(proxyConfig) {
      var proxyServer = process.env.http_proxy || process.env.HTTP_PROXY;
      if (proxyServer) {
        var agent = new HttpsProxyAgent(proxyServer);
        console.log('Using corporate proxy server: ' + proxyServer);
        proxyConfig.forEach(function(entry) {
          entry.agent = agent;
        });
      }
      return proxyConfig;
    }
    module.exports = setupForCorporateProxy(proxyConfig);

    // --- angular.json
    ...
    "architect": {
      "serve": {
        "builder": "@angular-devkit/build-angular:dev-server",
        "options": {
          "browserTarget": "your-application-name:build",
          "proxyConfig": "src/proxy.conf.json"
          // "proxyConfig": "src/proxy.conf.js"
        },
    ...

    // --- call "ng serve"
  

Testing

interfaces/classes/functions

    interface TestBed {
      platform: PlatformRef
      ngModule: Type<any> | Type<any>[]
      // initialize the environment for testing with a compiler factory
      // if need to change the providers, first use resetTestEnvironment
      // test modules and platforms for individual platforms
      // are available from '@angular/platform_name/testing'
      initTestEnvironment(
        ngModule: Type<any> | Type<any>[],
        platform: PlatformRef,
      ): void
      // reset the providers for the test injector
      resetTestEnvironment(): void
      // reset the initial test environment, including default testing module
      resetTestingModule(): void
      // retrieve a service from the current TestBed injector
      // with optional second parameter data as unfound provider replacement
      get(
        token: any, notFoundValue?: any ): any
      execute(
        tokens: any[], fn: Function, context?: any ): any
      configureCompiler(
        config: { providers?: any[]; } ): void
      // refine the testing module configuration for a particular set of tests
      // by adding and removing imports,
      // declarations (of components, directives, and pipes), and providers
      configureTestingModule(
        moduleDef: TestModuleMetadata ): void
      // after callings, configuration is frozen for the duration of the current spec
      compileComponents(): Promise<any>
      createComponent<T>(
        component: Type<T>): ComponentFixture<T>
      // replacing metadata
      // can reach deeply into the current testing module
      overrideModule(
        ngModule: Type<any>,
        override: MetadataOverride<NgModule> ): void
      overrideComponent(
        component: Type<any>,
        override: MetadataOverride<Component> ): void
      overrideDirective(
        directive: Type<any>,
        override: MetadataOverride<Directive> ): void
      overridePipe(
        pipe: Type<any>,
        override: MetadataOverride<Pipe> ): void
      // overwrites all providers for the given token
      // with the given provider definition
      overrideProvider(
        token: any,
        provider: { useFactory: Function; deps: any[]; } ): void
      overrideTemplateUsingTestingModule(
        component: Type<any>, template: string ): void
    }

    // fixture for debugging and testing a component
    class ComponentFixture<T> {
      constructor(
        componentRef: ComponentRef<T>,
        ngZone: NgZone,
        _autoDetect: boolean
      )
      debugElement: DebugElement
      componentInstance: T
      nativeElement: any
      elementRef: ElementRef
      // change detection functionality
      // add and remove views from the tree,
      // initiate change-detection, and explicitly mark views as dirty (changed and need to be rerendered)
      changeDetectorRef: ChangeDetectorRef
      componentRef: ComponentRef<T>
      ngZone: NgZone | null
      // trigger a change detection cycle for the component
      detectChanges(checkNoChanges: boolean = true): void
      // do a change detection run to make sure there were no changes
      checkNoChanges(): void
      // set whether the fixture should autodetect changes
      autoDetectChanges(autoDetect: boolean = true)
      // whether the fixture is currently stable
      // or has async tasks that have not been completed yet
      isStable(): boolean
      //get a promise that resolves when the fixture is stable
      // can be used to resume testing after events have triggered
      // asynchronous activity or asynchronous change detection
      whenStable(): Promise<any>
      // get a promise that resolves when the ui state is stable following animations
      whenRenderingDone(): Promise<any>
      // trigger component destruction
      destroy(): void
    }

    interface DebugElement extends DebugNode {
      // element tag name, if it is an element
      name: string
      properties: {...}
      attributes: {...}
      classes: {...}
      styles: {...}
      // immediate DebugElement children
      children: DebugElement[]
      childNodes: DebugNode[]
      // corresponding DOM element in the browser (null for WebWorkers)
      nativeElement: any
      // first DebugElement that matches the predicate
      query(predicate: Predicate<DebugElement>): DebugElement
      // all DebugElements that matches the predicate
      queryAll(predicate: Predicate<DebugElement>): DebugElement[]
      queryAllNodes(predicate: Predicate<DebugNode>): DebugNode[]
      // triggers the event by its name
      // if there is a corresponding listener in the element listeners collection
      // second parameter is the event object expected by the handler
      // if the event lacks a listener or there some other problem,
      // consider calling nativeElement.dispatchEvent(eventObject)
      triggerEventHandler(eventName: string, eventObj: any): void
      // callbacks attached to the component @Output properties
      // and/or the element event properties
      listeners: EventListener[]
      // DebugElement parent
      parent: DebugElement | null
      nativeNode: any
      // host dependency injector
      injector: Injector
      // element own component instance, if it has one
      componentInstance: any
      // provides parent context for this element
      // often an ancestor component instance that governs this element.
      // when an element is repeated within *ngFor, the context is an NgForRow
      // whose $implicit property is the value of the row instance value
      // for example, the hero in *ngFor="let hero of heroes"
      context: any
      // dictionary of objects associated with template local variables
      // (e.g. #foo), keyed by the local variable name
      references: {...}
      // this component injector lookup tokens
      // includes the component itself
      // plus the tokens that the component lists in its providers metadata
      providerTokens: any[]
    }

    // sync()
    it('...', waitForAsync(inject([AClass], (object) => {
      object.doSomething.then(() => {
        expect(...);
      })
    });

    // fakeAsync()
    describe('this test', () => {
      it('looks async but is synchronous', <any>fakeAsync((): void => {
        let flag = false;
        setTimeout(() => { flag = true; }, 100);
        expect(flag).toBe(false);
        tick(50);
        expect(flag).toBe(false);
        tick(50);
        expect(flag).toBe(true);
      }));
    });

    // tick()
    describe('this test', () => {
      it('looks async but is synchronous', <any>fakeAsync((): void => {
        let flag = false;
        setTimeout(() => { flag = true; }, 100);
        expect(flag).toBe(false);
        tick(50);
        expect(flag).toBe(false);
        tick(50);
        expect(flag).toBe(true);
      }));
    });

  
component class test

    // --- --- COMPONENT
    @Component({
      selector: 'lightswitch-comp',
      template: `
        <button (click)="clicked()">Click me!</button>
        <span>{{message}}</span>`
    })
    export class LightswitchComponent {
      // --- 1 - switch value
      isOn = false;
      clicked() { this.isOn = !this.isOn; }
      get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
      // --- 2 - binding
      @Input() hero: Hero;
      @Output() selected = new EventEmitter<Hero>();
      click() { this.selected.emit(this.hero); }
      // --- 3 - service dependencies
      welcome: string;
      constructor(private userService: UserService) { }
      ngOnInit(): void {
        this.welcome = this.userService.isLoggedIn ?
          'Welcome, ' + this.userService.user.name : 'Please log in.';
      }
    }
    // --- service
    class MockUserService {
      isLoggedIn = true;
      user = { name: 'Test User'};
    };

    // --- --- TEST
    // --- 1 - switch value
    it('#clicked() should toggle #isOn', () => {
      const comp = new LightswitchComponent();
      expect(comp.isOn).toBe(false, 'off at first');
      comp.clicked();
      expect(comp.isOn).toBe(true, 'on after click');
      comp.clicked();
      expect(comp.isOn).toBe(false, 'off after second click');
    });
    it('#clicked() should set #message to "is on"', () => {
      const comp = new LightswitchComponent();
      expect(comp.message).toMatch(/is off/i, 'off at first');
      comp.clicked();
      expect(comp.message).toMatch(/is on/i, 'on after clicked');
    });
    // --- 2 - binding
    it('raises the selected event when clicked', () => {
      const comp = new DashboardHeroComponent();
      const hero: Hero = { id: 42, name: 'Test' };
      comp.hero = hero;
      comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero));
      comp.click();
    });
    // --- 3 - service dependencies
    beforeEach(() => {
      TestBed.configureTestingModule({
        // provide the component-under-test and dependent service
        providers: [
          WelcomeComponent,
          { provide: UserService, useClass: MockUserService }
        ]
      });
      // inject both the component and the dependent service.
      comp = TestBed.inject(WelcomeComponent);
      userService = TestBed.inject(UserService);
    });
    it('should not have welcome message after construction', () => {
      expect(comp.welcome).toBeUndefined();
    });
    it('should welcome logged in user after Angular calls ngOnInit', () => {
      comp.ngOnInit();
      expect(comp.welcome).toContain(userService.user.name);
    });
    it('should ask user to log in if not logged in after ngOnInit', () => {
      userService.isLoggedIn = false;
      comp.ngOnInit();
      expect(comp.welcome).not.toContain(userService.user.name);
      expect(comp.welcome).toContain('log in');
    });
  
binding, DOM test, external urls and compileComponents()

    // --- 1 - initial
    import { Component } from '@angular/core';
    @Component({
      selector: 'app-banner',
      template: `<p>banner works!</p>`,
      styles: []
    })
    export class BannerComponent { }
    // --- 2 - with external urls
    import { Component } from '@angular/core';
    @Component({
      selector: 'app-banner',
      templateUrl: './banner-external.component.html',
      styleUrls:  ['./banner-external.component.css']
    })
    export class BannerComponent { title = 'Test Tour of Heroes'; }
    // --- 3
    import { Component } from '@angular/core';
    @Component({
      selector: 'app-banner',
      template: '<h1>{{title}}</h1>',
      styles: ['h1 { color: green; font-size: 350%}']
    })
    export class BannerComponent { title = 'Test Tour of Heroes'; }
  
--- --- TEST

    // --- 1 - initial
    import { By } from '@angular/platform-browser';
    import { DebugElement } from '@angular/core';
    import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
    import { BannerComponent } from './banner-initial.component';
    describe('BannerComponent (initial CLI generated)', () => {
      let component: BannerComponent;
      let fixture: ComponentFixture<BannerComponent>;
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          declarations: [ BannerComponent ]
        }).compileComponents();
      }));
      beforeEach(() => {
        fixture = TestBed.createComponent(BannerComponent);
        component = fixture.componentInstance;
        fixture.detectChanges();
      });
      it('should create', () => {
        expect(component).toBeDefined();
      });
    });
    describe('BannerComponent (minimal)', () => {
      it('should create', () => {
        TestBed.configureTestingModule({
          declarations: [ BannerComponent ]
        });
        const fixture = TestBed.createComponent(BannerComponent);
        const component = fixture.componentInstance;
        expect(component).toBeDefined();
      });
    });
    describe('BannerComponent (with beforeEach)', () => {
      let component: BannerComponent;
      let fixture: ComponentFixture<BannerComponent>;
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [ BannerComponent ]
        });
        fixture = TestBed.createComponent(BannerComponent);
        component = fixture.componentInstance;
      });
      it('should create', () => {
        expect(component).toBeDefined();
      });
      it('should contain "banner works!"', () => {
        const bannerElement: HTMLElement = fixture.nativeElement;
        expect(bannerElement.textContent).toContain('banner works!');
      });
      it('should have <p> with "banner works!"', () => {
        const bannerElement: HTMLElement = fixture.nativeElement;
        const p = bannerElement.querySelector('p');
        expect(p.textContent).toEqual('banner works!');
      });
      it('should find the <p> with fixture.debugElement.nativeElement)', () => {
        const bannerDe: DebugElement = fixture.debugElement;
        const bannerEl: HTMLElement = bannerDe.nativeElement;
        const p = bannerEl.querySelector('p');
        expect(p.textContent).toEqual('banner works!');
      });
      it('should find the <p> with fixture.debugElement.query(By.css)', () => {
        const bannerDe: DebugElement = fixture.debugElement;
        const paragraphDe = bannerDe.query(By.css('p'));
        const p: HTMLElement = paragraphDe.nativeElement;
        expect(p.textContent).toEqual('banner works!');
      });
    });

    // --- 2 - with external urls
    import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
    import { By }              from '@angular/platform-browser';
    import { DebugElement }    from '@angular/core';
    import { BannerComponent } from './banner-external.component';
    describe('BannerComponent (external files)', () => {
      let component: BannerComponent;
      let fixture:   ComponentFixture<BannerComponent>;
      let h1:        HTMLElement;
      describe('Two beforeEach', () => {
        beforeEach(waitForAsync(() => {
          TestBed.configureTestingModule({
            declarations: [ BannerComponent ],
          })
          .compileComponents();  // compile template and css
        }));
        // synchronous beforeEach
        beforeEach(() => {
          fixture = TestBed.createComponent(BannerComponent);
          component = fixture.componentInstance; // BannerComponent test instance
          h1 = fixture.nativeElement.querySelector('h1');
        });
        tests();
      });
      describe('One beforeEach', () => {
        beforeEach(waitForAsync(() => {
          TestBed.configureTestingModule({
            declarations: [ BannerComponent ],
          })
          .compileComponents()
          .then(() => {
            fixture = TestBed.createComponent(BannerComponent);
            component = fixture.componentInstance;
            h1 = fixture.nativeElement.querySelector('h1');
          });
        }));
        tests();
      });
      function tests() {
        it('no title in the DOM until manually call `detectChanges`', () => {
          expect(h1.textContent).toEqual('');
        });
        it('should display original title', () => {
          fixture.detectChanges();
          expect(h1.textContent).toContain(component.title);
        });
        it('should display a different test title', () => {
          component.title = 'Test Title';
          fixture.detectChanges();
          expect(h1.textContent).toContain('Test Title');
        });
      }
    });

    // --- 3
    import { BannerComponent } from './banner.component';
    describe('BannerComponent (inline template)', () => {
      let component: BannerComponent;
      let fixture:   ComponentFixture<BannerComponent>;
      let h1:        HTMLElement;
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [ BannerComponent ],
        });
        fixture = TestBed.createComponent(BannerComponent);
        component = fixture.componentInstance; // BannerComponent test instance
        h1 = fixture.nativeElement.querySelector('h1');
      });
      it('no title in the DOM after createComponent()', () => {
        expect(h1.textContent).toEqual('');
      });
      it('should display original title', () => {
        fixture.detectChanges();
        expect(h1.textContent).toContain(component.title);
      });
      it('should display original title after detectChanges()', () => {
        fixture.detectChanges();
        expect(h1.textContent).toContain(component.title);
      });
      it('should display a different test title', () => {
        component.title = 'Test Title';
        fixture.detectChanges();
        expect(h1.textContent).toContain('Test Title');
      });
    });
    // --- banner.component.detect-changes.spec.ts
    describe('BannerComponent (AutoChangeDetect)', () => {
      let comp:    BannerComponent;
      let fixture: ComponentFixture<BannerComponent>;
      let h1:      HTMLElement;
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [ BannerComponent ],
          providers: [
            { provide: ComponentFixtureAutoDetect, useValue: true }
          ]
        });
        fixture = TestBed.createComponent(BannerComponent);
        comp = fixture.componentInstance;
        h1 = fixture.nativeElement.querySelector('h1');
      });
      it('should display original title', () => {
        // Hooray! No `fixture.detectChanges()` needed
        expect(h1.textContent).toContain(comp.title);
      });
      it('should still see original title after comp.title change', () => {
        const oldTitle = comp.title;
        comp.title = 'Test Title';
        // Displayed title is old because Angular didn't hear the change :(
        expect(h1.textContent).toContain(oldTitle);
      });
      it('should display updated title after detectChanges', () => {
        comp.title = 'Test Title';
        fixture.detectChanges(); // detect changes explicitly
        expect(h1.textContent).toContain(comp.title);
      });
    });
  
component with a dependency

    // --- --- COMPONENT
    import { Component, OnInit } from '@angular/core';
    import { UserService }       from '../model/user.service';
    @Component({
      selector: 'app-welcome',
      template: '<h3 class="welcome"><i>{{welcome}}</i></h3>'
    })
    export class WelcomeComponent  implements OnInit {
      welcome: string;
      constructor(private userService: UserService) { }
      ngOnInit(): void {
        this.welcome = this.userService.isLoggedIn ?
          'Welcome, ' + this.userService.user.name : 'Please log in.';
      }
    }
    // --- --- TEST
    let userServiceStub: Partial<UserService>;
    beforeEach(() => {
      // stub UserService for test purposes
      userServiceStub = {
        isLoggedIn: true,
        user: { name: 'Test User'}
      };
      TestBed.configureTestingModule({
          declarations: [ WelcomeComponent ],
          providers:    [ {provide: UserService, useValue: userServiceStub } ]
      });
      fixture = TestBed.createComponent(WelcomeComponent);
      comp    = fixture.componentInstance;
      // UserService from the root injector
      userService = TestBed.inject(UserService);
      //  get the "welcome" element by CSS selector (e.g., by class name)
      el = fixture.nativeElement.querySelector('.welcome');
    });
    it('should welcome the user', () => {
      fixture.detectChanges();
      const content = el.textContent;
      expect(content).toContain('Welcome', '"Welcome ..."');
      expect(content).toContain('Test User', 'expected name');
    });
    it('should welcome "Bubba"', () => {
      userService.user.name = 'Bubba'; // welcome message hasn't been shown yet
      fixture.detectChanges();
      expect(el.textContent).toContain('Bubba');
    });
    it('should request login if not logged in', () => {
      userService.isLoggedIn = false; // welcome message hasn't been shown yet
      fixture.detectChanges();
      const content = el.textContent;
      expect(content).not.toContain('Welcome', 'not welcomed');
      expect(content).toMatch(/log in/i, '"log in"');
    });
  
component with sync and async service

    // --- --- COMPONENT
    import { TwainService } from './twain.service';
    @Component({
      selector: 'twain-quote',
      template: `
        <p class="twain"><i>{{quote | async}}</i></p>
        <button (click)="getQuote()">Next quote</button>
        <p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>`,
      styles: [ `.twain { font-style: italic; } .error { color: red; }` ]
    })
    export class TwainComponent implements OnInit {
      errorMessage: string;
      quote: Observable<string>;
      constructor(private twainService: TwainService) {}
      ngOnInit(): void { this.getQuote(); }
      getQuote() {
        this.errorMessage = '';
        this.quote = this.twainService.getQuote().pipe(
          startWith('...'),
          catchError( (err: any) => { // wait a turn, errorMessage already set once
            setTimeout(() => this.errorMessage = err.message || err.toString());
            return of('...'); // reset message to placeholder
          })
        );
      }
    }
    // Mark Twain Quote service gets quotes from server
    @Injectable()
    export class TwainService {
      constructor(private http: HttpClient) { }
      private nextId = 1;
      getQuote(): Observable<string> {
        return Observable.create(observer => observer.next(this.nextId++)).pipe(
          // tap((id: number) => console.log(id)),
          // tap((id: number) => { throw new Error('Simulated server error'); }),
          switchMap((id: number) => this.http.get<Quote>(`api/quotes/${id}`)),
          // tap((q : Quote) => console.log(q)),
          map((q: Quote) => q.quote),
          // `errors` is observable of http.get errors
          retryWhen(errors => errors.pipe(
            switchMap((error: HttpErrorResponse)  => {
              if (error.status === 404) {
                // Queried for quote that doesn't exist.
                this.nextId = 1; // retry with quote id:1
                return of(null); // signal OK to retry
              }
              // Some other HTTP error.
              console.error(error);
              return throwError('Cannot get Twain quotes from the server');
            }),
            take(2),
            // If a second retry value, then didn't find id:1 and triggers the following error
            concat(throwError('There are no Twain quotes')) // didn't find id:1
          ))
        );
      }
    }

    // --- --- TEST
    // --- async observable helpers, to return an asynchronous observable
    import { defer } from 'rxjs';
    // async observable that emits-once and completes after a JS engine turn
    export function asyncData<T>(data: T) {
      return defer(() => Promise.resolve(data));
    }
    // async observable error that errors after a JS engine turn
    export function asyncError<T>(errorObject: any) {
      return defer(() => Promise.reject(errorObject));
    }
    // ---
    import {
      waitForAsync, fakeAsync, ComponentFixture, TestBed, tick
    } from '@angular/core/testing';
    import { asyncData, asyncError }  from '../../testing';
    import { of, throwError, interval } from 'rxjs';
    import { last, delay, take } from 'rxjs/operators';
    import { TwainService }   from './twain.service';
    import { TwainComponent } from './twain.component';
    import { cold, getTestScheduler } from 'jasmine-marbles';
    describe('TwainComponent', () => {
      let component: TwainComponent;
      let fixture: ComponentFixture<TwainComponent>;
      let getQuoteSpy: jasmine.Spy;
      let quoteEl: HTMLElement;
      let testQuote: string;

      // Helper function to get the error message element value
      // An *ngIf keeps it out of the DOM until there is an error
      const errorMessage = () => {
        const el = fixture.nativeElement.querySelector('.error');
        return el ? el.textContent : null;
      };

      beforeEach(() => {
        testQuote = 'Test Quote';
        // Create a fake TwainService object with a `getQuote()` spy
        const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
        // Make the spy return a synchronous Observable with the test data
        getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );
        TestBed.configureTestingModule({
          declarations: [ TwainComponent ],
          providers:    [
            { provide: TwainService, useValue: twainService }
          ]
        });
        fixture = TestBed.createComponent(TwainComponent);
        component = fixture.componentInstance;
        quoteEl = fixture.nativeElement.querySelector('.twain');
      });

      describe('when test with synchronous observable', () => {
        it('should not show quote before OnInit', () => {
          expect(quoteEl.textContent).toBe('', 'nothing displayed');
          expect(errorMessage()).toBeNull('should not show error element');
          expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
        });

        // The quote would not be immediately available if the service were truly async
        it('should show quote after component initialized', () => {
          fixture.detectChanges(); // onInit()
          // sync spy result shows testQuote immediately after init
          expect(quoteEl.textContent).toBe(testQuote);
          expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
        });

        // The error would not be immediately available if the service were truly async
        // Use `fakeAsync` because the component error calls `setTimeout`
        it('should display error when TwainService fails',
        fakeAsync(() => {
          // tell spy to return an error observable
          getQuoteSpy.and.returnValue(
            throwError('TwainService test failure'));
          fixture.detectChanges(); // onInit()
          // sync spy errors immediately after init
          tick(); // flush the component's setTimeout()
          fixture.detectChanges(); // update errorMessage within setTimeout()
          expect(errorMessage()).toMatch(/test failure/, 'should display error');
          expect(quoteEl.textContent).toBe('...', 'should show placeholder');
        }));
      });

      describe('when test with asynchronous observable', () => {
        beforeEach(() => {
          // Simulate delayed observable values with the `asyncData()` helper
          getQuoteSpy.and.returnValue(asyncData(testQuote));
        });

        it('should not show quote before OnInit',
        () => {
          expect(quoteEl.textContent).toBe('', 'nothing displayed');
          expect(errorMessage()).toBeNull('should not show error element');
          expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
        });

        it('should still not show quote after component initialized',
        () => {
          fixture.detectChanges();
          // getQuote service is async => still has not returned with quote
          // so should show the start value, '...'
          expect(quoteEl.textContent).toBe('...', 'should show placeholder');
          expect(errorMessage()).toBeNull('should not show error');
          expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
        });

        it('should show quote after getQuote (fakeAsync)',
        fakeAsync(() => {
          fixture.detectChanges(); // ngOnInit()
          expect(quoteEl.textContent).toBe('...', 'should show placeholder');
          tick(); // flush the observable to get the quote
          fixture.detectChanges(); // update view
          expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
          expect(errorMessage()).toBeNull('should not show error');
        }));

        it('should show quote after getQuote (async)',
        waitForAsync(() => {
          fixture.detectChanges(); // ngOnInit()
          expect(quoteEl.textContent).toBe('...', 'should show placeholder');
          fixture.whenStable().then(() => { // wait for async getQuote
            fixture.detectChanges();        // update view with quote
            expect(quoteEl.textContent).toBe(testQuote);
            expect(errorMessage()).toBeNull('should not show error');
          });
        }));

        it('should show last quote (quote done)',
        (done: DoneFn) => {
          fixture.detectChanges();
          component.quote.pipe( last() ).subscribe(() => {
            fixture.detectChanges(); // update view with quote
            expect(quoteEl.textContent).toBe(testQuote);
            expect(errorMessage()).toBeNull('should not show error');
            done();
          });
        });

        it('should show quote after getQuote (spy done)',
        (done: DoneFn) => {
          fixture.detectChanges();
          // the spy's most recent call returns the observable with the test quote
          getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
            fixture.detectChanges(); // update view with quote
            expect(quoteEl.textContent).toBe(testQuote);
            expect(errorMessage()).toBeNull('should not show error');
            done();
          });
        });

        it('should display error when TwainService fails',
        fakeAsync(() => {
          // tell spy to return an async error observable
          getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));
          fixture.detectChanges();
          tick();                  // component shows error after a setTimeout()
          fixture.detectChanges(); // update error message
          expect(errorMessage()).toMatch(/test failure/, 'should display error');
          expect(quoteEl.textContent).toBe('...', 'should show placeholder');
        }));
      });

      // --- marbles
      // A synchronous test that simulates async behavior
      it('should show quote after getQuote (marbles)', () => {
        // observable test quote value and complete(), after delay:
        // cold observable that waits three frames (---),
        // emits a value (x), and completes (|)
        // second argument (x) is to the emitted value (testQuote)
        const q$ = cold('---x|', { x: testQuote });
        getQuoteSpy.and.returnValue( q$ );
        fixture.detectChanges(); // ngOnInit()
        expect(quoteEl.textContent).toBe('...', 'should show placeholder');
        getTestScheduler().flush(); // flush the observables
        fixture.detectChanges(); // update view
        expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
        expect(errorMessage()).toBeNull('should not show error');
      });
      // Still need fakeAsync() because of component setTimeout()
      it('should display error when TwainService fails', fakeAsync(() => {
        // observable error after delay
        const q$ = cold('---#|', null, new Error('TwainService test failure'));
        getQuoteSpy.and.returnValue( q$ );
        fixture.detectChanges(); // ngOnInit()
        expect(quoteEl.textContent).toBe('...', 'should show placeholder');
        getTestScheduler().flush(); // flush the observables
        tick();                     // component shows error after a setTimeout()
        fixture.detectChanges();    // update error message
        expect(errorMessage()).toMatch(/test failure/, 'should display error');
        expect(quoteEl.textContent).toBe('...', 'should show placeholder');
      }));

    });

    // --- --- fakeAsync sample
    describe('this test', () => {
      it('looks async but is synchronous',
      <any>fakeAsync((): void => {
        let flag = false;
        setTimeout(() => { flag = true; }, 100);
        expect(flag).toBe(false);
        tick(50);
        expect(flag).toBe(false);
        tick(50);
        expect(flag).toBe(true);
      }));
    });
    it('should get Date diff correctly in fakeAsync',
    fakeAsync(() => {
      const start = Date.now();
      tick(100);
      const end = Date.now();
      expect(end - start).toBe(100);
    }));
    it('should get Date diff correctly in fakeAsync with rxjs scheduler',
    fakeAsync(() => {
      // need to add `import 'zone.js/dist/zone-patch-rxjs-fake-async'
      // to patch rxjs scheduler
      let result = null;
      of ('hello').pipe(delay(1000)).subscribe(v => { result = v; });
      expect(result).toBeNull();
      tick(1000);
      expect(result).toBe('hello');

      const start = new Date().getTime();
      let dateDiff = 0;
      interval(1000).pipe(take(2)).subscribe(() => dateDiff = (new Date().getTime() - start));

      tick(1000);
      expect(dateDiff).toBe(1000);
      tick(1000);
      expect(dateDiff).toBe(2000);
    }));

    // --- --- jasmine.clock() - auto enter fakeAsync
    // --- src/test.ts
    (window as any)['__zone_symbol__fakeAsyncPatchLock'] = true;
    import 'zone.js/dist/zone-testing';
    // ---
    describe('use jasmine.clock()', () => {
      // need to config __zone_symbol__fakeAsyncPatchLock flag
      // before loading zone.js/dist/zone-testing
      beforeEach(() => { jasmine.clock().install(); });
      afterEach(() => { jasmine.clock().uninstall(); });
      it('should auto enter fakeAsync', () => {
        // is in fakeAsync now, don't need to call fakeAsync(testFn)
        let called = false;
        setTimeout(() => { called = true; }, 100);
        jasmine.clock().tick(100);
        expect(called).toBe(true);
      });
    });
  
macroTasks in fakeAsync

    // --- --- COMPONENT
    import { Component, AfterViewInit, ViewChild } from '@angular/core';
    @Component({
      selector: 'sample-canvas',
      template: '<canvas #sampleCanvas width="200" height="200"></canvas>'
    })
    export class CanvasComponent implements AfterViewInit {
      blobSize: number;
      @ViewChild('sampleCanvas') sampleCanvas;
      constructor() { }
      ngAfterViewInit() {
        const canvas = this.sampleCanvas.nativeElement;
        const context = canvas.getContext('2d');
        if (context) {
          context.clearRect(0, 0, 200, 200);
          context.fillStyle = '#FF1122';
          context.fillRect(0, 0, 200, 200);
          canvas.toBlob((blob: any) => {
            this.blobSize = blob.size;
          });
        }
      }
    }

    // --- --- TEST
    import { TestBed, waitForAsync, tick, fakeAsync } from '@angular/core/testing';
    import { CanvasComponent } from './canvas.component';
    describe('CanvasComponent', () => {
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          declarations: [
            CanvasComponent
          ],
        }).compileComponents();
      }));
      beforeEach(() => {
        window['__zone_symbol__FakeAsyncTestMacroTask'] = [
          {
            source: 'HTMLCanvasElement.toBlob',
            callbackArgs: [{ size: 200 }]
          }
        ];
      });
      it('should be able to generate blob data from canvas', fakeAsync(() => {
        const fixture = TestBed.createComponent(CanvasComponent);
        fixture.detectChanges();
        tick();
        const app = fixture.debugElement.componentInstance;
        expect(app.blobSize).toBeGreaterThan(0);
      }));
    });
  
inputs/outputs, host component, click (events), routing/routed component

    // --- --- HOST COMPONENT
    // set each component hero input property to the looping value
    // and listens for the component "selected" event:
    @Component({
      selector: 'app-dashboard',
      template: `
        <div class="grid grid-pad">
          <dashboard-hero *ngFor="let hero of heroes"  class="col-1-4"
            [hero]=hero  (selected)="gotoDetail($event)" >
          </dashboard-hero>
        </div>`,
      styleUrls: [ './dashboard.component.css' ]
    })
    export class DashboardComponent implements OnInit {
      heroes: Hero[] = [];
      constructor(
        private router: Router,
        private heroService: HeroService
      ) { }
      ngOnInit() {
        this.heroService.getHeroes()
          .subscribe(heroes => this.heroes = heroes.slice(1, 5));
      }
      gotoDetail(hero: Hero) {
        let url = `/heroes/${hero.id}`;
        this.router.navigateByUrl(url);
      }
      get title() {
        let cnt = this.heroes.length;
        return cnt === 0 ? 'No Heroes' :
          cnt === 1 ? 'Top Hero' :  `Top ${cnt} Heroes`;
      }
    }
    // --- --- COMPONENT
    @Component({
      selector: 'dashboard-hero',
      template: `
        <div (click)="click()" class="hero">
          {{hero.name | uppercase}}
        </div>`,
      styleUrls: [ './dashboard-hero.component.css' ]
    })
    export class DashboardHeroComponent {
      @Input() hero: Hero;
      @Output() selected = new EventEmitter<Hero>();
      click() { this.selected.emit(this.hero); }
    }

    // --- --- TEST
  
--- dashboard-hero.component.spec.ts

    // --- click helper from '../../testing'
    // button events to pass to "DebugElement.triggerEventHandler"
    // for RouterLink event handler
    export const ButtonClickEvents = {
      left:  { button: 0 },
      right: { button: 2 }
    };
    // Simulate element click. Defaults to mouse left-button click event
    export function click(
      el: DebugElement | HTMLElement, eventObj: any = ButtonClickEvents.left
    ): void {
      if (el instanceof HTMLElement) {
        el.click();
      } else {
        el.triggerEventHandler('click', eventObj);
      }
    }
    // ---
    import {
      waitForAsync, ComponentFixture, TestBed
    } from '@angular/core/testing';
    import { By } from '@angular/platform-browser';
    import { DebugElement } from '@angular/core';
    import { addMatchers, click } from '../../testing';
    import { Hero } from '../model/hero';
    import { DashboardHeroComponent } from './dashboard-hero.component';
    beforeEach( addMatchers );

    describe('DashboardHeroComponent class only', () => {
      it('raises the selected event when clicked', () => {
        const comp = new DashboardHeroComponent();
        const hero: Hero = { id: 42, name: 'Test' };
        comp.hero = hero;
        comp.selected.subscribe(
          selectedHero => expect(selectedHero).toBe(hero)
        );
        comp.click();
      });
    });

    describe('DashboardHeroComponent when tested stand-alone (directly)', () => {
      let comp: DashboardHeroComponent;
      let expectedHero: Hero;
      let fixture: ComponentFixture<DashboardHeroComponent>;
      let heroDe: DebugElement;
      let heroEl: HTMLElement;
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          declarations: [ DashboardHeroComponent ]
        }).compileComponents();
      }));
      beforeEach(() => {
        fixture = TestBed.createComponent(DashboardHeroComponent);
        comp    = fixture.componentInstance;
        // find the hero's DebugElement and element
        heroDe  = fixture.debugElement.query(By.css('.hero'));
        heroEl = heroDe.nativeElement;
        // mock the hero supplied by the parent component
        expectedHero = { id: 42, name: 'Test Name' };
        // simulate the parent setting the input property with that hero
        comp.hero = expectedHero;
        // trigger initial data binding
        fixture.detectChanges();
      });
      it('should display hero name in uppercase', () => {
        const expectedPipedName = expectedHero.name.toUpperCase();
        expect(heroEl.textContent).toContain(expectedPipedName);
      });

      it('should raise selected event when clicked (triggerEventHandler)', () => {
        let selectedHero: Hero;
        comp.selected.subscribe((hero: Hero) => selectedHero = hero);
        heroDe.triggerEventHandler('click', null);
        expect(selectedHero).toBe(expectedHero);
      });
      it('should raise selected event when clicked (element.click)', () => {
        let selectedHero: Hero;
        comp.selected.subscribe((hero: Hero) => selectedHero = hero);
        heroEl.click();
        expect(selectedHero).toBe(expectedHero);
      });
      it('should raise selected event when clicked (click helper)', () => {
        let selectedHero: Hero;
        comp.selected.subscribe(hero => selectedHero = hero);
        click(heroDe); // click helper with DebugElement
        click(heroEl); // click helper with native element
        expect(selectedHero).toBe(expectedHero);
      });

    });

    describe('DashboardHeroComponent when inside a TestHostComponent', () => {
      let testHost: TestHostComponent;
      let fixture: ComponentFixture<TestHostComponent>;
      let heroEl: HTMLElement;
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          declarations: [ DashboardHeroComponent, TestHostComponent ]
        }).compileComponents();
      }));
      beforeEach(() => {
        // create TestHostComponent instead of DashboardHeroComponent
        fixture  = TestBed.createComponent(TestHostComponent);
        testHost = fixture.componentInstance;
        heroEl   = fixture.nativeElement.querySelector('.hero');
        fixture.detectChanges(); // trigger initial data binding
      });
      it('should display hero name', () => {
        const expectedPipedName = testHost.hero.name.toUpperCase();
        expect(heroEl.textContent).toContain(expectedPipedName);
      });
      it('should raise selected event when clicked', () => {
        click(heroEl);
        // selected hero should be the same data bound hero
        expect(testHost.selectedHero).toBe(testHost.hero);
      });
    });

    // TestHostComponent
    import { Component } from '@angular/core';
    @Component({
      template: `
        <dashboard-hero
          [hero]="hero" (selected)="onSelected($event)">
        </dashboard-hero>`
    })
    class TestHostComponent {
      hero: Hero = { id: 42, name: 'Test Name' };
      selectedHero: Hero;
      onSelected(hero: Hero) { this.selectedHero = hero; }
    }
  
--- dashboard.component.spec.ts

    import {
      waitForAsync, inject, ComponentFixture, TestBed
    } from '@angular/core/testing';
    import { addMatchers, asyncData, click } from '../../testing';
    import { HeroService }   from '../model/hero.service';
    import { getTestHeroes } from '../model/testing/test-heroes';
    import { By }     from '@angular/platform-browser';
    import { Router } from '@angular/router';
    import { DashboardComponent } from './dashboard.component';
    import { DashboardModule }    from './dashboard.module';
    beforeEach ( addMatchers );
    let comp: DashboardComponent;
    let fixture: ComponentFixture<DashboardComponent>;
    // Deep
    describe('DashboardComponent (deep)', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          imports: [ DashboardModule ]
        });
      });
      compileAndCreate();
      tests(clickForDeep);
      function clickForDeep() {
        // get first <div class="hero">
        const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero');
        click(heroEl);
      }
    });
    // Shallow
    import { NO_ERRORS_SCHEMA } from '@angular/core';
    describe('DashboardComponent (shallow)', () => {
      beforeEach(() => {
        TestBed.configureTestingModule({
          declarations: [ DashboardComponent ],
          schemas:      [NO_ERRORS_SCHEMA]
        });
      });
      compileAndCreate();
      tests(clickForShallow);
      function clickForShallow() {
        // get first <dashboard-hero> DebugElement
        const heroDe = fixture.debugElement.query(By.css('dashboard-hero'));
        heroDe.triggerEventHandler('selected', comp.heroes[0]);
      }
    });
    // Add TestBed providers, compile, and create DashboardComponent
    function compileAndCreate() {
      beforeEach(waitForAsync(() => {
        const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
        const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
        TestBed.configureTestingModule({
          providers: [
            { provide: HeroService, useValue: heroServiceSpy },
            { provide: Router,      useValue: routerSpy }
          ]
        })
        .compileComponents().then(() => {
          fixture = TestBed.createComponent(DashboardComponent);
          comp = fixture.componentInstance;
          // getHeroes spy returns observable of test heroes
          heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
        });
      }));
    }

    // (almost) same tests for both
    // only change: the way that the first hero is clicked
    function tests(heroClick: Function) {
      it('should NOT have heroes before ngOnInit', () => {
        expect(comp.heroes.length).toBe(0,
          'should not have heroes before ngOnInit');
      });
      it('should NOT have heroes immediately after ngOnInit', () => {
        fixture.detectChanges(); // runs initial lifecycle hooks
        expect(comp.heroes.length).toBe(0,
          'should not have heroes until service promise resolves');
      });
      describe('after get dashboard heroes', () => {
        let router: Router;
        // Trigger component so it gets heroes and binds to them
        beforeEach(waitForAsync(() => {
          router = fixture.debugElement.injector.get(Router);
          fixture.detectChanges(); // runs ngOnInit -> getHeroes
          fixture.whenStable() // No need for the `lastPromise` hack!
            .then(() => fixture.detectChanges()); // bind to heroes
        }));
        it('should HAVE heroes', () => {
          expect(comp.heroes.length).toBeGreaterThan(0,
            'should have heroes after service promise resolves');
        });
        it('should DISPLAY heroes', () => {
          // Find and examine the displayed heroes
          // Look for them in the DOM by css class
          const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero');
          expect(heroes.length).toBe(4, 'should display 4 heroes');
        });

        it('should tell ROUTER to navigate when hero clicked', () => {
          heroClick(); // trigger click on first inner <div class="hero">
          // args passed to router.navigateByUrl() spy
          const spy = router.navigateByUrl as jasmine.Spy;
          const navArgs = spy.calls.first().args[0];
          // expecting to navigate to id of the component's first hero
          const id = comp.heroes[0].id;
          expect(navArgs).toBe('/heroes/' + id,
            'should nav to HeroDetail for first hero');
        });

      });
    }
  
--- dashboard.component.no-testbed.spec.ts

    import { Router } from '@angular/router';
    import { DashboardComponent } from './dashboard.component';
    import { Hero }               from '../model/hero';
    import { addMatchers }     from '../../testing';
    import { TestHeroService, HeroService } from '../model/testing/test-hero.service';
    class FakeRouter { navigateByUrl(url: string) { return url;  } }
    describe('DashboardComponent class only', () => {
      let comp: DashboardComponent;
      let heroService: TestHeroService;
      let router: Router;

      beforeEach(() => {
        addMatchers();
        router = new FakeRouter() as any as Router;
        heroService = new TestHeroService();
        comp = new DashboardComponent(router, heroService);
      });

      it('should NOT have heroes before calling OnInit', () => {
        expect(comp.heroes.length).toBe(0,
          'should not have heroes before OnInit');
      });
      it('should NOT have heroes immediately after OnInit', () => {
        comp.ngOnInit(); // ngOnInit -> getHeroes
        expect(comp.heroes.length).toBe(0,
          'should not have heroes until service promise resolves');
      });
      it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
        comp.ngOnInit(); // ngOnInit -> getHeroes
        heroService.lastResult // the one from getHeroes
          .subscribe(
            () => {
            // throw new Error('deliberate error'); // see it fail gracefully
            expect(comp.heroes.length).toBeGreaterThan(0,
              'should have heroes after service promise resolves');
            done();
          },
          done.fail);
      });

      it('should tell ROUTER to navigate by hero id', () => {
        const hero: Hero = {id: 42, name: 'Abbracadabra' };
        const spy = spyOn(router, 'navigateByUrl');
        comp.gotoDetail(hero);
        const navArgs = spy.calls.mostRecent().args[0];
        expect(navArgs).toBe('/heroes/42', 'should nav to HeroDetail for Hero 42');
      });

    });
  
module imports setups, ActivatedRouteStub, page object as component stub

    // --- hero-detail.component.ts
    import { HeroDetailService } from './hero-detail.service';
    @Component({
      selector:    'app-hero-detail',
      template: `
        <div *ngIf="hero">
          <h2><span>{{hero.name | titlecase}}</span> Details</h2>
          <div>
            <label>id: </label>{{hero.id}}</div>
          <div>
            <label for="name">name: </label>
            <input id="name" [(ngModel)]="hero.name" placeholder="name" />
          </div>
          <button (click)="save()">Save</button>
          <button (click)="cancel()">Cancel</button>
        </div>`,
      providers:  [ HeroDetailService ]
    })
    export class HeroDetailComponent implements OnInit {
      constructor(
        private heroDetailService: HeroDetailService,
        private route:  ActivatedRoute,
        private router: Router) {
      }
      @Input() hero: Hero;
      ngOnInit(): void {
        // get hero when `id` param changes
        this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
      }
      private getHero(id: string): void {
        // when no id or id===0, create new blank hero
        if (!id) {
          this.hero = { id: 0, name: '' } as Hero;
          return;
        }
        this.heroDetailService.getHero(id).subscribe(hero => {
          if (hero) {
            this.hero = hero;
          } else {
            this.gotoList(); // id not found; navigate to list
          }
        });
      }
      save(): void {
        this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());
      }
      cancel() { this.gotoList(); }
      gotoList() {
        this.router.navigate(['../'], {relativeTo: this.route});
      }
    }

    // --- hero-list.component.ts
    import { HeroService } from '../model/hero.service';
    @Component({
      selector: 'app-heroes',
      templateUrl: `
        <ul class="heroes">
          <li *ngFor="let hero of heroes | async "
            [class.selected]="hero === selectedHero"
            (click)="onSelect(hero)">
            <span class="badge">{{hero.id}}</span> {{hero.name}}
          </li>
        </ul>`
    })
    export class HeroListComponent implements OnInit {
      heroes: Observable<Hero[]>;
      selectedHero: Hero;
      constructor(
        private router: Router,
        private heroService: HeroService
      ) { }
      ngOnInit() {
        this.heroes = this.heroService.getHeroes();
      }
      onSelect(hero: Hero) {
        this.selectedHero = hero;
        this.router.navigate(['../heroes', this.selectedHero.id ]);
      }
    }

    // --- --- TEST
  
--- hero-detail.component.spec.ts

    import {
      waitForAsync, ComponentFixture, fakeAsync, inject, TestBed, tick
    } from '@angular/core/testing';
    import { Router }       from '@angular/router';
    import {
      ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
    } from '../../testing';
    import { Hero }                from '../model/hero';
    import { HeroDetailComponent } from './hero-detail.component';
    import { HeroDetailService }   from './hero-detail.service';
    import { HeroModule }          from './hero.module';
    // ---
    let activatedRoute: ActivatedRouteStub;
    let component: HeroDetailComponent;
    let fixture: ComponentFixture<HeroDetailComponent>;
    let page: Page;
    // --- TESTS CALLS
    describe('HeroDetailComponent', () => {
      beforeEach(() => {
        activatedRoute = new ActivatedRouteStub();
      });
      // --- import own feature module, when many mutual dependencies within the module
      describe('with HeroModule setup', heroModuleSetup);
      // --- fake all necessary features of a nested services with stub spy
      describe('when override its provided HeroDetailService', overrideSetup);
      // --- configure testing module from individual pieces
      describe('with FormsModule setup', formsModuleSetup);
      // --- combine frequently requested parts
      describe('with SharedModule setup', sharedModuleSetup);
    });

    // --- HELPERS
    // create the HeroDetailComponent, initialize it, set test variables
    function createComponent() {
      fixture = TestBed.createComponent(HeroDetailComponent);
      component = fixture.componentInstance;
      page = new Page(fixture);
      // 1st change detection triggers ngOnInit which gets a hero
      fixture.detectChanges();
      return fixture.whenStable().then(() => {
        // 2nd change detection displays the async-fetched hero
        fixture.detectChanges();
      });
    }
    class Page {
      // getter properties wait to query the DOM until called.
      get buttons()     {
        return this.queryAll<HTMLButtonElement>('button'); }
      get saveBtn()     {
        return this.buttons[0]; }
      get cancelBtn()   {
        return this.buttons[1]; }
      get nameDisplay() {
        return this.query<HTMLElement>('span'); }
      get nameInput()   {
        return this.query<HTMLInputElement>('input'); }
      gotoListSpy: jasmine.Spy;
      navigateSpy:  jasmine.Spy;
      constructor(fixture: ComponentFixture<HeroDetailComponent>) {
        // get the navigate spy from the injected router spy object
        const routerSpy = <any> fixture.debugElement.injector.get(Router);
        this.navigateSpy = routerSpy.navigate;
        // spy on component `gotoList()` method
        const component = fixture.componentInstance;
        this.gotoListSpy = spyOn(component, 'gotoList').and.callThrough();
      }
      // query helpers
      private query<T>(selector: string): T {
        return fixture.nativeElement.querySelector(selector);
      }
      private queryAll<T>(selector: string): T[] {
        return fixture.nativeElement.querySelectorAll(selector);
      }
    }
    function createRouterSpy() {
      return jasmine.createSpyObj('Router', ['navigate']);
    }
    // ActivatedRouteStub - ActivateRoute test double with a `paramMap` observable
    // use the `setParamMap()` method to add the next `paramMap` value
    export { ActivatedRoute } from '@angular/router';
    import { convertToParamMap, ParamMap, Params } from '@angular/router';
    import { ReplaySubject } from 'rxjs';
    export class ActivatedRouteStub {
      // Use a ReplaySubject to share previous values with subscribers
      // and pump new values into the `paramMap` observable
      private subject = new ReplaySubject<ParamMap>();
      constructor(initialParams?: Params) {
        this.setParamMap(initialParams);
      }
      // The mock paramMap observable
      readonly paramMap = this.subject.asObservable();
      // Set the paramMap observables's next value
      setParamMap(params?: Params) {
        this.subject.next(convertToParamMap(params));
      };
    }
  
--- --- heroModuleSetup() - import own feature module

    import {
      getTestHeroes, TestHeroService, HeroService
    } from '../model/testing/test-hero.service';
    const firstHero = getTestHeroes()[0];
    function heroModuleSetup() {
      beforeEach(waitForAsync(() => {
        const routerSpy = createRouterSpy();
        TestBed.configureTestingModule({
          imports:   [ HeroModule ],
          // declarations: [ HeroDetailComponent ], // NO!  DOUBLE DECLARATION
          providers: [
            { provide: ActivatedRoute, useValue: activatedRoute },
            { provide: HeroService,    useClass: TestHeroService },
            { provide: Router,         useValue: routerSpy},
          ]
        }).compileComponents();
      }));
      describe('when navigate to existing hero', () => {
        let expectedHero: Hero;
        beforeEach(waitForAsync(() => {
          expectedHero = firstHero;
          activatedRoute.setParamMap({ id: expectedHero.id });
          createComponent();
        }));
        it('should display that hero name', () => {
          expect(page.nameDisplay.textContent).toBe(expectedHero.name);
        });
        it('should navigate when click cancel', () => {
          click(page.cancelBtn);
          expect(page.navigateSpy.calls.any())
            .toBe(true, 'router.navigate called');
        });
        it('should save when click save but not navigate immediately', () => {
          // Get service injected into component and spy on its`saveHero` method.
          // It delegates to fake `HeroService.updateHero` which delivers a safe test result.
          const hds = fixture.debugElement.injector.get(HeroDetailService);
          const saveSpy = spyOn(hds, 'saveHero').and.callThrough();
          click(page.saveBtn);
          expect(saveSpy.calls.any())
            .toBe(true, 'HeroDetailService.save called');
          expect(page.navigateSpy.calls.any())
            .toBe(false, 'router.navigate not called');
        });
        it('should navigate when click save and save resolves', fakeAsync(() => {
          click(page.saveBtn);
          tick(); // wait for async save to complete
          expect(page.navigateSpy.calls.any())
            .toBe(true, 'router.navigate called');
        }));
        it('should convert hero name to Title Case', () => {
          // get the name's input and display elements from the DOM
          const hostElement = fixture.nativeElement;
          const nameInput: HTMLInputElement = hostElement.querySelector('input');
          const nameDisplay: HTMLElement = hostElement.querySelector('span');
          // simulate user entering a new name into the input box
          nameInput.value = 'quick BROWN  fOx';
          // dispatch a DOM event so that Angular learns of input value change.
          nameInput.dispatchEvent(newEvent('input'));
          // Tell Angular to update the display binding through the title pipe
          fixture.detectChanges();
          expect(nameDisplay.textContent).toBe('Quick Brown  Fox');
        });
      });

      describe('when navigate with no hero id', () => {
        beforeEach(waitForAsync( createComponent ));
        it('should have hero.id === 0', () => {
          expect(component.hero.id).toBe(0);
        });
        it('should display empty hero name', () => {
          expect(page.nameDisplay.textContent).toBe('');
        });
      });

      describe('when navigate to non-existent hero id', () => {
        beforeEach(waitForAsync(() => {
          activatedRoute.setParamMap({ id: 99999 });
          createComponent();
        }));
        it('should try to navigate back to hero list', () => {
          expect(page.gotoListSpy.calls.any())
            .toBe(true, 'comp.gotoList called');
          expect(page.navigateSpy.calls.any())
            .toBe(true, 'router.navigate called');
        });
      });

      // Why we must use `fixture.debugElement.injector` in `Page()`
      it('cannot use `inject` to get component provided HeroDetailService', () => {
        let service: HeroDetailService;
        fixture = TestBed.createComponent(HeroDetailComponent);
        expect(
          // Throws because `inject` only has access to TestBed's injector
          // which is an ancestor of the component's injector
          inject([HeroDetailService], (hds: HeroDetailService) =>  service = hds )
        ).toThrowError(/No provider for HeroDetailService/);
        // get `HeroDetailService` with component's own injector
        service = fixture.debugElement.injector.get(HeroDetailService);
        expect(service).toBeDefined('debugElement.injector');
      });
    }
  
--- --- overrideSetup() - fake all necessary features of a nested services with stub spy

    function overrideSetup() {
      class HeroDetailServiceSpy {
        testHero: Hero = {id: 42, name: 'Test Hero' };
        // emit cloned test hero
        getHero = jasmine.createSpy('getHero').and.callFake(
          () => asyncData(Object.assign({}, this.testHero))
        );
        // emit clone of test hero, with changes merged in
        saveHero = jasmine.createSpy('saveHero').and.callFake(
          (hero: Hero) => asyncData(Object.assign(this.testHero, hero))
        );
      }
      // the "id" value is irrelevant because ignored by service stub
      beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));
      beforeEach(waitForAsync(() => {
        const routerSpy = createRouterSpy();
        TestBed.configureTestingModule({
          imports:   [ HeroModule ],
          providers: [
            { provide: ActivatedRoute, useValue: activatedRoute },
            { provide: Router,         useValue: routerSpy},
            // HeroDetailService at this level is IRRELEVANT !
            { provide: HeroDetailService, useValue: {} }
          ]
        })
        // Override component own provider
        .overrideComponent(HeroDetailComponent, {
          set: {
            providers: [
              { provide: HeroDetailService, useClass: HeroDetailServiceSpy }
            ]
          }
        }).compileComponents();
      }));
      let hdsSpy: HeroDetailServiceSpy;
      beforeEach(waitForAsync(() => {
        createComponent();
        // get the component's injected HeroDetailServiceSpy
        hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
      }));
      it('should have called `getHero`', () => {
        expect(hdsSpy.getHero.calls.count())
          .toBe(1, 'getHero called once');
      });
      it('should display stub hero name', () => {
        expect(page.nameDisplay.textContent)
          .toBe(hdsSpy.testHero.name);
      });
      it('should save stub hero change', fakeAsync(() => {
        const origName = hdsSpy.testHero.name;
        const newName = 'New Name';
        page.nameInput.value = newName;
        page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
        expect(component.hero.name)
          .toBe(newName, 'component hero has new name');
        expect(hdsSpy.testHero.name)
          .toBe(origName, 'service hero unchanged before save');
        click(page.saveBtn);
        expect(hdsSpy.saveHero.calls.count())
          .toBe(1, 'saveHero called once');
        tick(); // wait for async save to complete
        expect(hdsSpy.testHero.name)
          .toBe(newName, 'service hero has new name after save');
        expect(page.navigateSpy.calls.any())
          .toBe(true, 'router.navigate called');
      }));
      it('fixture injected service is not the component injected service',
        // inject gets the service from the fixture
        inject([HeroDetailService], (fixtureService: HeroDetailService) => {
        // use `fixture.debugElement.injector` to get service from component
        const componentService = fixture.debugElement.injector.get(HeroDetailService);
        expect(fixtureService)
          .not.toBe(componentService, 'service injected from fixture');
      }));
    }
  
--- --- formsModuleSetup() - configure testing module from individual pieces

    import { FormsModule }         from '@angular/forms';
    import { TitleCasePipe }       from '../shared/title-case.pipe';
    function formsModuleSetup() {
      beforeEach(waitForAsync(() => {
        const routerSpy = createRouterSpy();
        TestBed.configureTestingModule({
          imports:      [ FormsModule ],
          declarations: [ HeroDetailComponent, TitleCasePipe ],
          providers: [
            { provide: ActivatedRoute, useValue: activatedRoute },
            { provide: HeroService,    useClass: TestHeroService },
            { provide: Router,         useValue: routerSpy},
          ]
        }).compileComponents();
      }));
      it('should display 1st hero name', waitForAsync(() => {
        const expectedHero = firstHero;
        activatedRoute.setParamMap({ id: expectedHero.id });
        createComponent().then(() => {
          expect(page.nameDisplay.textContent).toBe(expectedHero.name);
        });
      }));
    }
  
--- --- sharedModuleSetup() - combine frequently requested parts

    import { SharedModule }        from '../shared/shared.module';
    function sharedModuleSetup() {
      beforeEach(waitForAsync(() => {
        const routerSpy = createRouterSpy();
        TestBed.configureTestingModule({
          imports:      [ SharedModule ],
          declarations: [ HeroDetailComponent ],
          providers: [
            { provide: ActivatedRoute, useValue: activatedRoute },
            { provide: HeroService,    useClass: TestHeroService },
            { provide: Router,         useValue: routerSpy},
          ]
        })
        .compileComponents();
      }));
      it('should display 1st hero name', waitForAsync(() => {
        const expectedHero = firstHero;
        activatedRoute.setParamMap({ id: expectedHero.id });
        createComponent().then(() => {
          expect(page.nameDisplay.textContent).toBe(expectedHero.name);
        });
      }));
    }

    // --- shared.module.ts
    import { NgModule }      from '@angular/core';
    import { CommonModule }  from '@angular/common';
    import { FormsModule }   from '@angular/forms';
    import { HighlightDirective } from './highlight.directive';
    import { TitleCasePipe }      from './title-case.pipe';
    @NgModule({
      imports: [ CommonModule ],
      exports: [
        CommonModule,
        // SharedModule importers won't have to import FormsModule too
        FormsModule,
        HighlightDirective,
        TitleCasePipe
      ],
      declarations: [ HighlightDirective, TitleCasePipe ]
    })
    export class SharedModule { }
  
--- hero-detail.component.no-testbed.spec.ts

    import { asyncData, ActivatedRouteStub } from '../../testing';
    import { HeroDetailComponent } from './hero-detail.component';
    import { Hero }                from '../model/hero';

    describe('HeroDetailComponent - no TestBed', () => {
      let activatedRoute: ActivatedRouteStub;
      let comp: HeroDetailComponent;
      let expectedHero: Hero;
      let hds: any;
      let router: any;
      beforeEach((done: DoneFn) => {
        expectedHero = {id: 42, name: 'Bubba' };
        const activatedRoute = new ActivatedRouteStub({ id: expectedHero.id });
        router = jasmine.createSpyObj('router', ['navigate']);
        hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']);
        hds.getHero.and.returnValue(asyncData(expectedHero));
        hds.saveHero.and.returnValue(asyncData(expectedHero));
        comp = new HeroDetailComponent(hds, <any> activatedRoute, router);
        comp.ngOnInit();
        // OnInit calls HDS.getHero; wait for it to get the fake hero
        hds.getHero.calls.first().returnValue.subscribe(done);
      });
      it('should expose the hero retrieved from the service', () => {
        expect(comp.hero).toBe(expectedHero);
      });
      it('should navigate when click cancel', () => {
        comp.cancel();
        expect(router.navigate.calls.any())
          .toBe(true, 'router.navigate called');
      });
      it('should save when click save', () => {
        comp.save();
        expect(hds.saveHero.calls.any())
          .toBe(true, 'HeroDetailService.save called');
        expect(router.navigate.calls.any())
          .toBe(false, 'router.navigate not called yet');
      });
      it('should navigate when click save resolves',
      (done: DoneFn) => {
        comp.save();
        // waits for async save to complete before navigating
        hds.saveHero.calls.first().returnValue
        .subscribe(() => {
          expect(router.navigate.calls.any())
            .toBe(true, 'router.navigate called');
          done();
        });
      });
    });
  
--- hero-list.component.spec.ts

    import {
      waitForAsync, ComponentFixture, fakeAsync, TestBed, tick
    } from '@angular/core/testing';
    import { By }           from '@angular/platform-browser';
    import { DebugElement } from '@angular/core';
    import { Router }       from '@angular/router';
    import { addMatchers, newEvent } from '../../testing';
    import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
    import { HeroModule }         from './hero.module';
    import { HeroListComponent }  from './hero-list.component';
    import { HighlightDirective } from '../shared/highlight.directive';
    import { HeroService }        from '../model/hero.service';
    const HEROES = getTestHeroes();
    let comp: HeroListComponent;
    let fixture: ComponentFixture<HeroListComponent>;
    let page: Page;

    describe('HeroListComponent', () => {
      beforeEach(waitForAsync(() => {
        addMatchers();
        const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
        TestBed.configureTestingModule({
          imports: [HeroModule],
          providers: [
            { provide: HeroService, useClass: TestHeroService },
            { provide: Router,      useValue: routerSpy}
          ]
        })
        .compileComponents()
        .then(createComponent);
      }));
      it('should display heroes', () => {
        expect(page.heroRows.length).toBeGreaterThan(0);
      });
      it('1st hero should match 1st test hero', () => {
        const expectedHero = HEROES[0];
        const actualHero = page.heroRows[0].textContent;
        expect(actualHero).toContain(expectedHero.id.toString(), 'hero.id');
        expect(actualHero).toContain(expectedHero.name, 'hero.name');
      });
      it('should select hero on click', fakeAsync(() => {
        const expectedHero = HEROES[1];
        const li = page.heroRows[1];
        li.dispatchEvent(newEvent('click'));
        tick();
        // `.toEqual` because selectedHero is clone of expectedHero; see FakeHeroService
        expect(comp.selectedHero).toEqual(expectedHero);
      }));
      it('should navigate to selected hero detail on click', fakeAsync(() => {
        const expectedHero = HEROES[1];
        const li = page.heroRows[1];
        li.dispatchEvent(newEvent('click'));
        tick();
        // should have navigated
        expect(page.navSpy.calls.any()).toBe(true, 'navigate called');
        // composed hero detail will be URL like 'heroes/42'
        // expect link array with the route path and hero id
        // first argument to router.navigate is link array
        const navArgs = page.navSpy.calls.first().args[0];
        expect(navArgs[0]).toContain('heroes', 'nav to heroes detail URL');
        expect(navArgs[1]).toBe(expectedHero.id, 'expected hero.id');
      }));
      it('should find `HighlightDirective` with `By.directive', () => {
        // Can find DebugElement either by css selector or by directive
        const h2        = fixture.debugElement.query(By.css('h2'));
        const directive = fixture.debugElement.query(By.directive(HighlightDirective));
        expect(h2).toBe(directive);
      });
      it('should color header with `HighlightDirective`', () => {
        const h2 = page.highlightDe.nativeElement as HTMLElement;
        const bgColor = h2.style.backgroundColor;
        // different browsers report color values differently
        const isExpectedColor = bgColor === 'gold' || bgColor === 'rgb(255, 215, 0)';
        expect(isExpectedColor).toBe(true, 'backgroundColor');
      });
      it('the `HighlightDirective` is among the element providers', () => {
        expect(page.highlightDe.providerTokens).toContain(HighlightDirective, 'HighlightDirective');
      });
    });

    // --- helpers
    // Create the component and set the `page` test variables
    function createComponent() {
      fixture = TestBed.createComponent(HeroListComponent);
      comp = fixture.componentInstance;
      // change detection triggers ngOnInit which gets a hero
      fixture.detectChanges();
      return fixture.whenStable().then(() => {
        // got the heroes and updated component
        // change detection updates the view
        fixture.detectChanges();
        page = new Page();
      });
    }
    class Page {
      heroRows: HTMLLIElement[];
      highlightDe: DebugElement;
      navSpy: jasmine.Spy;
      constructor() {
        const heroRowNodes = fixture.nativeElement.querySelectorAll('li');
        this.heroRows = Array.from(heroRowNodes);
        // Find the first element with an attached HighlightDirective
        this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective));
        // Get the component injected router navigation spy
        const routerSpy = fixture.debugElement.injector.get(Router);
        this.navSpy = routerSpy.navigate as jasmine.Spy;
      };
    }
  
router-outlet, RouterLink in nested or AppComponent (using stubs)

    // --- --- COMPONENT
    import { Component } from '@angular/core';
    @Component({
      selector: 'app-root',
      template: `
        <app-banner></app-banner>
        <app-welcome></app-welcome>
        <nav>
          <a routerLink="/dashboard">Dashboard</a>
          <a routerLink="/heroes">Heroes</a>
          <a routerLink="/about">About</a>
        </nav>
        <router-outlet></router-outlet>`
    })
    export class AppComponent { }
    // --- --- TEST
  
--- app.component.spec.ts

    import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
    import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
    import { By } from '@angular/platform-browser';
    import { AppComponent } from './app.component';
    import { RouterLinkDirectiveStub } from '../testing';
    // stubs
    @Component({selector: 'app-banner', template: ''})
    class BannerStubComponent {}
    @Component({selector: 'router-outlet', template: ''})
    class RouterOutletStubComponent { }
    @Component({selector: 'app-welcome', template: ''})
    class WelcomeStubComponent {}
    let comp:    AppComponent;
    let fixture: ComponentFixture<AppComponent>;
    // with STUBS
    describe('AppComponent & TestModule', () => {
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          declarations: [
            AppComponent,
            RouterLinkDirectiveStub,
            BannerStubComponent,
            RouterOutletStubComponent,
            WelcomeStubComponent
          ]
        })
        .compileComponents().then(() => {
          fixture = TestBed.createComponent(AppComponent);
          comp    = fixture.componentInstance;
        });
      }));
      tests();
    });
    // with NO_ERRORS_SCHEMA
    describe('AppComponent & NO_ERRORS_SCHEMA', () => {
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          declarations: [
            AppComponent,
            BannerStubComponent,
            RouterLinkDirectiveStub
          ],
          // tell Angular compiler to ignore unrecognized elements and attributes
          // prevents the compiler from telling you about
          // the missing components and attributes that you omitted inadvertently or misspelled
          schemas: [ NO_ERRORS_SCHEMA ]
        })
        .compileComponents().then(() => {
          fixture = TestBed.createComponent(AppComponent);
          comp    = fixture.componentInstance;
        });
      }));
      tests();
    });
    // with real root module
    // Tricky because we are disabling the router and its configuration
    // Better to use RouterTestingModule
    describe('AppComponent & AppModule', () => {
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          imports: [ AppModule ]
        })
        // Get rid of app Router configuration otherwise many failures.
        // Doing so removes Router declarations; add the Router stubs
        .overrideModule(AppModule, {
          remove: { imports: [ AppRoutingModule ] },
          add: {
            declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ]
          }
        }).compileComponents()
        .then(() => {
          fixture = TestBed.createComponent(AppComponent);
          comp    = fixture.componentInstance;
        });
      }));
      tests();
    });
    function tests() {
      let routerLinks: RouterLinkDirectiveStub[];
      let linkDes: DebugElement[];
      beforeEach(() => {
        fixture.detectChanges(); // trigger initial data binding
        // find DebugElements with an attached RouterLinkStubDirective
        linkDes = fixture.debugElement
          .queryAll(By.directive(RouterLinkDirectiveStub));
        // get attached link directive instances
        // using each DebugElement injector
        routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
      });
      it('can instantiate the component', () => {
        expect(comp).not.toBeNull();
      });
      it('can get RouterLinks from template', () => {
        expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
        expect(routerLinks[0].linkParams).toBe('/dashboard');
        expect(routerLinks[1].linkParams).toBe('/heroes');
        expect(routerLinks[2].linkParams).toBe('/about');
      });
      it('can click Heroes link in template', () => {
        const heroesLinkDe = linkDes[1];   // heroes link DebugElement
        const heroesLink = routerLinks[1]; // heroes link directive
        expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');
        heroesLinkDe.triggerEventHandler('click', null);
        fixture.detectChanges();
        expect(heroesLink.navigatedTo).toBe('/heroes');
      });
    }
    // --- helpers
    import { Directive, Input, HostListener } from '@angular/core';
    // export for convenience
    export { RouterLink } from '@angular/router';
    // tslint:disable:directive-class-suffix
    // emulate RouterLink
    @Directive({ selector: '[routerLink]' })
    export class RouterLinkDirectiveStub {
      @Input('routerLink') linkParams: any;
      navigatedTo: any = null;
      @HostListener('click')
      onClick() { this.navigatedTo = this.linkParams; }
    }
    /// Dummy module to satisfy Angular Language service. Never used.
    import { NgModule } from '@angular/core';
    @NgModule({ declarations: [ RouterLinkDirectiveStub ] })
    export class RouterStubsModule {}
  
--- app.component.router.spec.ts

    import { HeroService, TestHeroService } from './model/testing/test-hero.service';
    let comp:     AppComponent;
    let fixture:  ComponentFixture<AppComponent>;
    let page:     Page;
    let router:   Router;
    let location: SpyLocation;
    describe('AppComponent & RouterTestingModule', () => {
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          imports: [ AppModule, RouterTestingModule ],
          providers: [
            { provide: HeroService, useClass: TestHeroService }
          ]
        })
        .compileComponents();
      }));
      it('should navigate to "Dashboard" immediately', fakeAsync(() => {
        createComponent();
        tick(); // wait for async data to arrive
        expect(location.path()).toEqual('/dashboard', 'after initialNavigation()');
        expectElementOf(DashboardComponent);
      }));
      it('should navigate to "About" on click', fakeAsync(() => {
        createComponent();
        click(page.aboutLinkDe);
        // page.aboutLinkDe.nativeElement.click(); // ok but fails in phantom
        advance();
        expectPathToBe('/about');
        expectElementOf(AboutComponent);
      }));
      it('should navigate to "About" w/ browser location URL change', fakeAsync(() => {
        createComponent();
        location.simulateHashChange('/about');
        // location.go('/about'); // also works ... except, perhaps, in Stackblitz
        advance();
        expectPathToBe('/about');
        expectElementOf(AboutComponent);
      }));
      // Can't navigate to lazy loaded modules with this technique
      xit('should navigate to "Heroes" on click (not working yet)', fakeAsync(() => {
        createComponent();
        page.heroesLinkDe.nativeElement.click();
        advance();
        expectPathToBe('/heroes');
      }));
    });

    // should be lazy loaded
    import { HeroModule }             from './hero/hero.module';
    import { HeroListComponent }      from './hero/hero-list.component';
    // cant get lazy loaded Heroes to work yet
    xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
      beforeEach(waitForAsync(() => {
        TestBed.configureTestingModule({
          imports: [ AppModule, RouterTestingModule ]
        })
        .compileComponents();
      }));
      beforeEach(fakeAsync(() => {
        createComponent();
        router.resetConfig([{path: 'heroes', loadChildren: () => HeroModule}]);
      }));
      it('should navigate to "Heroes" on click', waitForAsync(() => {
        page.heroesLinkDe.nativeElement.click();
        advance();
        expectPathToBe('/heroes');
        expectElementOf(HeroListComponent);
      }));
      it('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
        location.go('/heroes');
        advance();
        expectPathToBe('/heroes');
        expectElementOf(HeroListComponent);
      }));
    });

    // --- helpers
    // advance to the routed page
    // wait a tick, then detect changes, and tick again
    function advance(): void {
      tick(); // wait while navigating
      fixture.detectChanges(); // update view
      tick(); // wait for async data to arrive
    }
    function createComponent() {
      fixture = TestBed.createComponent(AppComponent);
      comp = fixture.componentInstance;
      const injector = fixture.debugElement.injector;
      location = injector.get(Location) as SpyLocation;
      router = injector.get(Router);
      router.initialNavigation();
      spyOn(injector.get(TwainService), 'getQuote')
        // fake fast async observable
        .and.returnValue(asyncData('Test Quote'));
      advance();
      page = new Page();
    }
    class Page {
      aboutLinkDe:     DebugElement;
      dashboardLinkDe: DebugElement;
      heroesLinkDe:    DebugElement;
      // for debugging
      comp: AppComponent;
      location: SpyLocation;
      router: Router;
      fixture: ComponentFixture<AppComponent>;
      constructor() {
        const links = fixture.debugElement.queryAll(By.directive(RouterLink));
        this.aboutLinkDe     = links[2];
        this.dashboardLinkDe = links[0];
        this.heroesLinkDe    = links[1];
        // for debugging
        this.comp    = comp;
        this.fixture = fixture;
        this.router  = router;
      }
    }
    function expectPathToBe(path: string, expectationFailOutput?: any) {
      expect(location.path()).toEqual(path, expectationFailOutput || 'location.path()');
    }
    function expectElementOf(type: Type<any>): any {
      const el = fixture.debugElement.query(By.directive(type));
      expect(el).toBeTruthy('expected an element for ' + type.name);
      return el;
    }
    import { defer } from 'rxjs';
    // async observable that emits-once and completes after a JS engine turn
    export function asyncData<T>(data: T) {
      return defer(() => Promise.resolve(data));
    }
    // async observable error that errors after a JS engine turn
    export function asyncError<T>(errorObject: any) {
      return defer(() => Promise.reject(errorObject));
    }
    // button events to pass to `DebugElement.triggerEventHandler`
    // for RouterLink event handler
    export const ButtonClickEvents = {
      left:  { button: 0 },
      right: { button: 2 }
    };
    // simulate element click, defaults to mouse left-button click event
    export function click(
      el: DebugElement | HTMLElement,
      eventObj: any = ButtonClickEvents.left
    ): void {
      if (el instanceof HTMLElement) {
        el.click();
      } else {
        el.triggerEventHandler('click', eventObj);
      }
    }
  
Service testing

    describe('testing', () => {
      let service: ValueService;

      // --- inject service inside test
      it('should use ValueService', () => {
        service = TestBed.inject(ValueService);
        expect(service.getValue()).toBe('real value');
      });

      // --- OR inject the service as part of your setup
      // set the preconditions for each it() test
      // and rely on the TestBed to create classes and inject services
      beforeEach(() => {
        TestBed.configureTestingModule({ providers: [ValueService] });
        service = TestBed.inject(ValueService);
      });

      // --- OR, while testing a service with a dependency
      // provide the mock in the providers array, here mock is a spy object
      let masterService: MasterService;
      let valueServiceSpy: jasmine.SpyObj<ValueService>;
      beforeEach(() => {
        const spy = jasmine.createSpyObj('ValueService', ['getValue']);
        TestBed.configureTestingModule({
          providers: [
            MasterService, // service to test
            { provide: ValueService, useValue: spy } // its (spy) dependency
          ]
        });
        masterService = TestBed.inject(MasterService); // injext service and
        valueServiceSpy = TestBed.inject(ValueService); // its (spy) dependency
      });
      // test consumes that spy in the same way it did earlier
      it('#getValue should return stubbed value from a spy', () => {
        const stubValue = 'stub value';
        valueServiceSpy.getValue.and.returnValue(stubValue);
        expect(masterService.getValue())
          .toBe(stubValue, 'service returned stub value');
        expect(valueServiceSpy.getValue.calls.count())
          .toBe(1, 'spy method was called once');
        expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
          .toBe(stubValue);
      });

      // --- create classes explicitly, MasterService (no beforeEach)
      it('#getValue should return stubbed value from a spy', () => {
        const { masterService, stubValue, valueServiceSpy } = setup();
        expect(masterService.getValue())
          .toBe(stubValue, 'service returned stub value');
        expect(valueServiceSpy.getValue.calls.count())
          .toBe(1, 'spy method was called once');
        expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
          .toBe(stubValue);
      });
      function setup() {
        const valueServiceSpy =
          jasmine.createSpyObj('ValueService', ['getValue']);
        const stubValue = 'stub value';
        const masterService = new MasterService(valueServiceSpy);
        valueServiceSpy.getValue.and.returnValue(stubValue);
        return { masterService, stubValue, valueServiceSpy };
      }

      // --- http services
      let httpClientSpy: { get: jasmine.Spy };
      let heroService: HeroService;
      beforeEach(() => {
        // TODO: spy on other methods too
        httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
        heroService = new HeroService(<any> httpClientSpy);
      });
      it('should return expected heroes (HttpClient called once)', () => {
        const expectedHeroes: Hero[] =
          [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
        httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
        heroService.getHeroes().subscribe(
          heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
          fail
        );
        expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
      });
      it('should return an error when the server returns a 404', () => {
        const errorResponse = new HttpErrorResponse({
          error: 'test 404 error',
          status: 404, statusText: 'Not Found'
        });
        httpClientSpy.get.and.returnValue(asyncError(errorResponse));
        heroService.getHeroes().subscribe(
          heroes => fail('expected an error, not heroes'),
          error  => expect(error.message).toContain('test 404 error')
        );
      });
    });
  
Directive testing

    // --- --- DIRECTIVE
    import { Directive, ElementRef, Input, OnChanges } from '@angular/core';
    @Directive({ selector: '[highlight]' })
    // set backgroundColor for the attached element to highlight color
    // and set the element customProperty to true
    export class HighlightDirective implements OnChanges {
      defaultColor =  'rgb(211, 211, 211)'; // lightgray
      @Input('highlight') bgColor: string;
      constructor(private el: ElementRef) {
        el.nativeElement.style.customProperty = true;
      }
      ngOnChanges() {
        this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
      }
    }

    // --- --- TEST
    import { Component, DebugElement }   from '@angular/core';
    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { By } from '@angular/platform-browser';
    import { HighlightDirective } from './highlight.directive';
    import { newEvent }           from '../../testing';
    // artificial test component that demonstrates all ways to apply the directive
    @Component({
      template: `
      <h2 highlight="yellow">Something Yellow</h2>
      <h2 highlight>The Default (Gray)</h2>
      <h2>No Highlight</h2>
      <input #box [highlight]="box.value" value="cyan"/>`
    })
    class TestComponent { }
    describe('HighlightDirective', () => {
      let fixture: ComponentFixture<TestComponent>;
      let des: DebugElement[];  // the three elements w/ the directive
      let bareH2: DebugElement; // the <h2> w/o the directive
      beforeEach(() => {
        fixture = TestBed.configureTestingModule({
          declarations: [ HighlightDirective, TestComponent ]
        })
        .createComponent(TestComponent);
        fixture.detectChanges(); // initial binding
        // all elements with an attached HighlightDirective
        des = fixture.debugElement.queryAll(By.directive(HighlightDirective));
        // the h2 without the HighlightDirective
        bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
      });
      // color tests
      it('should have three highlighted elements', () => {
        expect(des.length).toBe(3);
      });
      it('should color 1st <h2> background "yellow"', () => {
        const bgColor = des[0].nativeElement.style.backgroundColor;
        expect(bgColor).toBe('yellow');
      });
      it('should color 2nd <h2> background w/ default color', () => {
        const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
        const bgColor = des[1].nativeElement.style.backgroundColor;
        expect(bgColor).toBe(dir.defaultColor);
      });
      it('should bind <input> background to value color', () => {
        // easier to work with nativeElement
        const input = des[2].nativeElement as HTMLInputElement;
        expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');
        // dispatch a DOM event so that Angular responds to the input value change.
        input.value = 'green';
        input.dispatchEvent(newEvent('input'));
        fixture.detectChanges();
        expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
      });
      it('bare <h2> should not have a customProperty', () => {
        expect(bareH2.properties['customProperty']).toBeUndefined();
      });
      // injected directive
      // attached HighlightDirective can be injected
      it('can inject `HighlightDirective` in 1st <h2>', () => {
        const dir = des[0].injector.get(HighlightDirective);
        expect(dir).toBeTruthy();
      });
      it('cannot inject `HighlightDirective` in 3rd <h2>', () => {
        const dir = bareH2.injector.get(HighlightDirective, null);
        expect(dir).toBe(null);
      });
      // DebugElement.providerTokens
      // attached HighlightDirective should be listed in the providerTokens
      it('should have `HighlightDirective` in 1st <h2> providerTokens', () => {
        expect(des[0].providerTokens).toContain(HighlightDirective);
      });
      it('should not have `HighlightDirective` in 3rd <h2> providerTokens', () => {
        expect(bareH2.providerTokens).not.toContain(HighlightDirective);
      });
    });

    // --- --- TEST COMPONENT
    import { Component } from '@angular/core';
    @Component({
      template: `
      <h2 highlight="skyblue">About</h2>
      <h3>Quote of the day:</h3>
      <twain-quote></twain-quote>`
    })
    export class AboutComponent { }

    // --- --- TEST
    import { NO_ERRORS_SCHEMA }          from '@angular/core';
    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { AboutComponent }     from './about.component';
    import { HighlightDirective } from '../shared/highlight.directive';
    let fixture: ComponentFixture<AboutComponent>;
    describe('AboutComponent (highlightDirective)', () => {
      beforeEach(() => {
        fixture = TestBed.configureTestingModule({
          declarations: [ AboutComponent, HighlightDirective],
          schemas:      [ NO_ERRORS_SCHEMA ]
        })
        .createComponent(AboutComponent);
        fixture.detectChanges(); // initial binding
      });
      it('should have skyblue <h2>', () => {
        const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
        const bgColor = h2.style.backgroundColor;
        expect(bgColor).toBe('skyblue');
      });
    });
  
Pipe testing

    // --- --- PIPE
    import { Pipe, PipeTransform } from '@angular/core';
    @Pipe({name: 'titlecase', pure: true})
    // uppercase the first letter of the words in a string
    export class TitleCasePipe implements PipeTransform {
      transform(input: string): string {
        return input.length === 0 ? '' :
          input.replace(/\w\S*/g, (txt => txt[0].toUpperCase() + txt.substr(1).toLowerCase() ));
      }
    }

    // --- --- TEST
    import { TitleCasePipe } from './title-case.pipe';
    describe('TitleCasePipe', () => {
      // This pipe is a pure, stateless function so no need for BeforeEach
      let pipe = new TitleCasePipe();
      it('transforms "abc" to "Abc"', () => {
        expect(pipe.transform('abc')).toBe('Abc');
      });
      it('transforms "abc def" to "Abc Def"', () => {
        expect(pipe.transform('abc def')).toBe('Abc Def');
      });
      // ... more tests ...
      it('leaves "Abc Def" unchanged', () => {
        expect(pipe.transform('Abc Def')).toBe('Abc Def');
      });
      it('transforms "abc-def" to "Abc-def"', () => {
        expect(pipe.transform('abc-def')).toBe('Abc-def');
      });
      it('transforms "   abc   def" to "   Abc   Def" (preserves spaces) ', () => {
        expect(pipe.transform('   abc   def')).toBe('   Abc   Def');
      });
    });

    // --- --- COMPONENT TEST
    it('should convert hero name to Title Case', () => {
      // get the name's input and display elements from the DOM
      const hostElement = fixture.nativeElement;
      const nameInput: HTMLInputElement = hostElement.querySelector('input');
      const nameDisplay: HTMLElement = hostElement.querySelector('span');
      // simulate user entering a new name into the input box
      nameInput.value = 'quick BROWN  fOx';
      // dispatch a DOM event so that Angular learns of input value change.
      nameInput.dispatchEvent(newEvent('input'));
      // Tell Angular to update the display binding through the title pipe
      fixture.detectChanges();
      expect(nameDisplay.textContent).toBe('Quick Brown  Fox');
    });
  
HttpClientTestingModule

    // --- Http testing module and mocking controller
    import {
      HttpClientTestingModule, HttpTestingController
    } from '@angular/common/http/testing';
    // Other imports
    import { TestBed } from '@angular/core/testing';
    import { HttpClient, HttpErrorResponse } from '@angular/common/http';
    import { HttpHeaders } from '@angular/common/http';
    interface Data {
      name: string;
    }
    const testUrl = '/data';
    describe('HttpClient testing', () => {
      let httpClient: HttpClient;
      let httpTestingController: HttpTestingController;
      beforeEach(() => {
        TestBed.configureTestingModule({ imports: [ HttpClientTestingModule ] });
        // Inject the http service and test controller for each test
        httpClient = TestBed.inject(HttpClient);
        httpTestingController = TestBed.inject(HttpTestingController);
      });
      afterEach(() => {
        // After every test, assert that there are no more pending requests
        httpTestingController.verify();
      });
      /// Tests begin ///
      it('can test HttpClient.get', () => {
        const testData: Data = {name: 'Test Data'};
        // Make an HTTP GET request
        httpClient.get<Data>(testUrl)
          .subscribe(data =>
            // When observable resolves, result should match test data
            expect(data).toEqual(testData)
          );
        // The following `expectOne()` will match the request's URL.
        // If no requests or multiple requests matched that URL
        // `expectOne()` would throw.
        const req = httpTestingController.expectOne('/data');
        // Assert that the request is a GET.
        expect(req.request.method).toEqual('GET');
        // Respond with mock data, causing Observable to resolve.
        // Subscribe callback asserts that correct data was returned.
        req.flush(testData);
        // Finally, assert that there are no outstanding requests.
        httpTestingController.verify();
      });
      it('can test HttpClient.get with matching header', () => {
        const testData: Data = {name: 'Test Data'};
        // Make an HTTP GET request with specific header
        httpClient.get<Data>(testUrl, {
            headers: new HttpHeaders({'Authorization': 'my-auth-token'})
          })
          .subscribe(data =>
            expect(data).toEqual(testData)
          );
          // Find request with a predicate function.
        // Expect one request with an authorization header
        const req = httpTestingController.expectOne(
          req => req.headers.has('Authorization')
        );
        req.flush(testData);
      });
      it('can test multiple requests', () => {
        let testData: Data[] = [
          { name: 'bob' }, { name: 'carol' },
          { name: 'ted' }, { name: 'alice' }
        ];
        // Make three requests in a row
        httpClient.get<Data[]>(testUrl)
          .subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
        httpClient.get<Data[]>(testUrl)
          .subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
        httpClient.get<Data[]>(testUrl)
          .subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
        // get all pending requests that match the given URL
        const requests = httpTestingController.match(testUrl);
        expect(requests.length).toEqual(3);
        // Respond to each request with different results
        requests[0].flush([]);
        requests[1].flush([testData[0]]);
        requests[2].flush(testData);
      });
      it('can test for 404 error', () => {
        const emsg = 'deliberate 404 error';
        httpClient.get<Data[]>(testUrl).subscribe(
          data => fail('should have failed with the 404 error'),
          (error: HttpErrorResponse) => {
            expect(error.status).toEqual(404, 'status');
            expect(error.error).toEqual(emsg, 'message');
          }
        );
        const req = httpTestingController.expectOne(testUrl);
        // Respond with mock error
        req.flush(emsg, { status: 404, statusText: 'Not Found' });
      });
      it('can test for network error', () => {
        const emsg = 'simulated network error';
        httpClient.get<Data[]>(testUrl).subscribe(
          data => fail('should have failed with the network error'),
          (error: HttpErrorResponse) => {
            expect(error.error.message).toEqual(emsg, 'message');
          }
        );
        const req = httpTestingController.expectOne(testUrl);
        // Create mock ErrorEvent, raised when something goes wrong at the network level.
        // Connection timeout, DNS error, offline, etc
        const mockError = new ErrorEvent('Network error', {
          message: emsg,
          // The rest of this is optional and not used.
          // Just showing that you could provide this too.
          filename: 'HeroService.ts',
          lineno: 42,
          colno: 21
        });
        // Respond with mock error
        req.error(mockError);
      });
      it('httpTestingController.verify should fail if HTTP response not simulated', () => {
        // Sends request
        httpClient.get('some/api').subscribe();
        // verify() should fail because haven't handled the pending request.
        expect(() => httpTestingController.verify()).toThrow();
        // Now get and flush the request so that afterEach() doesn't fail
        const req = httpTestingController.expectOne('some/api');
        req.flush(null);
      });
      // Proves that verify in afterEach() really would catch error
      // if test doesnt simulate the HTTP response
      //
      // Must disable this test because can't catch an error in an afterEach()
      // Uncomment if you want to confirm that afterEach() does the job
      // it('afterEach() should fail when HTTP response not simulated',() => {
      //   // Sends request which is never handled by this test
      //   httpClient.get('some/api').subscribe();
      // });
    });
  
straight service Jasmine testing without Angular testing support

    describe('ValueService', () => {
      let service: ValueService;
      beforeEach(() => { service = new ValueService(); });
      it('#getValue should return real value', () => {
        expect(service.getValue()).toBe('real value');
      });
      it('#getObservableValue should return value from observable',
        (done: DoneFn) => {
        service.getObservableValue().subscribe(value => {
          expect(value).toBe('observable value');
          done();
        });
      });
      it('#getPromiseValue should return value from a promise',
        (done: DoneFn) => {
        service.getPromiseValue().then(value => {
          expect(value).toBe('promise value');
          done();
        });
      });
    });

    // --- testing a dependent service
    describe('MasterService without Angular testing support', () => {
      let masterService: MasterService;
      it('#getValue should return real value from the real service', () => {
        masterService = new MasterService(new ValueService());
        expect(masterService.getValue()).toBe('real value');
      });
      it('#getValue should return faked value from a fakeService', () => {
        masterService = new MasterService(new FakeValueService());
        expect(masterService.getValue()).toBe('faked service value');
      });
      it('#getValue should return faked value from a fake object', () => {
        const fake =  { getValue: () => 'fake value' };
        masterService = new MasterService(fake as ValueService);
        expect(masterService.getValue()).toBe('fake value');
      });
      it('#getValue should return stubbed value from a spy', () => {
        // create `getValue` spy on an object representing the ValueService
        const valueServiceSpy =
          jasmine.createSpyObj('ValueService', ['getValue']);
        // set the value to return when the `getValue` spy is called.
        const stubValue = 'stub value';
        valueServiceSpy.getValue.and.returnValue(stubValue);
        masterService = new MasterService(valueServiceSpy);
        expect(masterService.getValue())
          .toBe(stubValue, 'service returned stub value');
        expect(valueServiceSpy.getValue.calls.count())
          .toBe(1, 'spy method was called once');
        expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
          .toBe(stubValue);
      });
    });
  
configure CLI for CI testing in Chrome (Headless Chrome)

    // karma.conf.js
    browsers: ['Chrome'],
    customLaunchers: {
      ChromeHeadlessCI: {
        base: 'ChromeHeadless',
        flags: ['--no-sandbox']
      }
    },

    // protractor-ci.conf.js inside e2e tests project
    // extends the original protractor.conf.js
    const config = require('./protractor.conf').config;
    config.capabilities = {
      browserName: 'chrome',
      chromeOptions: {
        args: ['--headless', '--no-sandbox']
      }
    };
    exports.config = config;

    // run the following commands to use the --no-sandbox flag
    ng test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
    ng e2e -- --protractor-config=e2e/protractor-ci.conf.js
  
configure project for Circle CI

    # 1 - create a folder called .circleci at the project root
    # 2 - create a file called config.yml
    version: 2
    jobs:
      build:
        working_directory: ~/my-project
        docker:
          - image: circleci/node:8-browsers
        steps:
          - checkout
          - restore_cache:
              key: my-project-{{ .Branch }}-{{ checksum "package-lock.json" }}
          - run: npm install
          - save_cache:
              key: my-project-{{ .Branch }}-{{ checksum "package-lock.json" }}
              paths:
                - "node_modules"
          - run: npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
          - run: npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js
    # 3 - commit your changes and push them to your repository
    # 4 - sign up for Circle CI and add your project
  
configure project for Travis CI

    # 1 - create a file called .travis.yml at the project root
    dist: trusty
    sudo: false

    language: node_js
    node_js:
      - "8"

    addons:
      apt:
        sources:
          - google-chrome
        packages:
          - google-chrome-stable

    cache:
      directories:
         - ./node_modules

    install:
      - npm install

    script:
      - npm run test -- --no-watch --no-progress --browsers=ChromeHeadlessCI
      - npm run e2e -- --protractor-config=e2e/protractor-ci.conf.js
    # 2 - commit your changes and push them to your repository
    # 3 -  sign up for Travis CI and add your project, push a commit to trigger build
  

Deployment

Base href


    import {Component, NgModule} from '@angular/core';
    import {APP_BASE_HREF} from '@angular/common';
    @NgModule({
      providers: [{provide: APP_BASE_HREF, useValue: '/my/app'}]
    })
    class AppModule {}
  

fallback server configuration - routed apps must fallback to index.htm


    # --- Apache: .htaccess rewrite rule
    RewriteEngine On
    # If an existing asset or directory is requested go to it as it is
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
    RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
    RewriteRule ^ - [L]
    # If the requested resource doesn't exist, use index.html
    RewriteRule ^ /index.html

    # --- Nginx: use try_files, modified to serve index.html
    try_files $uri $uri/ /index.html;
  

enable CORS


    // ExpressJS
    app.use(function(req, res, next) {
      res.header("Access-Control-Allow-Origin", "*");
      res.header(
        "Access-Control-Allow-Headers",
        "Origin,
        X-Requested-With,
        Content-Type,
        Accept"
      );
      next();
    });
    app.get('/', function(req, res, next) {
      // Handle the get for this route
    });
    app.post('/', function(req, res, next) {
      // Handle the post for this route
    });
  

    // PHP
    <?php
    header("Access-Control-Allow-Origin: *");
  

    # NGINX
    # Wide-open CORS config for nginx
    location / {
      if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        # Custom headers and headers various browsers *should* be OK with but arent
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        # Tell client that this pre-flight info is valid for 20 days
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
      }
      if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
      }
      if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
      }
    }
  

gzip static assets in a NodeJS + expressJS app


    // check first "Response Headers" , "Content-Encoding: gzip"
    const compression = require('compression')
    const express = require('express')
    const app = express()
    app.use(compression())
  

RxJS

Observable

    // Observable - representation of any set of values over any amount of time
    class Observable<T> implements Subscribable {
      static create: (...args: any[]) => any
      constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic)
      source: Observable<any> | undefined
      operator: Operator<any, T> | undefined
      lift<R>(operator?: Operator<T, R>): Observable<R>
      subscribe(observerOrNext?: Partial<Observer<T>> | ((value: T) => void), error?: (error: any) => void, complete?: () => void): Subscription
      forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise<void>
      pipe(...operations: OperatorFunction<any, any>[]): Observable<any>
      toPromise(promiseCtor?: PromiseConstructorLike): Promise<T | undefined>
    }

    // subscribe with an Observer
    const sumObserver = {
      sum: 0,
      next(value) {
        console.log('Adding: ' + value);
        this.sum = this.sum + value;
      },
      error() { // We could just remove this method,
      },        // since we do not really care about errors right now
      complete() { console.log('Sum equals: ' + this.sum); }
    };
    Rx.Observable.of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes
    .subscribe(sumObserver);
    // Logs: "Adding: 1" "Adding: 2" "Adding: 3" "Sum equals: 6"

    // subscribe with functions
    let sum = 0;
    Rx.Observable.of(1, 2, 3)
    .subscribe(
      function(value) {
        console.log('Adding: ' + value);
        sum = sum + value;
      },
      undefined,
      function() { console.log('Sum equals: ' + sum); }
    );
    // Logs: "Adding: 1" "Adding: 2" "Adding: 3" "Sum equals: 6"

    // cancel a subscription
    const subscription = Rx.Observable.interval(1000).subscribe(
      num => console.log(num),
      undefined,
      () => console.log('completed!') // Will not be called, even
    );                                // when cancelling subscription
    setTimeout(() => {
      subscription.unsubscribe();
      console.log('unsubscribed!');
    }, 2500);
    // Logs: 0 after 1s , 1 after 2s ,  "unsubscribed!" after 2.5s
  
Subject

    // --- Subject
    // special type of Observable, allows values to be multicasted to many Observers
    // is like EventEmitters
    // every Subject is an Observable and an Observer
    class Subject<T> extends Observable implements SubscriptionLike {
      static create: (...args: any[]) => any
      constructor()
      closed: false
      observers: Observer<T>[]
      isStopped: false
      hasError: false
      thrownError: any
      get observed
      lift<R>(operator: Operator<T, R>): Observable<R>
      next(value: T)
      error(err: any)
      complete()
      unsubscribe()
      asObservable(): Observable<T>

      // inherited from index/Observable
      static create: (...args: any[]) => any
      constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic)
      source: Observable<any> | undefined
      operator: Operator<any, T> | undefined
      lift<R>(operator?: Operator<T, R>): Observable<R>
      subscribe(observerOrNext?: Partial<Observer<T>> | ((value: T) => void), error?: (error: any) => void, complete?: () => void): Subscription
      forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise<void>
      pipe(...operations: OperatorFunction<any, any>[]): Observable<any>
      toPromise(promiseCtor?: PromiseConstructorLike): Promise<T | undefined>
    }

    // --- BehaviorSubject - get last message
    // similar to a Subject except it requires an initial value as an argument
    // to mark the starting point of the data stream,
    // when we subscribe, returns the last message

    // --- ReplaySubject - time travel
    // once subscribed, broadcasts all messages, despite if we subscribed late or not
    // access all the values that were broadcast

    // --- AsyncSubject - once completed, get last message
    // similar to BehaviorSubject in terms of emitting the last value once subscribed
    // only difference, requires a complete() method to mark the stream as completed
    // once that is done, the last value is emitted
  
Unsubscription

    // --- take(n) - receive data amount of times before navigating

    // --- takeUntil
    done$ = new AsyncSubject();
    myService$ = interval(1000)
      .pipe(
        tap(curr => console.log(curr)),
        takeUntil(this.done$)
      );
    counter;
    constructor() { }
    ngOnInit() {
      this.myService$
        .subscribe(data => this.counter = data)
    }
    ngOnDestroy() {
      this.done$.next('');
      this.done$.complete();
    }

    // --- with async pipe, unsubscribe is automatic
    // template: Counter: {{ myService$ | async }}
    myService$ = interval(1000)
      .pipe(tap(curr => console.log(curr)));
    // counter
    constructor() { }
    ngOnInit() {
      // this.myService$.subscribe(data => this.counter = data)
    }
  
CREATION

    // --- from - convert Promise, array-like, or an iterable object into an Observable
    import { from } from 'rxjs';
    const data = from(fetch('/api/endpoint'));
    data.subscribe({
     next(response) { console.log(response); },
     error(err) { console.error('Error: ' + err); },
     complete() { console.log('Completed'); }
    });

    // --- fromEvent - Observable from DOM events, or Node.js EventEmitter events or others
    import { fromEvent } from 'rxjs';
    // function fromEvent(target, eventName) { // is the same
    //   return new Observable((observer) => {
    //     const handler = (e) => observer.next(e);
    //     target.addEventListener(eventName, handler);
    //     return () => { target.removeEventListener(eventName, handler); };
    //   });
    // }
    const el = document.getElementById('my-element');
    const mouseMoves = fromEvent(el, 'mousemove');
    const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
      console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
    });

    // --- fromFetch - Observable from Fetch API, to make an HTTP request
    import { of } from 'rxjs';
    import { fromFetch } from 'rxjs/fetch';
    import { switchMap, catchError } from 'rxjs/operators';
    const data$ = fromFetch('https://api.github.com/users?per_page=5').pipe(
     switchMap(response => {
       if (response.ok) { return response.json(); }
       else { return of({ error: true, message: `Error ${response.status}` });  }
     }),
     catchError(err => {
       // Network or other error, handle appropriately
       console.error(err);
       return of({ error: true, message: err.message })
     })
    );
    data$.subscribe({
     next: result => console.log(result),
     complete: () => console.log('done')
    });
    // with Chunked Transfer Encoding
    // promise will resolve as soon as the response headers are received
    import { of } from 'rxjs';
    import { fromFetch } from 'rxjs/fetch';
    const data$ = fromFetch('https://api.github.com/users?per_page=5', {
      selector: response => response.json()
    });
    data$.subscribe({
     next: result => console.log(result),
     complete: () => console.log('done')
    });

    // --- of - converts the arguments to an observable sequence
    import { of } from 'rxjs';
    of(10, 20, 30)
      .subscribe(
        next => console.log('next:', next),
        err => console.log('error:', err),
        () => console.log('the end'),
      );
    // result: 'next: 10' , 'next: 20' , 'next: 30'

    // --- observeOn
    // re-emits all notifications from source Observable with specified scheduler.
    // ensure values in subscribe are called just before browser repaint:
    import { interval } from 'rxjs';
    import { observeOn } from 'rxjs/operators';
    // Intervals are scheduled with async scheduler by default...
    const intervals = interval(10);
    intervals.pipe(
      // ...but we will observe on animationFrame scheduler to ensure smooth animation
      observeOn(animationFrameScheduler),
    ).subscribe(val => {
      someDiv.style.height = val + 'px';
    });

    // --- repeatWhen
    // returns an Observable that mirrors the source Observable
    // with the exception of a complete
    // if the source Observable calls complete,
    // this method will emit to the Observable returned from notifier
    // if that Observable calls complete or error,
    // then this method will call complete or error on the child subscription
    // otherwise this method will resubscribe to the source Observable
    repeatWhen<T>(
      notifier: (notifications: Observable<any>) => Observable<any>
    ): MonoTypeOperatorFunction<T>
    // --- repeat
    // returns an Observable that repeats the stream of items
    // emitted by the source Observable at most count times
    repeat<T>(count: number = -1): MonoTypeOperatorFunction<T>

    // --- publish
    // makes a cold Observable hot.
    // make source$ hot by applying publish operator,
    // then merge each inner observable into a single one and subscribe:
    import { of, zipWith, interval, merge } from "rxjs";
    import { map, publish } from "rxjs/operators";
    const source$ = zipWith(
      interval(2000),
        of(1, 2, 3, 4, 5, 6, 7, 8, 9),
      ).pipe(
        map(values => values[1])
      );
    source$.pipe(
      publish(multicasted$ => {
        return merge(
            multicasted$.pipe(tap(x => console.log('Stream 1:', x))),
            multicasted$.pipe(tap(x => console.log('Stream 2:', x))),
            multicasted$.pipe(tap(x => console.log('Stream 3:', x))),
        );
      })).subscribe();
    /* results every two seconds:
    Stream 1: 1
    Stream 2: 1
    Stream 3: 1
    ...
    Stream 1: 9
    Stream 2: 9
    Stream 3: 9 */

    // --- raceWith
    // mirror first source Observable to emit a next, error or complete notification
    // from the combination of the Observable to which the operator is applied and supplied Observables
    import { interval } from 'rxjs';
    import { mapTo, raceWith } from 'rxjs/operators';
    const obs1 = interval(1000).pipe(mapTo('fast one'));
    const obs2 = interval(3000).pipe(mapTo('medium one'));
    const obs3 = interval(5000).pipe(mapTo('slow one'));
    obs2.pipe(
      raceWith(obs3, obs1)
    ).subscribe(
      winner => console.log(winner)
    ); // outputs a series of 'fast one'

    // --- animationFrames
    // emits the the amount of time elapsed since subscription and timestamp on each animation frame
    // defaults to milliseconds provided to the requestAnimationFrame callback
    // does not end on its own.
    // uuseful for setting up animations with RxJS.
    // tweening a div to move it on the screen:
    import { animationFrames } from 'rxjs';
    import { map, takeWhile, endWith } from 'rxjs/operators';
    function tween(start: number, end: number, duration: number) {
      const diff = end - start;
      return animationFrames().pipe(
        map(({elapsed}) => elapsed / duration), // figure out what percentage of time has passed
        takeWhile(v => v < 1), // take the vector while less than 100%
        endWith(1), // finish with 100%
        map(v => v * diff + start) // calculate the distance traveled between start and end
      );
    }
    // setup a div to move around
    const div = document.createElement('div');
    document.body.appendChild(div);
    div.style.position = 'absolute';
    div.style.width = '40px';
    div.style.height = '40px';
    div.style.backgroundColor = 'lime';
    div.style.transform = 'translate3d(10px, 0, 0)';
    tween(10, 200, 4000).subscribe(x => {
      div.style.transform = `translate3d(${x}px, 0, 0)`;
    });
    // providing a custom timestamp provider:
    import { animationFrames, TimestampProvider } from 'rxjs';
    let now = 0; // custom timestamp provider
    const customTSProvider: TimestampProvider = { now() { return now++; } };
    const source$ = animationFrames(customTSProvider);
    source$.subscribe(({ elapsed }) => console.log(elapsed)); // 0...1...2... on every animation frame
  
COMBINATION

    // --- combineLatest
    // combine multiple Observables to create an Observable whose values
    // are calculated from the latest values of each of its input Observables.
    // - combine an array of Observables:
    import { combineLatest, of } from 'rxjs';
    import { delay, startWith } from 'rxjs/operators';
    const observables = [1, 5, 10].map(
      n => of(n).pipe(
        delay(n * 1000),   // emit 0 and then emit n after n seconds
        startWith(0),
      )
    );
    const combined = combineLatest(observables);
    combined.subscribe(value => console.log(value)); // [0, 0, 0] (immediately) , [1, 0, 0] (in 1s) , [1, 5, 0] (in 5s) , [1, 5, 10] (in 10s)
    // - combine two timer Observables:
    import { combineLatest, timer } from 'rxjs'; // 0, 1, 2... after every second, starting from now
    const firstTimer = timer(0, 1000); // 0, 1, 2... after every second, starting 0,5s from now
    const secondTimer = timer(500, 1000);
    const combinedTimers = combineLatest(firstTimer, secondTimer);
    combinedTimers.subscribe(value => console.log(value)); // [0, 0] (in 0.5s) , [1, 0] (in 1s) , [1, 1] (in 1.5s) , [2, 1] (in 2s)
    // - combine a dictionary of Observables:
    import { combineLatest, of } from 'rxjs';
    import { delay, startWith } from 'rxjs/operators';
    const observables = {
      a: of(1).pipe(delay(1000), startWith(0)),
      b: of(5).pipe(delay(5000), startWith(0)),
      c: of(10).pipe(delay(10000), startWith(0))
    };
    const combined = combineLatest(observables);
    combined.subscribe(value => console.log(value));
    // {a: 0, b: 0, c: 0} immediately
    // {a: 1, b: 0, c: 0} after 1s
    // {a: 1, b: 5, c: 0} after 5s
    // {a: 1, b: 5, c: 10} after 10s
    // - use map operator to dynamically calculate the Body-Mass Index:
    import { combineLatest, of } from 'rxjs';
    import { map } from 'rxjs/operators';
    const weight = of(70, 72, 76, 79, 75);
    const height = of(1.76, 1.77, 1.78);
    const bmi = combineLatest([weight, height]).pipe(
      map(([w, h]) => w / (h * h)),
    );
    bmi.subscribe(x => console.log('BMI is ' + x));
    // BMI is 24.212293388429753
    // BMI is 23.93948099205209
    // BMI is 23.671253629592222

    // --- combineLatestWith
    // create an observable that combines the latest values
    // from all passed observables and the source into arrays and emits them.
    / calculation from two inputs:
    const input1 = document.createElement('input');
    document.body.appendChild(input1);
    const input2 = document.createElement('input');
    document.body.appendChild(input2);
    // get streams of changes
    const input1Changes$ = fromEvent(input1, 'change');
    const input2Changes$ = fromEvent(input2, 'change');
    // combine the changes by adding them together
    input1Changes$.pipe(
      combineLatestWith(input2Changes$),
      map(([e1, e2]) => Number(e1.target.value) + Number(e2.target.value)),
    )
    .subscribe(x => console.log(x));

    // --- combineLatestAll
    // takes an Observable of Observables, and collects all Observables from it
    // once the outer Observable completes, it subscribes to all collected Observables
    // and combines their values using the combineLatest strategy, such that:
    // - every time an inner Observable emits, the output Observable emits
    // when the returned observable emits, it emits all of the latest values by:
    // - if a project function is provided, it is called with each recent value
    //   from each inner Observable in whatever order they arrived,
    //   and the result of the project function is what is emitted by the output Observable
    // - if there is no project function,
    //   an array of all the most recent values is emitted by the output Observable
    // map two click events to a finite interval Observable, then apply combineLatestAll
    import { map, combineLatestAll, take } from 'rxjs/operators';
    import { fromEvent } from 'rxjs/observable/fromEvent';
    const clicks = fromEvent(document, 'click');
    const higherOrder = clicks.pipe(
      map(ev =>
         interval(Math.random() * 2000).pipe(take(3))
      ),
      take(2)
    );
    const result = higherOrder.pipe(
      combineLatestAll()
    );
    result.subscribe(x => console.log(x));

    // --- concat
    // concatenates multiple Observables together
    // by sequentially emitting their values, one Observable after the other
    import { concat, interval, range } from 'rxjs';
    import { take } from 'rxjs/operators';
    // timer counting from 0 to 3 with a synchronous sequence from 1 to 10
    const timer = interval(1000).pipe(take(4));
    const sequence = range(1, 10);
    const result = concat(timer, sequence);
    result.subscribe(x => console.log(x));
    // results in: 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3 -immediate-> 1 ... 10

    // --- concatWith
    // emits all of the values from the source observable, then, once it completes,
    // subscribes to each observable source provided, one at a time, emitting all of their values,
    // and not subscribing to the next one until it completes.
    // Listen for one mouse click, then listen for all mouse moves:
    import { fromEvent } from 'rxjs';
    import { concatWith } from 'rxjs/operators';
    const clicks$ = fromEvent(document, 'click');
    const moves$ = fromEvent(document, 'mousemove');
    clicks$.pipe(
      map(() => 'click'),
      take(1),
      concatWith(
        moves$.pipe(
          map(() => 'move')
        )
      )
    )
    .subscribe(x => console.log(x)); // 'click' 'move' 'move' 'move' ...

    // --- mergeWith
    // merge the values from all observables to an single observable result
    import { fromEvent } from 'rxjs';
    import { map, mergeWith } from 'rxjs/operators';
    const clicks$ = fromEvent(document, 'click').pipe(map(() => 'click'));
    const mousemoves$ = fromEvent(document, 'mousemove').pipe(map(() => 'mousemove'));
    const dblclicks$ = fromEvent(document, 'dblclick').pipe(map(() => 'dblclick'));
    mousemoves$.pipe( mergeWith(clicks$, dblclicks$) )
    .subscribe(x => console.log(x));
    // result (assuming user interactions)
    // "mousemove"
    // "mousemove"
    // "mousemove"
    // "click"
    // "click"
    // "dblclick"

    // --- forkJoin
    // accepts an Array of ObservableInput or a dictionary Object of ObservableInput
    // returns an Observable that emits either an array of values in the exact same order as the passed array,
    // or a dictionary of values in the same shape as the passed dictionary.
    // dictionary of observable inputs:
    import { forkJoin, of, timer } from 'rxjs';
    const observable = forkJoin({
      foo: of(1, 2, 3, 4),
      bar: Promise.resolve(8),
      baz: timer(4000),
    });
    observable.subscribe({
     next: value => console.log(value),
     complete: () => console.log('This is how it ends!'),
    });
    // { foo: 4, bar: 8, baz: 0 } after 4 seconds
    // "This is how it ends!" immediately after
    // array of observable inputs:
    import { forkJoin, of, timer } from 'rxjs';
    const observable = forkJoin([
      of(1, 2, 3, 4),
      Promise.resolve(8),
      timer(4000),
    ]);
    observable.subscribe({
     next: value => console.log(value),
     complete: () => console.log('This is how it ends!'),
    });
    // [4, 8, 0] after 4 seconds
    // "This is how it ends!" immediately after

    // --- startWith
    // return an Observable that emits the items you specify as arguments
    // before it begins to emit items emitted by the source Observable
    import { of } from 'rxjs';
    import { startWith } from 'rxjs/operators';
    of("from source")
      .pipe(startWith("first", "second"))
      .subscribe(x => console.log(x));
    // results: "first" "second" "from source"

    // --- withLatestFrom
    // combine the source Observable with other Observables
    // to create an Observable whose values are calculated
    // from the latest values of each, only when the source emits
    import { fromEvent, interval } from 'rxjs';
    import { withLatestFrom } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const timer = interval(1000);
    const result = clicks.pipe(withLatestFrom(timer));
    result.subscribe(x => console.log(x));
    // on every click event,
    // emit an array with the latest timer event plus the click event

    // --- zipWith , zipAll
    // combines multiple Observables to create an Observable
    // whose values are calculated from the values, in order,
    // of each of its input Observables
    import { zipWith, of } from 'rxjs';
    import { map } from 'rxjs/operators';
    let age$ = of<number>(27, 25, 29);
    let name$ = of<string>('Foo', 'Bar', 'Beer');
    let d$ = of<boolean>(true, true, false);
    zipWith(age$, name$, d$).pipe(
      map(([age, name, d]) => ({ age, name, d })),
    ).subscribe(x => console.log(x));
    // outputs
    // { age: 27, name: 'Foo', d: true }
    // { age: 25, name: 'Bar', d: true }
    // { age: 29, name: 'Beer', d: false }

    // --- reduce
    // combines together all values emitted on the source,
    // using an accumulator function that knows how to join a new source value
    // into the accumulation from the past
    import { fromEvent, interval } from 'rxjs';
    import { reduce, takeUntil, mapTo } from 'rxjs/operators';
    const clicksInFiveSeconds = fromEvent(document, 'click').pipe(
      takeUntil(interval(5000)),
    );
    const ones = clicksInFiveSeconds.pipe(mapTo(1));
    const seed = 0;
    const count = ones.pipe(reduce((acc, one) => acc + one, seed));
    count.subscribe(x => console.log(x));
  
FILTERING

    // --- debounceTime
    // emits a value from the source Observable
    // only after a particular time span has passed without another source emission
    import { fromEvent } from 'rxjs';
    import { debounceTime } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(debounceTime(1000));
    result.subscribe(x => console.log(x));

    // --- distinctUntilChanged
    // returns an Observable that emits all items emitted by the source Observable
    // that are distinct by comparison from the previous item
    import { of } from 'rxjs';
    import { distinctUntilChanged } from 'rxjs/operators';
    interface Person { age: number, name: string }
    of<Person>(
        { age: 4, name: 'Foo'},
        { age: 7, name: 'Bar'},
        { age: 5, name: 'Foo'},
        { age: 6, name: 'Foo'},
      ).pipe( // using a compare function
        distinctUntilChanged((p: Person, q: Person) => p.name === q.name),
        // distinctUntilKeyChanged('name'),
      )
      .subscribe(x => console.log(x));
    // displays:
    // { age: 4, name: 'Foo' }
    // { age: 7, name: 'Bar' }
    // { age: 5, name: 'Foo' }

    // --- filter
    // filter items emitted by the source Observable
    // by only emitting those that satisfy a specified predicate
    import { fromEvent } from 'rxjs';
    import { filter } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const clicksOnDivs = clicks.pipe(filter(ev => ev.target.tagName === 'DIV'));
    clicksOnDivs.subscribe(x => console.log(x));

    // --- partition
    // like filter, but returns two Observables: one like the output of filter,
    // and other with values that did not pass the condition.
    // partition a set of numbers into odds and evens observables
    import { of, partition } from 'rxjs';
    const observableValues = of(1, 2, 3, 4, 5, 6);
    const [evens$, odds$] = partition(observableValues, (value, index) => value % 2 === 0);
    odds$.subscribe(x => console.log('odds', x));
    evens$.subscribe(x => console.log('evens', x));
    // odds 1   odds 3   odds 5   evens 2   evens 4   evens 6

    // --- take
    // emits only the first count values emitted by the source Observable
    // take the first 5 seconds of an infinite 1-second interval Observable:
    import { interval } from 'rxjs';
    import { take } from 'rxjs/operators';
    const intervalCount = interval(1000);
    const takeFive = intervalCount.pipe(take(5));
    takeFive.subscribe(x => console.log(x));
    // Logs: 0 1 2 3 4

    // --- takeUntil
    // emits the values emitted by the source Observable
    // until a notifier Observable emits a value
    // tick every second until the first click happens:
    import { fromEvent, interval } from 'rxjs';
    import { takeUntil } from 'rxjs/operators';
    const source = interval(1000);
    const clicks = fromEvent(document, 'click');
    const result = source.pipe(takeUntil(clicks));
    result.subscribe(x => console.log(x));

    // --- takeWhile
    // emits values emitted by the source Observable
    // so long as each value satisfies the given predicate,
    // and then completes as soon as this predicate is not satisfied
    import { fromEvent } from 'rxjs';
    import { takeWhile } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(takeWhile(ev => ev.clientX > 200));
    result.subscribe(x => console.log(x));

    // --- firstValueFrom
    // converts an observable to a promise by subscribing to the observable,
    // returning a promise that will resolve as soon as the first value arrives from the observable
    // will then be closed.
    // wait for the first value from a stream and emit it from a promise in an async function:
    import { interval, firstValueFrom } from 'rxjs';
    async function execute() {
      const source$ = interval(2000);
      const firstNumber = await firstValueFrom(source$);
      console.log(`The first number is ${firstNumber}`);
    }
    execute(); // "The first number is 0"

    // --- lastValueFrom
    // converts an observable to a promise by subscribing to the observable,
    // waiting for it to complete, and resolving the returned promise with the last value from the observed stream.
    // wait for the last value from a stream and emit it from a promise in an async function:
    import { interval, lastValueFrom } from 'rxjs';
    import { take } from 'rxjs/operators';
    async function execute() {
      const source$ = interval(2000).pipe(take(10));
      const finalNumber = await lastValueFrom(source$);
      console.log(`The final number is ${finalNumber}`);
    }
    execute(); // "The final number is 9"

    // --- skip
    // returns an Observable that skips the first count items emitted by the source Observable
    skip<T>(count: number): MonoTypeOperatorFunction<T>
    // --- skipLast
    // skip the last count values emitted by the source Observable
    import { range } from 'rxjs';
    import { skipLast } from 'rxjs/operators';
    const many = range(1, 5);
    const skipLastTwo = many.pipe(skipLast(2));
    skipLastTwo.subscribe(x => console.log(x)); // Results in: 1 2 3
    // --- skipUntil
    // returns an Observable that skips items emitted by the source Observable
    // until a second Observable emits an item
    skipUntil<T>(notifier: Observable<any>): MonoTypeOperatorFunction<T>
    // --- skipWhile
    // returns an Observable that skips all items emitted by the source Observable
    // as long as a specified condition holds true,
    // but emits all further source items as soon as the condition becomes false
    skipWhile<T>(
      predicate: (value: T, index: number) => boolean
    ): MonoTypeOperatorFunction<T>

    // --- auditTime
    // when it sees a source values, it ignores that plus the next ones
    // for duration milliseconds, and then it emits the most recent value from the source.
    // --- audit
    // like auditTime, but the silencing duration is determined by a second Observable.
    // emit clicks at a rate of at most one click per second:
    import { fromEvent } from 'rxjs';
    import { auditTime } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(auditTime(1000));
    // const result = clicks.pipe(audit(ev => interval(1000)));
    result.subscribe(x => console.log(x));

    // --- exhaustAll
    // flattens an Observable-of-Observables by dropping the next
    // inner Observables while the current inner is still executing.
    // run a finite timer for each click, only if there is no currently active timer:
    import { fromEvent, interval } from 'rxjs';
    import { exhaustAll, map, take } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const higherOrder = clicks.pipe(
      map((ev) => interval(1000).pipe(take(5))),
    );
    const result = higherOrder.pipe(exhaustAll());
    result.subscribe(x => console.log(x));

    // --- exhaustMap
    // maps each value to an Observable,
    // then flattens all of these inner Observables using exhaustAll.
    // run a finite timer for each click, only if there is no currently active timer:
    import { fromEvent, } from 'rxjs';
    import { exhaustMap, take } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(
      exhaustMap((ev) => interval(1000).pipe(take(5))),
    );
    result.subscribe(x => console.log(x));
  
TRANSFORMATION

    // --- map
    // applies a given project function to each value emitted by the source Observable,
    // and emits the resulting values as an Observable
    import { fromEvent } from 'rxjs';
    import { map } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const positions = clicks.pipe(map(ev => ev.clientX));
    positions.subscribe(x => console.log(x));

    // --- mergeMap
    // maps each value to an Observable,
    // then flattens all of these inner Observables using mergeAll
    import { of, interval } from 'rxjs';
    import { mergeMap, map } from 'rxjs/operators';
    const letters = of('a', 'b', 'c');
    const result = letters.pipe(
      mergeMap(x => interval(1000).pipe(map(i => x+i))),
    );
    result.subscribe(x => console.log(x));
    // Results in the following: a0 b0 c0 a1 b1 c1
    // continues to list a,b,c with respective ascending integers
    // --- mergeMapTo
    // like mergeMap, but maps each value always to the same inner Observable
    // --- mergeAll
    // flattens an Observable-of-Observables.
    // spawn a new interval Observable for each click event,
    // and blend their outputs as one Observable:
    import { fromEvent, interval } from 'rxjs';
    import { map, mergeAll } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const higherOrder = clicks.pipe(map((ev) => interval(1000)));
    const firstOrder = higherOrder.pipe(mergeAll());
    firstOrder.subscribe(x => console.log(x));
    // count from 0 to 9 every second for each click,
    // but only allow 2 concurrent timers:
    import { fromEvent, interval } from 'rxjs';
    import { take, map, mergeAll } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const higherOrder = clicks.pipe(
      map((ev) => interval(1000).pipe(take(10))),
    );
    const firstOrder = higherOrder.pipe(mergeAll(2));
    firstOrder.subscribe(x => console.log(x));

    // --- concatMap
    // project each source value to an Observable
    // which is merged in the output Observable, in a serialized fashion
    // waiting for each one to complete before merging the next
    import { fromEvent, interval } from 'rxjs';
    import { concatMap, take } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(
      concatMap(ev => interval(1000).pipe(take(4)),
    );
    result.subscribe(x => console.log(x));
    // Results in the following: (results are not concurrent)
    // For every click on the "document" it will emit values 0 to 3 spaced
    // on a 1000ms interval
    // one click = 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3

    // --- concatMapTo
    // projects each source value to the same Observable
    // which is merged multiple times in a serialized fashion on the output Observable
    // like concatMap, but maps each value always to the same inner Observable
    // for each click event, tick every second from 0 to 3, with no concurrency:
    import { fromEvent, interval } from 'rxjs';
    import { concatMapTo, take } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(
      concatMapTo(interval(1000).pipe(take(4))),
    );
    result.subscribe(x => console.log(x));
    // for every click on the "document" it will emit values 0 to 3 spaced
    // on a 1000ms interval one click = 1000ms-> 0 -1000ms-> 1 -1000ms-> 2 -1000ms-> 3

    // --- switchMap
    // projects each source value to an Observable which is merged in the output Observable,
    // emitting values only from the most recently projected Observable
    import { fromEvent, interval } from 'rxjs';
    import { switchMap } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(switchMap((ev) => interval(1000)));
    result.subscribe(x => console.log(x));

    // --- pluck
    // like map, but picking one of the nested properties
    // of every emitted object.
    // map every click to the tagName of the clicked target element:
    import { fromEvent } from 'rxjs';
    import { pluck } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const tagNames = clicks.pipe(pluck('target', 'tagName'));
    tagNames.subscribe(x => console.log(x));

    // --- scan
    // applies an accumulator function over the source Observable,
    // and returns each intermediate result, with an optional seed value
    // count the number of click events:
    import { fromEvent } from 'rxjs';
    import { scan, mapTo } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const ones = clicks.pipe(mapTo(1));
    const seed = 0;
    const count = ones.pipe(scan((acc, one) => acc + one, seed));
    count.subscribe(x => console.log(x));

    // --- mergeScan
    // applies an accumulator function over the source Observable
    // where the accumulator function itself returns an Observable,
    //then each intermediate Observable returned is merged into the output Observable
    import { fromEvent, of } from 'rxjs';
    import { mapTo, mergeScan } from 'rxjs/operators';
    const click$ = fromEvent(document, 'click');
    const one$ = click$.pipe(mapTo(1));
    const seed = 0;
    const count$ = one$.pipe(
      mergeScan((acc, one) => of(acc + one), seed),
    );
    count$.subscribe(x => console.log(x)); // 1 2 3 4 ...and so on for each click

    // --- switchScan
    // applies an accumulator function over the source Observable
    // where the accumulator function itself returns an Observable,
    // emitting values only from the most recently returned Observable.
    // like scan, but only the most recent Observable returned by the accumulator is merged into the outer Observable
  
UTILITY

    // --- tap
    // perform a side effect for every emission on the source Observable,
    // but return an Observable that is identical to the source
    import { fromEvent } from 'rxjs';
    import { tap, map } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const positions = clicks.pipe(
      tap(ev => console.log(ev)),
      map(ev => ev.clientX),
    );
    positions.subscribe(x => console.log(x));

    // --- count
    // counts the number of emissions on the source and emits that number when the source completes
    // how many seconds have passed before the first click happened:
    import { fromEvent, interval } from 'rxjs';
    import { count, takeUntil } from 'rxjs/operators';
    const seconds = interval(1000);
    const clicks = fromEvent(document, 'click');
    const secondsBeforeClick = seconds.pipe(takeUntil(clicks));
    const result = secondsBeforeClick.pipe(count());
    result.subscribe(x => console.log(x));
    // counts how many odd numbers are there between 1 and 7:
    import { range } from 'rxjs';
    import { count } from 'rxjs/operators';
    const numbers = range(1, 7);
    const result = numbers.pipe(count(i => i % 2 === 1));
    result.subscribe(x => console.log(x));
    // Results in: 4

    // --- min , max
    // operates on an Observable that emits numbers
    // (or items that can be compared with a provided function),
    // and when source Observable completes it emits
    // a single item: the item with the smallest/largest value
    // minimal value of a series of numbers
    import { of } from 'rxjs';
    import { min, max } from 'rxjs/operators';
    of(5, 4, 7, 2, 8).pipe(
      min(),
      // max(),
    ).subscribe(x => console.log(x)); // -> 2
    // use a comparer function to get the minimal item
    import { of } from 'rxjs';
    import { min } from 'rxjs/operators';
    interface Person {
      age: number,
      name: string
    }
    of<Person>(
      {age: 7, name: 'Foo'},
      {age: 5, name: 'Bar'},
      {age: 9, name: 'Beer'},
    ).pipe(
      min<Person>( (a: Person, b: Person) => a.age < b.age ? -1 : 1),
      // max<Person>( (a: Person, b: Person) => a.age < b.age ? -1 : 1),
    ).subscribe((x: Person) => console.log(x.name)); // -> 'Bar'
  
BUFFER , WINDOW

    // --- buffer
    // buffers the source Observable values until closingNotifier emits.
    // --- window* - like buffer, but emit a nested Observable instead of an array.
    // emit array of most recent interval events on every click:
    import { fromEvent, interval } from 'rxjs';
    import { buffer } from 'rxjs/operators';
    // window
    // import { window, mergeAll, map take } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const interval = interval(1000);
    const result = interval.pipe(buffer(clicks));
    // every window of 1 second each, emit at most 2 click events:
    // const result = clicks.pipe(
    //   window(sec),
    //   map(win => win.pipe(take(2))), // each window has at most 2 emissions
    //   mergeAll(),              // flatten the Observable-of-Observables
    // );
    result.subscribe(x => console.log(x));

    // --- bufferCount
    // buffers the source Observable values
    // until the size hits the maximum bufferSize given.
    // emit the last two click events as an array:
    import { fromEvent } from 'rxjs';
    import { bufferCount } from 'rxjs/operators';
    // for windowCount:
    import { windowCount, map, mergeAll } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(bufferCount(2));
    // ignore every 3rd click event, starting from the first one:
    // const result = clicks.pipe(
    //   windowCount(3)),
    //   map(win => win.skip(1)), // skip first of every 3 clicks
    //   mergeAll(),              // flatten the Observable-of-Observables
    // );
    // ignore every 3rd click event, starting from the third one:
    //  windowCount(2, 3),
    //  mergeAll(),              // flatten the Observable-of-Observables
    // );
    result.subscribe(x => console.log(x));

    // --- bufferTime
    // buffers the source Observable values for a specific time period
    // every second, emit an array of the recent click events:
    import { fromEvent } from 'rxjs';
    import { bufferTime } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const buffered = clicks.pipe(bufferTime(1000));
    buffered.subscribe(x => console.log(x));
    // every 5 seconds, emit the click events from the next 2 seconds:
    import { fromEvent } from 'rxjs';
    import { bufferTime } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const buffered = clicks.pipe(bufferTime(2000, 5000));
    buffered.subscribe(x => console.log(x));
    // --- windowTime
    // in every window of 1 second each, emit at most 2 click events
    import { fromEvent } from 'rxjs';
    import { windowTime, map, mergeAll } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(
      windowTime(1000),
      map(win => win.take(2)),   // each window has at most 2 emissions
      mergeAll(),                // flatten the Observable-of-Observables
    );
    result.subscribe(x => console.log(x));
    // every 5 seconds start a window 1 second long,
    // and emit at most 2 click events per window
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(
      windowTime(1000, 5000),
      map(win => win.take(2)),   // each window has at most 2 emissions
      mergeAll(),                // flatten the Observable-of-Observables
    );
    result.subscribe(x => console.log(x));
    // same as example above but with maxWindowCount instead of take
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(
      windowTime(1000, 5000, 2), // each window has still at most 2 emissions
      mergeAll(),                // flatten the Observable-of-Observables
    );
    result.subscribe(x => console.log(x));

    // --- bufferToggle
    // buffers the source Observable values
    // starting from an emission from openings
    // and ending when the output of closingSelector emits
    import { fromEvent, interval, empty } from 'rxjs';
    import { bufferToggle } from 'rxjs/operators';
    // windowToggle
    // import { windowToggle, mergeAll } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const openings = interval(1000);
    const buffered = clicks.pipe(
      bufferToggle(
        openings, i => i % 2 ? interval(500) : empty()
      )
      // windowToggle(
      //   openings, i => i % 2 ? interval(500) : empty()
      // ),
      // mergeAll(),
    );
    buffered.subscribe(x => console.log(x));

    // --- bufferWhen
    // buffers the source Observable values,
    // using a factory function of closing Observables to determine
    // when to close, emit, and reset the buffer
    import { fromEvent, interval } from 'rxjs';
    import { bufferWhen } from 'rxjs/operators';
    // windowWhen
    // import { windowWhen, map, mergeAll } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const buffered = clicks.pipe(
      bufferWhen(
        () => interval(1000 + Math.random() * 4000)
      )
      // windowWhen(
      //   () => interval(1000 + Math.random() * 4000)
      // ),
      // map(win => win.pipe(take(2))),     // each window has at most 2 emissions
      // mergeAll(),                        // flatten the Observable-of-Observables
    );
    buffered.subscribe(x => console.log(x));

    // --- withLatestFrom
    // whenever the source Observable emits a value,
    // it computes a formula using that value
    // plus the latest values from other input Observables,
    // then emits the output of that formula
    import { fromEvent, interval } from 'rxjs';
    import { withLatestFrom } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const timer = interval(1000);
    const result = clicks.pipe(withLatestFrom(timer));
    result.subscribe(x => console.log(x));
  
TIMING

    // --- delay
    // time shifts each item by some specified amount of milliseconds.
    // delay each click by one second:
    import { fromEvent } from 'rxjs';
    import { delay } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    // each click emitted after 1 second
    const delayedClicks = clicks.pipe(delay(1000));
    delayedClicks.subscribe(x => console.log(x));
    // delay all clicks until a future date happens:
    import { fromEvent } from 'rxjs';
    import { delay } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const date = new Date('March 15, 2050 12:00:00'); // in the future
    const delayedClicks = clicks.pipe(delay(date)); // click emitted only after that date
    delayedClicks.subscribe(x => console.log(x));

    // --- timeout
    // errors if Observable does not emit a value in given time span.
    // check if ticks are emitted within certain timespan:
    import { interval } from 'rxjs';
    import { timeout } from 'rxjs/operators';
    const seconds = interval(1000);
    // Lets use bigger timespan to be safe,
    // since "interval" might fire a bit later then scheduled
    seconds.pipe(timeout(1100)).subscribe(
      // will emit numbers just as regular "interval" would
      value => console.log(value),
      err => console.log(err), // Will never be called
    );
    seconds.pipe(timeout(900)).subscribe(
      value => console.log(value), // Will never be called
      // will emit error before even first value is emitted,
      // since it did not arrive within 900ms period
      err => console.log(err)
    );
    // use Date to check if Observable completed:
    import { interval } from 'rxjs';
    import { timeout } from 'rxjs/operators';
    const seconds = interval(1000);
    seconds.pipe(
      timeout(new Date("December 17, 2020 03:24:00")),
    ).subscribe(
      // will emit values as regular "interval" would
      // until December 17, 2020 at 03:24:00
      value => console.log(value),
      // on December 17, 2020 at 03:24:00 it will emit an error,
      // since Observable did not complete by then
      err => console.log(err)
    );

    // --- timeoutWith
    // a version of timeout, lets you specify fallback Observable
    // add fallback observable
    import { intrerval } from 'rxjs';
    import { timeoutWith } from 'rxjs/operators';
    const seconds = interval(1000);
    const minutes = interval(60 * 1000);
    seconds.pipe(timeoutWith(900, minutes))
    .subscribe(
      // After 900ms, will start emitting `minutes`,
      // since first value of `seconds` will not arrive fast enough.
      value => console.log(value),
      // Would be called after 900ms in case of `timeout`,
      // but here will never be called.
      err => console.log(err),
    );

    // --- timeInterval
    // emits an object containing the current value,
    // and the time that has passed
    // between emitting the current value and the previous value,
    // which is calculated by using the provided scheduler now() method
    // to retrieve the current time at each emission,
    // then calculating the difference
    // scheduler defaults to async, so by default, the interval will be in milliseconds
    // emit inteval between current value with the last value:
    const seconds = interval(1000);
    seconds.pipe(timeinterval()).subscribe(
      value => console.log(value),
      err => console.log(err),
    );
    seconds.pipe(timeout(900)).subscribe(
      value => console.log(value),
      err => console.log(err),
    );
    // NOTE: The values will never be this precise,
    // intervals created with `interval` or `setInterval`
    // are non-deterministic.
    // {value: 0, interval: 1000}
    // {value: 1, interval: 1000}
    // {value: 2, interval: 1000}
  
MULTICASTING

    // --- share(options: ShareConfig<T> = {}): Observable
    // return a new Observable that multicasts (shares) the original Observable
    // as long as there is at least one Subscriber
    // this Observable will be subscribed and emitting data
    // when all subscribers have unsubscribed
    // it will unsubscribe from the source Observable
    // because the Observable is multicasting it makes the stream hot
    // this is an alias for multicast(() => new Subject()), refCount().
    // - generate new multicast Observable from the source Observable value:
    import { interval } from 'rxjs';
    import { share, map } from 'rxjs/operators';
    const source = interval(1000)
    .pipe(
      map((x: number) => {
        console.log('Processing: ', x);
        return x*x;
      }),
      share()
    );
    source.subscribe(x => console.log('subscription 1: ', x));
    source.subscribe(x => console.log('subscription 1: ', x));
    // Logs:
    // Processing:  0
    // subscription 1:  0
    // subscription 1:  0
    // Processing:  1
    // subscription 1:  1
    // subscription 1:  1
    // Processing:  2
    // subscription 1:  4
    // subscription 1:  4
    // Processing:  3
    // subscription 1:  9
    // subscription 1:  9
    // ... and so on
    // - with notifier factory, delayed reset:
    import { interval } from 'rxjs';
    import { share, take, timer } from 'rxjs/operators';
    const source = interval(1000).pipe(take(3), share({ resetOnRefCountZero: () => timer(1000) }));
    const subscriptionOne = source.subscribe(x => console.log('subscription 1: ', x));
    setTimeout(() => subscriptionOne.unsubscribe(), 1300);
    setTimeout(() => source.subscribe(x => console.log('subscription 2: ', x)), 1700);
    setTimeout(() => source.subscribe(x => console.log('subscription 3: ', x)), 5000);
    // Logs:
    // subscription 1:  0
    // (subscription 1 unsubscribes here)
    // (subscription 2 subscribes here ~400ms later, source was not reset)
    // subscription 2:  1
    // subscription 2:  2
    // (subscription 2 unsubscribes here)
    // (subscription 3 subscribes here ~2000ms later, source did reset before)
    // subscription 3:  0
    // subscription 3:  1
    // subscription 3:  2

    // --- connect
    // creates an observable by multicasting the source within a function
    // that allows to define the usage of the multicast prior to connection.
    // sharing a totally synchronous observable:
    import { defer, of } from 'rxjs';
    import { tap, connect } from 'rxjs/operators';
    const source$ = defer(() => {
     console.log('subscription started');
     return of(1, 2, 3, 4, 5).pipe(
       tap(n => console.log(`source emitted ${n}`))
     );
    });
    source$.pipe(
     // merging 3 subscriptions to shared$
     connect((shared$) => merge(
         shared$.pipe(map(n => `all ${n}`)),
         shared$.pipe(filter(n => n % 2 === 0), map(n => `even ${n}`)),
         shared$.pipe(filter(n => n % 2 === 1), map(n => `odd ${n}`)),
     ))
    )
    .subscribe(console.log); // expected output: (notice only one subscription)
    // "subscription started" "source emitted 1" "all 1" "odd 1"
    // "source emitted 2" "all 2" "even 2"
    // "source emitted 3" "all 3" "odd 3"
    // "source emitted 4" "all 4" "even 4"
    // "source emitted 5" "all 5" "odd 5"

    // --- connectable
    // creates an observable that multicasts once connect() is called on it
    // connectable<T>(source: ObservableInput<T>, config: ConnectableConfig<T> = DEFAULT_CONFIG): ConnectableObservableLike<T>

    // --- shareReplay - share source and replay specified number of emissions on subscription
    // replay values on subscription differentiates share and shareReplay
    // for side-effects or taxing computations
    // that you do not wish to be executed amongst multiple subscriber,
    // also where late subscribers to a stream that need access to previously emitted values
    // multiple subscribers sharing source:
    import { Subject } from 'rxjs/Subject';
    import { ReplaySubject } from 'rxjs/ReplaySubject';
    import { pluck, share, shareReplay, tap } from 'rxjs/operators';
    const routeEnd = new Subject<{data: any, url: string}>(); // simulate url change with subject
    const lastUrl = routeEnd.pipe( // grab url and share with subscribers
      tap(_ => console.log('executed')),
      pluck('url'),
      // defaults to all values so we set it to just keep and replay last one
      shareReplay(1)
    );
    const initialSubscriber = lastUrl.subscribe(console.log); // requires initial subscription
    routeEnd.next({data: {}, url: 'my-path'}); // simulate route change, logged: 'executed', 'my-path'
    const lateSubscriber = lastUrl.subscribe(console.log); // logged: 'my-path'
  
AJAX , ERROR HANDLING

    // --- ajax
    // - fetch the response object that is being returned from API:
    import { ajax } from 'rxjs/ajax';
    import { map, catchError } from 'rxjs/operators';
    import { of } from 'rxjs';
    const obs$ = ajax(`https://api.github.com/users?per_page=5`).pipe(
      map(userResponse => console.log('users: ', userResponse)),
      catchError(error => {
        console.log('error: ', error);
        return of(error);
      })
    );
    // - using ajax.getJSON() to fetch data from API:
    content_copyopen_in_new
    import { ajax } from 'rxjs/ajax';
    import { map, catchError } from 'rxjs/operators';
    import { of } from 'rxjs';
    const obs$ = ajax.getJSON(`https://api.github.com/users?per_page=5`).pipe(
      map(userResponse => console.log('users: ', userResponse)),
      catchError(error => {
        console.log('error: ', error);
        return of(error);
      })
    );
    // - using ajax() with object as argument and method POST with a two seconds delay:
    content_copyopen_in_new
    import { ajax } from 'rxjs/ajax';
    import { of } from 'rxjs';
    const users = ajax({
      url: 'https://httpbin.org/delay/2',
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'rxjs-custom-header': 'Rxjs'
      },
      body: {
        rxjs: 'Hello World!'
      }
    }).pipe(
      map(response => console.log('response: ', response)),
      catchError(error => {
        console.log('error: ', error);
        return of(error);
      })
    );
    // - using ajax() to fetch. An error object that is being returned from the request:
    content_copyopen_in_new
    import { ajax } from 'rxjs/ajax';
    import { map, catchError } from 'rxjs/operators';
    import { of } from 'rxjs';
    const obs$ = ajax(`https://api.github.com/404`).pipe(
      map(userResponse => console.log('users: ', userResponse)),
      catchError(error => {
        console.log('error: ', error);
        return of(error);
      })
    );

    // --- catchError
    // catches errors on the observable to be handled
    // by returning a new observable or throwing an error.
    // continue with a different Observable when there an error:
    import { of } from 'rxjs';
    import { map, catchError } from 'rxjs/operators';
    of(1, 2, 3, 4, 5).pipe(
      map(n => {
        if (n == 4) { throw 'four!'; }
        return n;
      }), catchError(err => of('I', 'II', 'III', 'IV', 'V')),
    ).subscribe(x => console.log(x)); // 1, 2, 3, I, II, III, IV, V
    // retries the caught source Observable again in case of error,
    // similar to retry() operator
    import { of } from 'rxjs';
    import { map, catchError, take } from 'rxjs/operators';
    of(1, 2, 3, 4, 5).pipe(
      map(n => {
        if (n === 4) { throw 'four!'; }
        return n;
      }),
      catchError((err, caught) => caught),
      take(30),
    ).subscribe(x => console.log(x)); // 1, 2, 3, 1, 2, 3, ...
    // throws a new error when the source Observable throws an error
    import { of } from 'rxjs';
    import { map, catchError } from 'rxjs/operators';
    of(1, 2, 3, 4, 5).pipe(
      map(n => {
        if (n == 4) { throw 'four!'; }
        return n;
      }),
      catchError(err => {
        throw 'error in source. Details: ' + err;
      }),
    ) .subscribe(
      x => console.log(x),
      err => console.log(err)
    ); // 1, 2, 3, error in source. Details: four!

    // --- throwIfEmpty
    // if the source observable completes without emitting a value, it will emit an error
    // error will be created at that time by the optional errorFactory argument,
    // otherwise, the error will be EmptyError.
    // click on dcument or error will be thrown:
    fromEvent(document, 'click').pipe(
      takeUntil(timer(1000)),
      throwIfEmpty(
        () => new Error('the button was not clicked within 1 second')
      ),
    )
    .subscribe({
      next() { console.log('The button was clicked'); },
      error(err) { console.error(err); },
    });

    // --- throttleTime
    // lets a value pass,
    // then ignores source values for the next duration milliseconds.
    // --- throttle
    // like throttleTime, but the silencing duration is determined by a second Observable.
    // emit clicks at a rate of at most one click per second:
    import { fromEvent } from 'rxjs';
    import { throttleTime, throttle } from 'rxjs/operators';
    const clicks = fromEvent(document, 'click');
    const result = clicks.pipe(throttleTime(1000));
    // const result = clicks.pipe(throttle(ev => interval(1000)));
    result.subscribe(x => console.log(x));

    // --- finalize - returns an Observable that mirrors the source Observable,
    // but will call a specified function
    // when the source terminates on complete or error.
    // 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);
      })
    );
  

TOOLS


Back to Main Page