[ angular.json ]

    {
      "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
      "version": 1,
      "newProjectRoot": "projects",
      "projects": {
        "angular-tour": {
          "root": "",
          "sourceRoot": "src",
          "projectType": "application",
          "prefix": "app",
          "schematics": {},
          "architect": {
            "build": {
              "builder": "@angular-devkit/build-angular:browser",
              "options": {
                "outputPath": "dist/browser",
                "index": "src/index.html",
                "main": "src/main.ts",
                "polyfills": "src/polyfills.ts",
                "tsConfig": "src/tsconfig.app.json",
                "assets": [
                  "src/favicon.ico",
                  "src/assets",
                  "src/manifest.webmanifest"
                ],
                "styles": [
                  "src/styles.css"
                ],
                "scripts": [
                  {
                    "input": "node_modules/document-register-element/build/document-register-element.js"
                  }
                ]
              },
              "configurations": {
                "production": {
                  "fileReplacements": [
                    {
                      "replace": "src/environments/environment.ts",
                      "with": "src/environments/environment.prod.ts"
                    }
                  ],
                  "optimization": true,
                  "outputHashing": "all",
                  "sourceMap": true,
                  "extractCss": true,
                  "namedChunks": false,
                  "aot": true,
                  "extractLicenses": true,
                  "vendorChunk": false,
                  "buildOptimizer": true,
                  "budgets": [
                    {
                      "type": "initial",
                      "maximumWarning": "2mb",
                      "maximumError": "5mb"
                    }
                  ],
                  "serviceWorker": true
                },
                "ru": {
                  "aot": true,
                  "outputPath": "dist/angular-tour-ru/",
                  "i18nFile": "src/locale/messages.ru.xlf",
                  "i18nFormat": "xlf",
                  "i18nLocale": "ru",
                  "i18nMissingTranslation": "error"
                }
              }
            },
            "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"
                }
              }
            },
            "extract-i18n": {
              "builder": "@angular-devkit/build-angular:extract-i18n",
              "options": {
                "browserTarget": "angular-tour:build"
              }
            },
            "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"
                ]
              }
            },
            "lint": {
              "builder": "@angular-devkit/build-angular:tslint",
              "options": {
                "tsConfig": [
                  "src/tsconfig.app.json",
                  "src/tsconfig.spec.json"
                ],
                "exclude": [
                  "**/node_modules/**"
                ]
              }
            },
            "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"
                    }
                  ]
                }
              }
            }
          }
        },
        "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/**"
                ]
              }
            }
          }
        }
      },
      "defaultProject": "angular-tour"
    }
    
[ 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)"
            ]
          }
        }
      ]
    }
    
[ package.json ]

    {
      "name": "angular-tour",
      "version": "0.0.0",
      "browserslist": [
        "last 2 versions",
        "not ie <= 10",
        "not ie_mob <= 10"
      ],
      "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "build": "ng build",
        "test": "ng test",
        "lint": "ng lint",
        "e2e": "ng e2e",
        "compile:server": "webpack --config webpack.server.config.js --progress --colors",
        "serve:ssr": "node dist/server",
        "build:ssr": "npm run build:client-and-server-bundles && npm run compile:server",
        "build:client-and-server-bundles": "ng build --prod && ng run angular-tour:server:production"
      },
      "private": true,
      "dependencies": {
        "@angular/animations": "~7.2.0",
        "@angular/common": "~7.2.0",
        "@angular/compiler": "~7.2.0",
        "@angular/core": "~7.2.0",
        "@angular/elements": "^7.2.4",
        "@angular/forms": "~7.2.0",
        "@angular/http": "~7.2.0",
        "@angular/platform-browser": "~7.2.0",
        "@angular/platform-browser-dynamic": "~7.2.0",
        "@angular/platform-server": "~7.2.0",
        "@angular/pwa": "^0.13.6",
        "@angular/router": "~7.2.0",
        "@angular/service-worker": "~7.2.0",
        "@nguniversal/express-engine": "^7.1.1",
        "@nguniversal/module-map-ngfactory-loader": "0.0.0",
        "angular-in-memory-web-api": "^0.8.0",
        "core-js": "^2.5.4",
        "document-register-element": "^1.7.2",
        "express": "^4.15.2",
        "http-server": "^0.11.1",
        "rxjs": "~6.3.3",
        "tslib": "^1.9.0",
        "web-animations-js": "^2.3.1",
        "zone.js": "~0.8.26"
      },
      "devDependencies": {
        "@angular-devkit/build-angular": "^0.13.8",
        "@angular/cli": "~7.2.3",
        "@angular/compiler-cli": "^7.2.10",
        "@angular/language-service": "~7.2.0",
        "@types/jasmine": "~2.8.8",
        "@types/jasminewd2": "~2.0.3",
        "@types/node": "~8.9.4",
        "codelyzer": "~4.5.0",
        "jasmine-core": "~2.99.1",
        "jasmine-spec-reporter": "~4.2.1",
        "karma": "^4.0.1",
        "karma-chrome-launcher": "~2.2.0",
        "karma-coverage-istanbul-reporter": "~2.0.1",
        "karma-jasmine": "~1.1.2",
        "karma-jasmine-html-reporter": "^0.2.2",
        "protractor": "~5.4.0",
        "source-map-explorer": "^1.8.0",
        "ts-loader": "^5.2.0",
        "ts-node": "~7.0.0",
        "tslint": "~5.11.0",
        "typescript": "~3.2.2",
        "webpack-cli": "^3.1.0"
      }
    }
    
   APP
   +---- app-config.ts

    /*
      Must put this interface in its own file instead of app.config.ts
      or else TypeScript gives a (bogus) warning:
      WARNING in ./src/app/... .ts
      "export 'AppConfig' was not found in './app.config'
    */
    export interface AppConfig {
      title: string;
    }
   +---- app-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule, PreloadAllModules } from '@angular/router';

    import { AuthGuard }                          from './auth/auth.guard';

    import { DashboardComponent  } from './dashboard/dashboard.component';
    import { DynamicFormPageComponent } from './dynamic-form/dynamic-form-page.component';

    import {HttpExamplesComponent } from './http-examples/http-examples.component';
    import {PageNotFoundComponent } from './page-not-found.component';
    import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service';

    import { AnimationsComponent } from './animations/animations.component';
    import { TranslationsComponent } from './translations/translations.component';

    const routes: Routes = [
      {
        path: 'dashboard',
        component: DashboardComponent,
        data: { animation: 'dashboard' }
      },
      {
        path: 'admin',
        // loadChildren: () => import('IMPORT_STRING').then(M => M.EXPORT_NAME)
        loadChildren: () => import('./admin/admin.module').then(mod => mod.AdminModule)
        canLoad: [AuthGuard]
      },
      {
        path: 'help-center',
        loadChildren: () => import('./help-center/help-center.module').then(mod => mod.HelpCenterModule)
        data: { preload: true } // flag for SelectivePreloadingStrategy service
      },
      {
        path: 'dynamic-form',
        component: DynamicFormPageComponent,
        outlet: 'fordynamicform'
      },
      { path: 'http-examples', component: HttpExamplesComponent },
      { path: 'animations', component: AnimationsComponent },
      { path: 'translations', component: TranslationsComponent },
      { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
      { path: '**', component: PageNotFoundComponent }
    ];

    @NgModule({
      // forRoot() - supplies the service providers and directives needed for routing
      // and performs the initial navigation based on the current browser URL
      imports: [RouterModule.forRoot(
        routes
        ,{
          // enableTracing: true, // <-- debugging purposes only
          // preloadingStrategy: PreloadAllModules
          preloadingStrategy: SelectivePreloadingStrategyService
          // useHash?: boolean
          // initialNavigation?: InitialNavigation
          // errorHandler?: ErrorHandler
          // onSameUrlNavigation?: 'reload' | 'ignore'
          // scrollPositionRestoration?: 'disabled' | 'enabled' | 'top'
          // anchorScrolling?: 'disabled' | 'enabled'
          // scrollOffset?: [number, number] | (() => [number, number])
          // paramsInheritanceStrategy?: 'emptyOnly' | 'always'
          // malformedUriErrorHandler?: (
          //   error: URIError, urlSerializer: UrlSerializer, url: string) => UrlTree
          // urlUpdateStrategy?: 'deferred' | 'eager'
          // relativeLinkResolution?: 'corrected' | 'legacy'
        }
      )],
      exports: [RouterModule]
    })
    export class AppRoutingModule { }
    
   +---- app.component.css

    * {

    }
   +---- app.component.html

    <h1>{{ title }}</h1>

    <nav>
      <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
      <a routerLink="/currentnotes" routerLinkActive="active">Notes</a>
      <a routerLink="/help-center" routerLinkActive="active">Help Center</a>
      <a routerLink="/admin" routerLinkActive="active">Admin</a>
      <a routerLink="/http-examples" routerLinkActive="active">HttpExamples</a>
      <a routerLink="/animations" routerLinkActive="active">Animations</a>
      <a routerLink="/translations" routerLinkActive="active">Translations</a>

      <a
        [routerLink]="[{ outlets: { fordynamicform: ['dynamic-form'] } }]"
      >Dynamic Form (in own outlet)</a>
      <a routerLink="/no-controller">Invalid URL</a>
    </nav>

    <div [@routeAnimation]="prepareRoute(routerOutlet)">
      <router-outlet #routerOutlet="outlet"></router-outlet>
    </div>
    <router-outlet name="fordynamicform"></router-outlet>

    <app-messages></app-messages>

    <br>
    <hr>
    <button (click)="adSwitch = !adSwitch">
    Ads are: {{adSwitch ? 'false' : 'true'}}
    </button>
    <ng-template [appUnless]="adSwitch">
      <app-ad-banner [ads]="ads"></app-ad-banner>
    </ng-template>
    <hr>

    <input #input value="Message">
    <button (click)="popup.showAsComponent(input.value)">Show as component</button>
    <button (click)="popup.showAsElement(input.value)">Show as element</button>
   +---- app.component.ts

    import { Component, Injector, Inject } from '@angular/core';
    import { RouterOutlet } from '@angular/router';

    import { createCustomElement } from '@angular/elements';
    import { PopupComponent } from './popup/popup.component';
    import { PopupService } from './popup/popup.service';
    import { AdService }         from './ad/ad.service';
    import { AdItem }            from './ad/ad-item';

    import { slideInAnimation } from './animations';

    import { APP_CONFIG, AppConfig } from './app.config';
    import { environment } from './../environments/environment';

    // import { Observable } from 'rxjs';

    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css'],
      animations: [ slideInAnimation ]
    })
    export class AppComponent {
      title: string;
      ads: AdItem[];
      adSwitch: boolean = true; // for unless directive
      constructor(
        @Inject(APP_CONFIG) config: AppConfig,
        injector: Injector,
        public popup: PopupService,
        private adService: AdService
      ) {
        this.title = config.title;
        // Convert `PopupComponent` to a custom element.
        const PopupElement = createCustomElement(PopupComponent, {injector});
        // Register the custom element with the browser.
        customElements.define('popup-element', PopupElement);
        // console.log("Is environment prod: "+environment.production);
      }
      ngOnInit() {
        this.ads = this.adService.getAds();
      }
      prepareRoute(outlet: RouterOutlet) {
        return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
      }
    }
    
   +---- app.config.ts

    import { AppConfig } from './app-config';
    export { AppConfig } from './app-config';

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

    export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');

    export const MAIN_CONFIG: AppConfig = {
      title: 'Angular TOUR'
    };
    
   +---- app.module.ts

    import { BrowserModule } from '@angular/platform-browser';
    import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
    import { NgModule } from '@angular/core';
    import { FormsModule } from '@angular/forms'; // <-- NgModel lives here
    import { ReactiveFormsModule } from '@angular/forms';
    import { HttpClientModule }    from '@angular/common/http';
    import { HttpClientXsrfModule } from '@angular/common/http';
    // import { Router } from '@angular/router';

    import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
    import { InMemoryDataService }  from './in-memory-data.service';
    import { APP_CONFIG, MAIN_CONFIG }    from './app.config';

    import { AppComponent } from './app.component';
    import { AppRoutingModule } from './app-routing.module';
    // import { RouterModule, Routes } from '@angular/router';
    // const appRoutes: Routes = [
    //   { path: 'crisis-center', component: CrisisListComponent },
    //   { path: 'heroes', component: HeroListComponent },

    //   { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
    //   { path: '**', component: PageNotFoundComponent }
    // ];

    import { MessagesComponent } from './messages/messages.component';
    import { DashboardComponent } from './dashboard/dashboard.component';
    import { PopupComponent } from './popup/popup.component';
    import { PopupService } from './popup/popup.service';
    import { MessageService } from './messages/message.service';
    import { AdService } from './ad/ad.service';
    import { AdDirective } from './ad/ad.directive';
    import { AdBannerComponent } from './ad/ad-banner.component';
    import { JobAdComponent } from './ad/job-ad.component';
    import { ProfileComponent } from './ad/profile.component';
    import { HighlightDirective } from './highlight.directive';
    import { UnlessDirective } from './unless.directive';
    import { ExponentitPipe } from './exponentit.pipe';
    import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';
    import { DynamicFormQuestionComponent } from './dynamic-form/dynamic-form-question.component';
    import { DynamicFormPageComponent } from './dynamic-form/dynamic-form-page.component';

    import { HttpExamplesComponent } from './http-examples/http-examples.component';
    import { AuthService }          from './auth/auth.service';
    import { RequestCache, RequestCacheWithMap } from './request-cache.service';
    import { httpInterceptorProviders } from './http-interceptors/index';

    import { FilterNotePipe } from './notes/filter-note.pipe';
    import { NotesModule } from './notes/notes.module';

    // lazy loaded in routes
    // import { HelpCenterModule }      from './help-center/help-center.module';
    // import { AdminModule }      from './admin/admin.module';

    import { PageNotFoundComponent } from './page-not-found.component';
    import { AuthModule }              from './auth/auth.module';
    import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service';

    import { AnimationsComponent } from './animations/animations.component';

    import { TranslationsComponent } from './translations/translations.component';
    import { registerLocaleData } from '@angular/common';
    import localeRu from '@angular/common/locales/ru';
    import { ServiceWorkerModule } from '@angular/service-worker';
    import { environment } from '../environments/environment';
    // import localeFrExtra from '@angular/common/locales/extra/fr';
    // the second parameter 'fr' is optional - custom locale id
    registerLocaleData(localeRu);
    // with extra
    // registerLocaleData(localeFr, 'fr-FR', localeFrExtra);

    @NgModule({
      declarations: [
        AppComponent,

        MessagesComponent,
        DashboardComponent,
        PopupComponent,
        AdDirective,
        AdBannerComponent,
        JobAdComponent,
        ProfileComponent,
        HighlightDirective,
        UnlessDirective,
        ExponentitPipe,
        FilterNotePipe,

        DynamicFormComponent,
        DynamicFormQuestionComponent,
        DynamicFormPageComponent,

        HttpExamplesComponent,

        PageNotFoundComponent,

        AnimationsComponent,

        TranslationsComponent
      ],
      imports: [
        BrowserModule.withServerTransition({ appId: 'serverApp' }),
        FormsModule,
        ReactiveFormsModule,
        BrowserAnimationsModule,

        NotesModule,

        // lazy loaded in routes
        // HelpCenterModule,
        // AdminModule,

        AuthModule,

        AppRoutingModule,
        // RouterModule.forRoot(
        //   appRoutes,
        //   { enableTracing: true } // <-- debugging purposes only
        // )

        // import HttpClientModule after BrowserModule.
        HttpClientModule,
        HttpClientXsrfModule.withOptions({
          cookieName: 'My-Xsrf-Cookie',
          headerName: 'My-Xsrf-Header',
        }),

        // The HttpClientInMemoryWebApiModule module intercepts HTTP requests
        // and returns simulated server responses
        // Remove it when a real server is ready to receive requests !
        HttpClientInMemoryWebApiModule.forRoot(
          InMemoryDataService, {
            dataEncapsulation: false,
            passThruUnknownUrl: true,
            put204: false // return entity after PUT/update
          }
        ),

        ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production })

      ],
      // Include the 'PopupService provider
      // but exclude PopupComponent from compilation
      // because it will be added dynamically
      providers: [
        PopupService,
        MessageService,
        AdService,
        {
          provide: APP_CONFIG,
          useValue: MAIN_CONFIG
        },
        AuthService,
        { provide: RequestCache, useClass: RequestCacheWithMap },
        httpInterceptorProviders,
        SelectivePreloadingStrategyService
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule {
      // // Diagnostic only: inspect router configuration
      // constructor(router: Router) {
      //   // Use a custom replacer to display function names in the route configs
      //   const replacer = (key, value) => (typeof value === 'function') ? value.name : value;
      //   console.log('AppModule router.config: ', JSON.stringify(router.config, replacer, 2));
      // }
    }
    
   +---- app.server.module.ts

    import { NgModule } from '@angular/core';
    import { ServerModule } from '@angular/platform-server';

    import { AppModule } from './app.module';
    import { AppComponent } from './app.component';
    import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader';

    @NgModule({
      imports: [
        AppModule,
        ServerModule,
        ModuleMapLoaderModule,
      ],
      bootstrap: [AppComponent],
    })
    export class AppServerModule {}
    
   +---- ad
   |      +---- ad-banner.component.ts

    import {
      Component, Input, OnInit, ViewChild, ComponentFactoryResolver, OnDestroy
    } from '@angular/core';

    import { AdDirective } from './ad.directive';
    import { AdItem }      from './ad-item';
    import { AdComponent } from './ad.component';
    import { style } from '@angular/animations';
    // import { AdService } from './ad.service';

    // ng-template - for dinamic component load
    @Component({
      selector: 'app-ad-banner',
      template: `
                  <div class="ad-banner-example">
                    <h3>Advertisements</h3>
                    <ng-template ad-host></ng-template>
                  </div>
                `,
      styles: [`
        .ad-banner-example {
          width: 400px;
        }
      `]
    })
    export class AdBannerComponent implements OnInit, OnDestroy {
      @Input() ads: AdItem[];
      currentAdIndex = -1;
      @ViewChild(AdDirective) adHost: AdDirective;
      interval: any;
      constructor(
        private componentFactoryResolver: ComponentFactoryResolver
        // ,private adService: AdService
      ) { }
      ngOnInit() {
        this.loadComponent();
        this.getAds();
      }
      ngOnDestroy() {
        clearInterval(this.interval);
      }
      loadComponent() {
        // this.ads = this.adService.getAds();
        this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
        let adItem = this.ads[this.currentAdIndex];
        let componentFactory =
          this.componentFactoryResolver.resolveComponentFactory(adItem.component);
        let viewContainerRef = this.adHost.viewContainerRef;
        viewContainerRef.clear();
        let componentRef = viewContainerRef.createComponent(componentFactory);
        (<AdComponent>componentRef.instance).data = adItem.data;
      }
      getAds() {
        this.interval = setInterval(() => {
          this.loadComponent();
        }, 3000);
      }
    }
    
   |      +---- ad-item.ts

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

    export class AdItem {
      constructor(public component: Type<any>, public data: any) {}
    }
   |      +---- ad.component.ts

    export interface AdComponent {
      data: any;
    }
    
   |      +---- ad.directive.ts

    import { Directive, ViewContainerRef } from '@angular/core';

    // injects ViewContainerRef to gain access to the view container of the element
    @Directive({
      selector: '[ad-host]', // apply the directive to the element
    })
    export class AdDirective {
      constructor(public viewContainerRef: ViewContainerRef) { }
    }
   |      +---- ad.service.ts

    import { Injectable }           from '@angular/core';
    import { JobAdComponent }   from './job-ad.component';
    import { ProfileComponent } from './profile.component';
    import { AdItem }               from './ad-item';
    @Injectable()
    export class AdService {
      getAds() {
        return [
          new AdItem(
            ProfileComponent,
            {name: 'Bombasto', bio: 'Brave as they come'}),
          new AdItem(
            ProfileComponent,
            {name: 'Dr IQ', bio: 'Smart as they come'}),
          new AdItem(
            JobAdComponent,
            {headline: 'Hiring for several positions',
              body: 'Submit your resume today!'}),
          new AdItem(
            JobAdComponent,
            {headline: 'Openings in all departments',
              body: 'Apply today'}),
        ];
      }
    }
    
   |      +---- job-ad.component.ts

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

    import { AdComponent }      from './ad.component';

    @Component({
      template: `
        <div class="ad">
          <h4>{{data.headline}}</h4>

          {{data.body}}
        </div>
      `,
      styles: [`
        .ad {
          border: 1px solid gray;
          padding: 5px;
          padding-bottom: 20px;
          padding-left: 20px;
          border-radius: 10px;
          background-color: lightblue;
          color: black;
      }`]
    })
    export class JobAdComponent implements AdComponent {
      @Input() data: any;

    }



    /*
    Copyright Google LLC. All Rights Reserved.
    Use of this source code is governed by an MIT-style license that
    can be found in the LICENSE file at http://angular.io/license
    */
   |      +---- profile.component.ts

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

    import { AdComponent }       from './ad.component';

    @Component({
      template: `
        <div class="profile">
          <h3>Featured Profile</h3>
          <h4>{{data.name}}</h4>
          <p>{{data.bio}}</p>
          <strong>Hire today!</strong>
        </div>
      `,
      styles: [`
        .profile {
          border: 1px solid gray;
          padding: 5px;
          padding-bottom: 20px;
          padding-left: 20px;
          border-radius: 10px;
          background-color: lightgreen;
          color: black;
        }`]
    })
    export class ProfileComponent implements AdComponent {
      @Input() data: any;
    }
   +---- admin
   |      +---- admin-dashboard
   |            +---- admin-dashboard.component.html

    <p>Dashboard</p>

    <p>Session ID: {{ sessionId | async }}</p>
    <a id="anchor"></a>
    <p>Token: {{ token | async }}</p>

    Preloaded Modules
    <ul>
      <li *ngFor="let module of modules">{{ module }}</li>
    </ul>
   |            +---- admin-dashboard.component.ts

    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute }     from '@angular/router';
    import { Observable }         from 'rxjs';
    import { map }                from 'rxjs/operators';
    import {
      SelectivePreloadingStrategyService
    } from '../../selective-preloading-strategy.service';

    @Component({
      selector: 'app-admin-dashboard',
      templateUrl: './admin-dashboard.component.html'
    })
    export class AdminDashboardComponent implements OnInit {
      sessionId: Observable<string>;
      token: Observable<string>;
      modules: string[];
      constructor(
        private route: ActivatedRoute,
        preloadStrategy: SelectivePreloadingStrategyService
      ) {
        this.modules = preloadStrategy.preloadedModules;
      }
      ngOnInit() {
        // capture session ID if available
        this.sessionId = this.route
          .queryParamMap
          .pipe(map(params => params.get('session_id') || 'None'));
        // capture the fragment if available
        this.token = this.route
          .fragment
          .pipe(map(fragment => fragment || 'None'));
      }
    }
    
   |      +---- admin-helps
   |            +---- admin-helps.component.html

    <p>
      admin-helps works!
    </p>
    
   |            +---- admin-helps.component.ts

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

    @Component({
      selector: 'app-admin-helps',
      templateUrl: './admin-helps.component.html'
    })
    export class AdminHelpsComponent implements OnInit {

      constructor() { }

      ngOnInit() {
      }

    }
    
   |      +---- admin-notes
   |            +---- admin-notes.component.html

    <p>
      admin-notes works!
    </p>
    
   |            +---- admin-notes.component.ts

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

    @Component({
      selector: 'app-admin-notes',
      templateUrl: './admin-notes.component.html'
    })
    export class AdminNotesComponent implements OnInit {

      constructor() { }

      ngOnInit() {
      }

    }
    
   |      +---- admin-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';

    import { AdminComponent } from './admin.component';
    import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
    import { AdminNotesComponent } from './admin-notes/admin-notes.component';
    import { AdminHelpsComponent } from './admin-helps/admin-helps.component';

    import { AuthGuard }                from '../auth/auth.guard';

    const routes: Routes = [
      {
        path: '',
        component: AdminComponent,
        canActivate: [AuthGuard],
        children: [
          {
            // component-less route makes it easier to guard child routes
            path: '',
            canActivateChild: [AuthGuard],
            children: [
              { path: 'helps', component: AdminHelpsComponent },
              { path: 'notes', component: AdminNotesComponent },
              { path: '', component: AdminDashboardComponent }
            ]
          }
        ]
      }
    ];
    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class AdminRoutingModule { }
    
   |      +---- admin.component.html

    <h3>ADMIN</h3>
    <nav>
      <a
        routerLink="./"
        routerLinkActive="active"
        [routerLinkActiveOptions]="{ exact: true }"
        queryParamsHandling='preserve'
        preserveFragment
      >Dashboard</a>
      <a
        routerLink="./helps"
        routerLinkActive="active"
        queryParamsHandling='preserve'
        preserveFragment
      >Manage Helps</a>
      <a
        routerLink="./notes"
        routerLinkActive="active"
        queryParamsHandling='preserve'
        preserveFragment
      >Manage Notes</a>
    </nav>
    <router-outlet></router-outlet>
   |      +---- admin.component.ts

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

    @Component({
      selector: 'app-admin',
      templateUrl: './admin.component.html'
    })
    export class AdminComponent {
    }
   |      +---- admin.module.ts

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { AdminRoutingModule } from './admin-routing.module';
    import { AdminComponent } from './admin.component';

    import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component';
    import { AdminNotesComponent } from './admin-notes/admin-notes.component';
    import { AdminHelpsComponent } from './admin-helps/admin-helps.component';

    @NgModule({
      declarations: [
        AdminComponent,
        AdminDashboardComponent,
        AdminNotesComponent,
        AdminHelpsComponent
      ],
      imports: [
        CommonModule,
        AdminRoutingModule
      ]
    })
    export class AdminModule { }
    
   +---- animations
   |      +---- animations.component.css

    :host {
    display: block;
    }

    .open-close-container {
    border: 1px solid #dddddd;
    margin-top: 1em;
    padding: 20px 20px 0px 20px;
    color: #000000;
    font-weight: bold;
    font-size: 20px;
    }

    .insert-remove-container {
      border: 1px solid #dddddd;
      margin-top: 1em;
      padding: 20px 20px 0px 20px;
      color: #000000;
      font-weight: bold;
      font-size: 20px;
    }

    .kfBox {
      width: 10em;
      border: .2em solid black;
      display: block;
      line-height: 5em;
      text-align: center;
      font-size: 2em;
      color: white;
    }
   |      +---- animations.component.html

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

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


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

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

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

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

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

    import {
      Component, OnInit, HostBinding, EventEmitter
    } from '@angular/core';
    import {
      state, style, trigger,
      animate, transition, group,
      query, stagger, keyframes,
      useAnimation,
      AnimationEvent
    } from '@angular/animations';

    import {
      transOnAnimation, transOffAnimation
    } from '../animations';

    import { NOTES } from '../notes/mock-notes';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    }
   +---- animations.ts

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

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

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

    import { Injectable }       from '@angular/core';
    import {
      CanActivate, Router,
      ActivatedRouteSnapshot,
      RouterStateSnapshot,
      CanActivateChild,
      NavigationExtras,
      CanLoad, Route
    }                           from '@angular/router';
    import { AuthService }      from './auth.service';

    @Injectable({
      providedIn: 'root',
    })
    export class AuthGuard implements CanActivate, CanActivateChild, CanLoad {
      constructor(
        private authService: AuthService,
        private router: Router
      ) {}

      canActivate(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): boolean {
        let url: string = state.url;
        return this.checkLogin(url);
      }
      canActivateChild(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): boolean {
        return this.canActivate(route, state);
      }
      canLoad(route: Route): boolean {
        let url = `/${route.path}`;
        return this.checkLogin(url);
      }
      checkLogin(url: string): boolean {
        if (this.authService.isLoggedIn) { return true; }
        this.authService.redirectUrl = url; // attempted URL for redirecting
        let sessionId = 123456789; // dummy session id
        // navigation extras object: global query params and fragment
        let navigationExtras: NavigationExtras = {
          queryParams: { 'session_id': sessionId },
          fragment: 'anchor'
        };
        // navigate to the login page with extras
        this.router.navigate(['/login'], navigationExtras);
        return false;
      }
    }
    
   |      +---- auth.module.ts

    import { NgModule }       from '@angular/core';
    import { CommonModule }   from '@angular/common';
    import { FormsModule }    from '@angular/forms';

    import { LoginComponent }    from './login/login.component';

    // import { AuthRoutingModule } from './auth-routing.module';
    import { RouterModule, Routes } from '@angular/router';
    const appRoutes: Routes = [
      { path: 'login', component: LoginComponent }
    ];

    @NgModule({
      imports: [
        CommonModule,
        FormsModule,
        // ,AuthRoutingModule
        RouterModule.forRoot(appRoutes)
      ],
      declarations: [
        LoginComponent
      ]
    })
    export class AuthModule {}
   |      +---- auth.service.ts

    import { Injectable } from '@angular/core';
    import { Observable, of } from 'rxjs';
    import { tap, delay } from 'rxjs/operators';

    /** Mock client-side authentication/authorization service */
    @Injectable({
      providedIn: 'root',
    })
    export class AuthService {
      isLoggedIn = false;
      redirectUrl: string; // url to redirect after login
      login(): Observable<boolean> {
        return of(true).pipe(
          delay(1000),
          tap(val => this.isLoggedIn = true)
        );
      }
      logout(): void {
        this.isLoggedIn = false;
      }
      getAuthorizationToken() {
        return 'some-auth-token';
      }
    }
   |      +---- login
   |            +---- login.component.html

    <h2>LOGIN</h2>
    <p>{{message}}</p>
    <p>
      <button
        (click)="login()"
        *ngIf="!authService.isLoggedIn"
      >Login</button>
      <button
        (click)="logout()"
        *ngIf="authService.isLoggedIn"
      >Logout</button>
    </p>
   |            +---- login.component.ts

    import { Component } from '@angular/core';
    import { Router, NavigationExtras } from '@angular/router';
    import { AuthService } from '../auth.service';

    @Component({
      selector: 'app-login',
      templateUrl: './login.component.html'
    })
    export class LoginComponent {
      message: string;
      constructor(
        public authService: AuthService,
        public router: Router
      ) {
        this.setMessage();
      }
      setMessage() {
        this.message = 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out');
      }
      login() {
        this.message = 'Trying to log in ...';
        this.authService.login().subscribe(() => {
          this.setMessage();
          if (this.authService.isLoggedIn) {
            // redirect using default or attempted url
            let redirect = this.authService.redirectUrl ?
                          this.authService.redirectUrl : '/help-center/admin';
            // keep navigation extras object
            let navigationExtras: NavigationExtras = {
              queryParamsHandling: 'preserve', // or 'merge', to concat params
              preserveFragment: true
            };
            this.router.navigate([redirect], navigationExtras); // redirect
          }
        });
      }
      logout() {
        this.authService.logout();
        this.setMessage();
      }
    }
   +---- can-deactivate.guard.ts

    import { Injectable }    from '@angular/core';
    import { CanDeactivate } from '@angular/router';
    import { Observable }    from 'rxjs';

    export interface CanComponentDeactivate {
    canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
    }

    @Injectable({
      providedIn: 'root',
    })
    export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
      canDeactivate(component: CanComponentDeactivate) {
        return component.canDeactivate ? component.canDeactivate() : true;
      }
    }
   +---- dashboard
   |      +---- dashboard.component.css

    [class*='col-'] {
      float: left;
      padding-right: 20px;
      padding-bottom: 20px;
    }
    [class*='col-']:last-of-type {
      padding-right: 0;
    }
    a {
      text-decoration: none;
    }
    *, *:after, *:before {
      -webkit-box-sizing: border-box;
      -moz-box-sizing: border-box;
      box-sizing: border-box;
    }
    h3 {
      text-align: center;
      margin-bottom: 0;
    }

    .grid {
      margin: 0;
    }
    .grid-pad {
      padding: 10px 0;
    }
    .grid-pad > [class*='col-']:last-of-type {
      padding-right: 20px;
    }
    .col-1-4 {
      width: 25%;
    }
    .module {
      padding: 20px;
      text-align: center;
      color: #eee;
      max-height: 120px;
      min-width: 120px;
      background-color: #607d8b;
      border-radius: 2px;
    }
    .module:hover {
      background-color: #eee;
      cursor: pointer;
      color: #607d8b;
    }
    @media (max-width: 600px) {
      .module {
        font-size: 10px;
        max-height: 75px; }
    }
    @media (max-width: 1024px) {
      .grid {
        margin: 0;
      }
      .module {
        min-width: 60px;
      }
    }
   |      +---- dashboard.component.html

    <h3>Top Notes</h3>
    <div class="grid grid-pad">
      <a
        *ngFor="let note of notes"
        class="col-1-4"
        routerLink="/note/{{note.id}}"
      >
        <div class="module note" [appHighlight]="hColor">
          <h4>{{note.title}}</h4>
        </div>
      </a>
    </div>
    <h4>Pick a highlight color</h4>
    <div>
      <input type="radio" name="colors" (click)="hColor='lightgreen'">Green
      <input type="radio" name="colors" (click)="hColor='yellow'">Yellow
      <input type="radio" name="colors" (click)="hColor='cyan'">Cyan
    </div>
    
   |      +---- dashboard.component.ts

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

    @Component({
      selector: 'app-dashboard',
      templateUrl: './dashboard.component.html',
      styleUrls: ['./dashboard.component.css']
    })
    export class DashboardComponent implements OnInit {
      notes: Note[] = [];
      hColor: string;
      constructor(private noteService: NoteService) { }
      ngOnInit() {
        this.getNotes();
      }
      getNotes(): void {
        this.noteService.getNotes()
          .subscribe(notes => this.notes = notes.slice(1, 5));
      }
    }
    
   +---- dialog.service.spec.ts

    // Straight Jasmine testing without Angular testing support
    import { DialogService }  from './dialog.service';
    describe('DialogService', () => {
    let service: DialogService;
    beforeEach(() => { service = new DialogService(); });

    //  it('#confirm', () => {
    //    expect(service.confirm()).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();
    //   });
    // });

    });
   +---- dialog.service.ts

    import { Injectable } from '@angular/core';
    import { Observable, of } from 'rxjs';

    // Async modal dialog service
    // makes this app easier to test by faking this service
    // TODO: better modal implementation that doesn't use window.confirm
    @Injectable({
      providedIn: 'root',
    })
    export class DialogService {
      // ask user to confirm an action
      // `message` explains the action and choices
      // returns observable resolving to `true`=confirm or `false`=cancel
      confirm(message?: string): Observable<boolean> {
        const confirmation = window.confirm(message || 'Is it OK?');
        return of(confirmation);
      };
    }
   +---- dynamic-form
   |      +---- dynamic-form-page.component.ts

    import { Component, OnInit } from '@angular/core';
    import { DropdownQuestion } from './question-dropdown';
    import { QuestionBase }     from './question-base';
    import { TextboxQuestion }  from './question-textbox';
    @Component({
      selector: 'app-dynamic-form-page',
      template: `
        <div>
          <h2>Job Application for Heroes</h2>
          <app-dynamic-form [questions]="questions"></app-dynamic-form>
        </div>
      `
    })
    export class DynamicFormPageComponent implements OnInit {
      questions: any[];

      constructor() { }

      ngOnInit() {
        this.questions = this.getQuestions();
      }

      // TODO: get from a remote source of question metadata
      // TODO: make asynchronous
      getQuestions() {
        let questions: QuestionBase<any>[] = [
          new DropdownQuestion({
            key: 'brave',
            label: 'Bravery Rating',
            options: [
              {key: 'solid',  value: 'Solid'},
              {key: 'great',  value: 'Great'},
              {key: 'good',   value: 'Good'},
              {key: 'unproven', value: 'Unproven'}
            ],
            order: 3
          }),
          new TextboxQuestion({
            key: 'firstName',
            label: 'First name',
            value: 'Bombasto',
            required: true,
            order: 1
          }),
          new TextboxQuestion({
            key: 'emailAddress',
            label: 'Email',
            type: 'email',
            order: 2
          })
        ];
        return questions.sort((a, b) => a.order - b.order);
      }
    }
    
   |      +---- dynamic-form-question.component.html

    <div [formGroup]="form">
      <label [attr.for]="question.key">{{question.label}}</label>
      <div [ngSwitch]="question.controlType">
        <input *ngSwitchCase="'textbox'" [formControlName]="question.key"
                [id]="question.key" [type]="question.type">
        <select [id]="question.key" *ngSwitchCase="'dropdown'" [formControlName]="question.key">
          <option *ngFor="let opt of question.options" [value]="opt.key">{{opt.value}}</option>
        </select>
      </div>
      <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div>
    </div>
   |      +---- dynamic-form-question.component.ts

    import { Component, Input } from '@angular/core';
    import { FormGroup }        from '@angular/forms';

    import { QuestionBase }     from './question-base';

    @Component({
      selector: 'app-question',
      templateUrl: './dynamic-form-question.component.html'
    })
    export class DynamicFormQuestionComponent {
      @Input() question: QuestionBase<any>;
      @Input() form: FormGroup;
      get isValid() { return this.form.controls[this.question.key].valid; }
    }
   |      +---- dynamic-form.component.html

    <div>
      <form (ngSubmit)="onSubmit()" [formGroup]="form">
        <div *ngFor="let question of questions" class="form-row">
          <app-question [question]="question" [form]="form"></app-question>
        </div>
        <div class="form-row">
          <button type="submit" [disabled]="!form.valid">Save</button>
          <button (click)="cancel()">Cancel</button>
        </div>
      </form>
      <div *ngIf="payLoad" class="form-row">
        <strong>Saved the following values</strong><br>{{payLoad}}
      </div>
    </div>
   |      +---- dynamic-form.component.ts

    import { Component, Input, OnInit }  from '@angular/core';
    import { Router }                 from '@angular/router';
    import { FormGroup }                 from '@angular/forms';
    import { QuestionBase }              from './question-base';
    import { QuestionControlService }    from './question-control.service';
    @Component({
      selector: 'app-dynamic-form',
      templateUrl: './dynamic-form.component.html',
      providers: [ QuestionControlService ]
    })
    export class DynamicFormComponent implements OnInit {
      @Input() questions: QuestionBase<any>[] = [];
      form: FormGroup;
      payLoad = '';
      constructor(
        private qcs: QuestionControlService,
        private router: Router
      ) {  }
      ngOnInit() {
        this.form = this.qcs.toFormGroup(this.questions);
      }
      onSubmit() {
        this.payLoad = JSON.stringify(this.form.value);
      }
      cancel() {
        this.closePopup();
      }
      closePopup() {
        // clears the contents of the named outlet
        this.router.navigate([{ outlets: { fordynamicform: null }}]);
      }
    }
   |      +---- question-base.ts

    export class QuestionBase<T> {
      value: T;
      key: string;
      label: string;
      required: boolean;
      order: number;
      controlType: string;

      constructor(options: {
          value?: T,
          key?: string,
          label?: string,
          required?: boolean,
          order?: number,
          controlType?: string
        } = {}) {
        this.value = options.value;
        this.key = options.key || '';
        this.label = options.label || '';
        this.required = !!options.required;
        this.order = options.order === undefined ? 1 : options.order;
        this.controlType = options.controlType || '';
      }
    }
   |      +---- question-control.service.ts

    import { Injectable }   from '@angular/core';
    import { FormControl, FormGroup, Validators } from '@angular/forms';
    import { QuestionBase } from './question-base';
    @Injectable()
    export class QuestionControlService {
      constructor() { }
      toFormGroup(questions: QuestionBase<any>[] ) {
        let group: any = {};
        questions.forEach(question => {
          group[question.key] = question.required ?
          new FormControl(question.value || '', Validators.required) :
          new FormControl(question.value || '');
        });
        return new FormGroup(group);
      }
    }
   |      +---- question-dropdown.ts

    import { QuestionBase } from './question-base';

    export class DropdownQuestion extends QuestionBase<string> {
      controlType = 'dropdown';
      options: {key: string, value: string}[] = [];

      constructor(options: {} = {}) {
        super(options);
        this.options = options['options'] || [];
      }
    }
   |      +---- question-textbox.ts

    import { QuestionBase } from './question-base';

    export class TextboxQuestion extends QuestionBase<string> {
      controlType = 'textbox';
      type: string;

      constructor(options: {} = {}) {
        super(options);
        this.type = options['type'] || '';
      }
    }
   +---- exponentit.pipe.ts

    import { Pipe, PipeTransform } from '@angular/core';
    /*
    * Raise the value exponentially
    * Takes an exponent argument that defaults to 1.
    * Usage: value | exponentit:exponent
    * Example: {{ 2 | exponentit:10 }} // formats to: 1024
    */
    @Pipe({name: 'exponentit'})
    export class ExponentitPipe implements PipeTransform {
      transform(value: number, exponent: string): number {
        let exp = parseFloat(exponent);
        return Math.pow(value, isNaN(exp) ? 1 : exp);
      }
    }
   +---- help-center
   |      +---- help-center-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';

    import { HelpCenterComponent } from './help-center.component';
    import { HelpListComponent }       from './help-list/help-list.component';
    import { HelpDetailsComponent }     from './help-details/help-details.component';
    import { HelpDetailsResolverService } from './help-details-resolver.service';

    import { CanDeactivateGuard } from '../can-deactivate.guard';

    const routes: Routes = [
      {
        path: '',
        component: HelpCenterComponent,
        children: [
          {
            path: '',
            component: HelpListComponent,
            children: [
              {
                path: ':id',
                component: HelpDetailsComponent,
                canDeactivate: [CanDeactivateGuard],
                resolve: {
                  help: HelpDetailsResolverService
                }
              }
              // ,{
              //   path: '',
              //   component: HelpCenterComponent
              // }
            ]
          }
        ]
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class HelpCenterRoutingModule { }
    
   |      +---- help-center.component.ts

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

    @Component({
      selector: 'app-help-center',
      template:`
        <h2>HELP CENTER</h2>
        <router-outlet></router-outlet>
      `
    })
    export class HelpCenterComponent implements OnInit {

      constructor() { }

      ngOnInit() {
      }

    }
    
   |      +---- help-center.module.ts

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule }    from '@angular/forms';

    import { HelpCenterRoutingModule } from './help-center-routing.module';
    import { HelpCenterComponent } from './help-center.component';
    import { HelpListComponent } from './help-list/help-list.component';
    import { HelpDetailsComponent } from './help-details/help-details.component';

    @NgModule({
      declarations: [
        HelpCenterComponent,
        HelpListComponent,
        HelpDetailsComponent
      ],
      imports: [
        CommonModule,
        FormsModule,
        HelpCenterRoutingModule
      ]
    })
    export class HelpCenterModule { }
    
   |      +---- help-details
   |            +---- help-details.component.html

    <div *ngIf="help">
      <h3>"{{ editName }}"</h3>
      <div>
        <label>Id: </label>{{ help.id }}</div>
      <div>
        <label>Name: </label>
        <input [(ngModel)]="editName" placeholder="name"/>
      </div>
      <p>
        <button (click)="save()">Save</button>
        <button (click)="cancel()">Cancel</button>
      </p>
    </div>
   |            +---- help-details.component.ts

    import { Component, OnInit, HostBinding } from '@angular/core';
    import { ActivatedRoute, Router } from '@angular/router';
    import { Observable } from 'rxjs';

    import { Help }         from '../help';
    import { DialogService }  from '../../dialog.service';

    @Component({
      selector: 'app-help-details',
      templateUrl: './help-details.component.html'
    })
    export class HelpDetailsComponent implements OnInit {
      help: Help;
      editName: string;

      constructor(
        private route: ActivatedRoute,
        private router: Router,
        public dialogService: DialogService
      ) {}

      ngOnInit() {
        this.route.data
          .subscribe((data: { help: Help }) => {
            this.editName = data.help.name;
            this.help = data.help;
          });
      }

      cancel() {
        this.gotoHelps();
      }

      save() {
        this.help.name = this.editName;
        this.gotoHelps();
      }

      canDeactivate(): Observable<boolean> | boolean {
        if (!this.help || this.help.name === this.editName) {
          return true; // no changes or no data for component
        }
        // decision observable
        return this.dialogService.confirm('Discard changes?');
      }

      gotoHelps() {
        let helpId = this.help ? this.help.id : null;
        // Pass along the help id if available
        // so that the HelpListComponent can select that help
        // Add a totally useless `foo` parameter for kicks
        // Relative navigation back to the help
        this.router.navigate(['../', { id: helpId, foo: 'foo' }], { relativeTo: this.route });
      }
    }
    
   |      +---- help-details-resolver.service.ts

    import { Injectable }             from '@angular/core';
    import {
      Router, Resolve,
      RouterStateSnapshot,
      ActivatedRouteSnapshot
    }                                 from '@angular/router';
    import { Observable, of, EMPTY }  from 'rxjs';
    import { mergeMap, take }         from 'rxjs/operators';

    import { HelpsService }  from './helps.service';
    import { Help } from './help';

    @Injectable({
      providedIn: 'root',
    })
    export class HelpDetailsResolverService implements Resolve<Help> {
      constructor(
        private hs: HelpsService,
        private router: Router
      ) {}

      resolve(
        route: ActivatedRouteSnapshot,
        state: RouterStateSnapshot
      ): Observable<Help> | Observable<never> {
        let id = route.paramMap.get('id');

        return this.hs.getHelp(id).pipe(
          take(1), // Observable completes after retrieving the first value
          mergeMap(help => {
            if (help) {
              return of(help);
            } else { // id not found
              this.router.navigate(['/help-center']);
              return EMPTY;
            }
          })
        );
      }
    }
    
   |      +---- help-list
   |            +---- help-list.component.css

    /* CrisisListComponent's private CSS styles */
    .helps {
    margin: 0 0 2em 0;
    list-style-type: none;
    padding: 0;
    width: 24em;
    }
    .helps li {
    position: relative;
    cursor: pointer;
    background-color: #EEE;
    margin: .5em;
    padding: .3em 0;
    height: 1.6em;
    border-radius: 4px;
    }

    .helps li:hover {
    color: #607D8B;
    background-color: #DDD;
    }

    .helps a {
    color: #888;
    text-decoration: none;
    display: block;
    }

    .helps a:hover {
    color:#607D8B;
    }

    .helps .badge {
    display: inline-block;
    font-size: small;
    color: white;
    padding: 0.8em 0.7em 0 0.7em;
    background-color: #607D8B;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -4px;
    height: 1.8em;
    min-width: 16px;
    text-align: right;
    margin-right: .8em;
    border-radius: 4px 0 0 4px;
    }

    button {
    background-color: #eee;
    border: none;
    padding: 5px 10px;
    border-radius: 4px;
    cursor: pointer;
    cursor: hand;
    font-family: Arial;
    }

    button:hover {
    background-color: #cfd8dc;
    }

    button.delete {
    position: relative;
    left: 194px;
    top: -32px;
    background-color: gray !important;
    color: white;
    }

    .helps li.selected {
    background-color: #CFD8DC;
    color: white;
    }
    .helps li.selected:hover {
    background-color: #BBD8DC;
    }
    
   |            +---- help-list.component.html

    <ul class="helps">
      <li *ngFor="let help of helps$ | async"
        [class.selected]="help.id === selectedId">
        <a [routerLink]="[help.id]">
          <span class="badge">{{ help.id }}</span>{{ help.name }}
        </a>
      </li>
    </ul>

    <router-outlet></router-outlet>
   |            +---- help-list.component.ts

    import { Component, OnInit }  from '@angular/core';
    import { ActivatedRoute }     from '@angular/router';
    import { Observable }     from 'rxjs';
    import { switchMap }      from 'rxjs/operators';

    import { HelpsService }  from '../helps.service';
    import { Help }         from '../help';

    @Component({
      selector: 'app-help-list',
      templateUrl: './help-list.component.html',
      styleUrls: ['./help-list.component.css']
    })
    export class HelpListComponent implements OnInit {
      helps$: Observable<Help[]>;
      selectedId: number;

      constructor(
        private service: HelpsService,
        private route: ActivatedRoute
      ) {}

      ngOnInit() {
        this.helps$ = this.route.paramMap.pipe(
          switchMap(params => {
            this.selectedId = +params.get('id');
            return this.service.getHelps();
          })
        );
      }
    }
   |      +---- help.service.spec.ts

    import { HelpsService } from './helps.service';
    import { MessageService } from '../messages/message.service';

    describe('HelpsService without Angular testing support', () => {
    let helpsService: HelpsService;

    it('#getValue should return real value from the real service', () => {
      helpsService = new HelpsService(new MessageService());
      expect(helpsService.getValue()).toBe('real value');
    });

    // it('#getValue should return faked value from a fakeService', () => {
    //   helpsService = new HelpsService(new FakeMessageService());
    //   expect(helpsService.getValue()).toBe('faked service value');
    // });

    // it('#getValue should return faked value from a fake object', () => {
    //   const fake =  { getValue: () => 'fake value' };
    //   helpsService = new HelpsService(fake as MessageService);
    //   expect(helpsService.getValue()).toBe('fake value');
    // });

    it('#getValue should return stubbed value from a spy', () => {
      // create `getValue` spy on an object representing the MessageService
      const messageServiceSpy =
        jasmine.createSpyObj('MessageService', ['getValue']);

      // set the value to return when the `getValue` spy is called.
      const stubValue = 'stub value';
      messageServiceSpy.getValue.and.returnValue(stubValue);

      helpsService = new HelpsService(messageServiceSpy);

      expect(helpsService.getValue())
        .toBe(stubValue, 'service returned stub value');
      expect(messageServiceSpy.getValue.calls.count())
        .toBe(1, 'spy method was called once');
      expect(messageServiceSpy.getValue.calls.mostRecent().returnValue)
        .toBe(stubValue);
    });
    });
   |      +---- help.ts

    export class Help {
      id: number;
      name: string;
    }
      
   |      +---- helps.service.ts

    import { BehaviorSubject } from 'rxjs';
    import { map } from 'rxjs/operators';

    import { Injectable } from '@angular/core';
    import { MessageService } from '../messages/message.service';
    import { Help } from './help';
    import { HELPS } from './mock-helps';

    @Injectable({
      providedIn: 'root',
    })
    export class HelpsService {
      static nextHelpId = 100;
      private helps$: BehaviorSubject<Help[]> = new BehaviorSubject<Help[]>(HELPS);

      constructor(
        private messageService: MessageService
      ) { }

      getHelps() { return this.helps$; }

      getHelp(id: number | string) {
        return this.getHelps().pipe(
          map(helps => helps.find(help => help.id === +id))
        );
      }

      addHelp(name: string) {
        name = name.trim();
        if (name) {
          let help = { id: HelpsService.nextHelpId++, name };
          HELPS.push(help);
          this.helps$.next(HELPS);
        }
      }
    }
   |      +---- mock-helps.ts

    import { Help } from './help';

    export const HELPS: Help[] = [
      { id: 1, name: 'Dragon Burning Cities' },
      { id: 2, name: 'Sky Rains Great White Sharks' },
      { id: 3, name: 'Giant Asteroid Heading For Earth' },
      { id: 4, name: 'Procrastinators Meeting Delayed Again' },
    ];
    
   +---- highlight.directive.ts

    import { Directive, ElementRef, HostListener, Input } from '@angular/core';

    @Directive({
      selector: '[appHighlight]' // brackets ([]) make it an attribute selector
    })
    export class HighlightDirective {
      // this property is public and available for binding
      // <p [appHighlight]="color" defaultColor="violet">
      @Input() defaultColor: string;
      // alias directive itself as an input for color from outside
      // <p [appHighlight]="color">Highlight me!</p> // waits "color" to be a definition
      @Input('appHighlight') highlightColor: string;
      constructor(private el: ElementRef) { }
      @HostListener('mouseenter') onMouseEnter() {
        this.highlight(this.highlightColor || this.defaultColor || 'red');
      }
      @HostListener('mouseleave') onMouseLeave() {
        this.highlight(null);
      }
      private highlight(color: string) {
        this.el.nativeElement.style.backgroundColor = color;
      }
    }
    
   +---- http-examples
   |      +---- http-examples.component.html

    <button (click)="clear(); showConfig()">showConfig</button>
    <button (click)="clear(); showConfigResponse()">showConfigResponse</button>
    <button (click)="clear(); showTextfileContent()">showTextfileContent</button>
    <button (click)="clear(); makeError()">makeError</button>
    <button (click)="clear()">clear</button>
    <span *ngIf="result_obj">
      <p *ngFor="let key of objectKeys(result_obj)">
        {{key}}: {{result_obj[key]}}
      </p>
      <!-- <p>API URL is "{{result_obj.apiUrl}}"</p> -->
      <!-- <p>Textfile URL is "{{result_obj.textfile}}"</p> -->
      <div *ngIf="headers">
        Response headers:
        <ul>
          <li *ngFor="let header of headers">{{header}}</li>
        </ul>
      </div>
    </span>
    <p *ngIf="contents">Contents: "{{contents}}"</p>

    <textarea *ngIf="error" class="error">{{error | json}}</textarea>

    <hr>
    <h3>Upload file</h3>
    <form enctype="multipart/form-data" method="post">
      <div>
        <label for="picked">Choose file to upload</label>
        <div>
          <input type="file" id="picked" #picked
            (click)="message=''"
            (change)="onPicked(picked)">
        </div>
      </div>
      <p *ngIf="message">{{message}}</p>
    </form>
    
   |      +---- http-examples.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Config, HttpExamplesService } from './http-examples.service';
    import { UploaderService } from './uploader.service';

    @Component({
      selector: 'app-http-examples',
      templateUrl: './http-examples.component.html',
      styles:[`
        textarea.error {
          width:50%;
          height:20em;
        }
      `],
      providers: [HttpExamplesService, UploaderService]
    })
    export class HttpExamplesComponent implements OnInit {
      error: any;
      headers: string[];
      config: Config;
      result_obj: Object;
      contents: string;
      message: string;

      configUrl = 'assets/config.json';
      textfileUrl = 'assets/textfile.txt';

      objectKeys = Object.keys;
      constructor(
        private httpExamplesService: HttpExamplesService,
        private uploaderService: UploaderService
      ) {}
      ngOnInit() { }

      clear() {
        this.error = undefined;
        this.headers = undefined;
        this.config = undefined;
        this.result_obj = undefined;
        this.contents = undefined;
      }

      showConfig() {
        this.httpExamplesService.getData(this.configUrl)
          .subscribe(
            (data: Config) => {
              this.config = { ...data }; // typed response object container
              this.result_obj = this.config; // assign to local object
            }, // success path
            error => this.error = error // error path
          );
          // .subscribe((data: Config) => this.config = {
          //   apiUrl: data['heroesUrl'],
          //   textfile:  data['textfile']
          // });
      }

      showConfigResponse() {
        this.httpExamplesService.getConfigDataResponse(
          this.configUrl
        ).subscribe(resp => { // resp is of type `HttpResponse<Config>`
            // display its headers
            const keys = resp.headers.keys();
            this.headers = keys.map(key =>
              `${key}: ${resp.headers.get(key)}`);
            // access the body directly, which is typed as `Config`.
            this.config = { ... resp.body }; // typed response object container
            this.result_obj = this.config; // assign here to local object
          });
      }

      onPicked(input: HTMLInputElement) {
        const file = input.files[0];
        if (file) {
          this.uploaderService.upload(file).subscribe(
            msg => {
              input.value = null;
              this.contents = msg;
            }
          );
        }
      }

      showTextfileContent() {
        this.httpExamplesService.getData(
          this.textfileUrl,
          {responseType: 'text'}
        ).subscribe(
          results => this.contents = results.toString(),
          error => this.error = error // error path
        );
        // .subscribe((data: Config) => this.config = {
        //   apiUrl: data['heroesUrl'],
        //   textfile:  data['textfile']
        // });
      }

      makeError() {
        this.httpExamplesService.makeIntentionalError()
          .subscribe(null, error => this.error = error );
      }

    }
    
   |      +---- http-examples.service.ts

    import { Injectable } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { HttpErrorResponse, HttpResponse } from '@angular/common/http';

    import { Observable, throwError } from 'rxjs';
    import { catchError, retry, tap } from 'rxjs/operators';

    import { MessageService } from '../messages/message.service';

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

    @Injectable()
    export class HttpExamplesService {

      constructor(
        private http: HttpClient,
        private messageService: MessageService
      ) { }

      getData(url: string, options_obj = {}) {
        return this.http.get(url, options_obj)
          .pipe(
            retry(3),
            tap(
              data => data,
              error => this.handleError(error)
            )
          );
        // Observable of Config
        // configUrl = 'assets/config.json';
        // return this.http.get<Config>(this.configUrl)
        // .pipe(
        //   retry(3), // retry a failed request up to 3 times
        //   catchError(this.handleError) // then handle the error
        // );
        // return this.http.get(this.configUrl);
      }

      getConfigDataResponse(url: string): Observable<HttpResponse<Config>> {
        return this.http.get<Config>(
          url,
          { observe: 'response' }
        );
      }

      makeIntentionalError() {
        return this.http.get('not/a/real/url')
          .pipe(
            tap(
              data => data,
              error => this.handleError(error)
            )
          );
      }

      private handleError(error: HttpErrorResponse) {
        if (error.error instanceof ErrorEvent) {
          // client-side or network error occurred
          this.log('An error occurred:'+ error.error.message);
        } else {
          // backend returned an unsuccessful response
          this.log(
            `Backend returned code ${error.status}`);
            // , body was: ${error.error}
        }
        // return an observable with a user-facing error message
        return throwError(
          'Something bad happened; please try again later.');
      };

      private log(data: string) {
        this.messageService.add(data);
      }

    }
    
   |      +---- uploader.service.ts

    import { Injectable } from '@angular/core';
    import {
      HttpClient, HttpEvent, HttpEventType, HttpProgressEvent,
      HttpRequest, HttpResponse, HttpErrorResponse
    } from '@angular/common/http';
    import { of } from 'rxjs';
    import { catchError, last, map, tap } from 'rxjs/operators';
    import { MessageService } from '../messages/message.service';

    @Injectable()
    export class UploaderService {
      constructor(
        private http: HttpClient,
        private messenger: MessageService) {}

      // If uploading multiple files, change to:
      // upload(files: FileList) {
      //   const formData = new FormData();
      //   files.forEach(f => formData.append(f.name, f));
      //   new HttpRequest('POST', '/upload/file', formData, {reportProgress: true});
      //   ...
      // }

      upload(file: File) {
        if (!file) { return; }

        // COULD HAVE WRITTEN:
        // return this.http.post('/upload/file', file, {
        //   reportProgress: true,
        //   observe: 'events'
        // }).pipe(
        const req = new HttpRequest('POST', '/upload/file', file, {
          reportProgress: true
        });

        // The `HttpClient.request` API produces a raw event stream
        // which includes start (sent), progress, and response events.
        return this.http.request(req).pipe(
          map(event => this.getEventMessage(event, file)),
          tap(message => this.showProgress(message)),
          last(), // return last (completed) message to caller
          catchError(this.handleError(file))
        );
      }

      /** Return distinct message for sent, upload progress, & response events */
      private getEventMessage(event: HttpEvent<any>, file: File) {
        switch (event.type) {
          case HttpEventType.Sent:
            return `Uploading file "${file.name}" of size ${file.size}.`;

          case HttpEventType.UploadProgress:
            // Compute and show the % done:
            const percentDone = Math.round(100 * event.loaded / event.total);
            return `File "${file.name}" is ${percentDone}% uploaded.`;

          case HttpEventType.Response:
            return `File "${file.name}" was completely uploaded!`;

          default:
            return `File "${file.name}" surprising upload event: ${event.type}.`;
        }
      }

      /**
      * Returns a function that handles Http upload failures.
      * @param file - File object for file being uploaded
      *
      * When no `UploadInterceptor` and no server,
      * you'll end up here in the error handler.
      */
      private handleError(file: File) {
        const userMessage = `${file.name} upload failed.`;

        return (error: HttpErrorResponse) => {
          // TODO: send the error to remote logging infrastructure
          console.error(error); // log to console instead

          const message = (error.error instanceof Error) ?
            error.error.message :
          `server returned code ${error.status} with body "${error.error}"`;

          this.messenger.add(`${userMessage} ${message}`);

          // Let app keep running but indicate failure.
          return of(userMessage);
        };
      }

      private showProgress(message: string) {
        this.messenger.add(message);
      }
    }
    
   +---- http-interceptors
   |      +---- auth-interceptor.ts

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

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

    import { notesUrl } from '../notes/note.service';
    // searchUrl = 'https://npmsearch.com/query';

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

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

    import { HTTP_INTERCEPTORS } from '@angular/common/http';
    import { AuthInterceptor } from './auth-interceptor';
    import { CachingInterceptor } from './caching-interceptor';
    import { EnsureHttpsInterceptor } from './ensure-https-interceptor';
    import { LoggingInterceptor } from './logging-interceptor';
    import { TrimNameInterceptor } from './trim-name-interceptor';
    import { UploadInterceptor } from './upload-interceptor';
    //Http interceptor providers in outside-in order
    export const httpInterceptorProviders = [
      { provide: HTTP_INTERCEPTORS, useClass: EnsureHttpsInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: TrimNameInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: UploadInterceptor, multi: true },
      { provide: HTTP_INTERCEPTORS, useClass: CachingInterceptor, multi: true }
    ];
   |      +---- logging-interceptor.ts

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

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

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

    import { InMemoryDbService } from 'angular-in-memory-web-api';
    import { Injectable } from '@angular/core';

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

    import { Help } from './help-center/help';
    import { HELPS } from './help-center/mock-helps';

    @Injectable({
      providedIn: 'root'
    })
    export class InMemoryDataService {
      // constructor() { }
      createDb() {
        const notes = NOTES;
        return {notes};
      }
      // Overrides the genId method to ensure that a note always has an id
      // heroes array is empty = method returns the initial number (11)
      // heroes array is not empty = method below returns the highest
      // hero id + 1
      genId(notes: Note[]): number {
        return notes.length > 0 ? Math.max(...notes.map(note => note.id)) + 1 : 11;
      }
    }
    
   +---- messages
   |      +---- message.service.ts

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

    @Injectable({
      providedIn: 'root'
    })
    export class MessageService {
      messages: string[] = [];
      // constructor() { }
      add(message: string) {
        this.messages.push(message);
      }
      clear() {
        this.messages = [];
      }
    }
    
   |      +---- messages.component.css

    /* MessagesComponent's private CSS styles */
    h2 {
      color: red;
      font-family: Arial, Helvetica, sans-serif;
      font-weight: lighter;
    }
    body {
      margin: 2em;
    }
    body, input[text], button {
      color: crimson;
      font-family: Cambria, Georgia;
    }

    button.clear {
      font-family: Arial;
      background-color: #eee;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      cursor: hand;
    }
    button:hover {
      background-color: #cfd8dc;
    }
    button:disabled {
      background-color: #eee;
      color: #aaa;
      cursor: auto;
    }
    button.clear {
      color: #888;
      margin-bottom: 12px;
    }
   |      +---- messages.component.html

    <div *ngIf="messageService.messages.length">
      <h2>Messages</h2>
      <button class="clear"
      (click)="messageService.clear()">clear</button>
      <div *ngFor='let message of messageService.messages'>
        {{message}}
      </div>
    </div>
   |      +---- messages.component.ts

    import { Component, OnInit } from '@angular/core';
    import { MessageService } from './message.service';

    @Component({
      selector: 'app-messages',
      templateUrl: './messages.component.html',
      styleUrls: ['./messages.component.css']
    })
    export class MessagesComponent implements OnInit {

      constructor(public messageService: MessageService) { }

      ngOnInit() {
      }

    }
    
   +---- notes
   |      +---- filter-note.pipe.ts

    import { Pipe, PipeTransform } from '@angular/core';
    import { Note } from './note';

    @Pipe({ name: 'filternote' })
    export class FilterNotePipe implements PipeTransform {
      transform(allNotes: Note[]) {
        return allNotes.filter(hero => hero.id); // here, avoid id 0
      }
    }
   |      +---- mock-notes.ts

    import { Note } from './note';

    export const NOTES: Note[] = [ // array of Notes-class
        { id: 11, title: 'Mr. Nice', content: 'who is Mr. Nice' },
        { id: 12, title: 'Narco', content: 'why he is Narco' },
        { id: 13, title: 'Bombasto', content: 'where lives Bombasto' },
        { id: 14, title: 'Celeritas', content: 'is Celeritas a girl' },
        { id: 15, title: 'Magneta', content: 'find Magneta' },
        { id: 16, title: 'RubberMan', content: 'use RubberMan' },
        { id: 17, title: 'Dynama', content: 'Dynama is on the way' },
        { id: 18, title: 'Dr IQ', content: 'find out IQ of Dr IQ' },
        { id: 19, title: 'Magma', content: 'no sence to call Magma' },
        { id: 20, title: 'Tornado', content: 'Tornado looks scarry' }
      ];
    
   |      +---- note-details
   |            +---- note-details.component.css

    label {
      display: inline-block;
      width: 3em;
      margin: .5em 0;
      color: #607D8B;
      font-weight: bold;
    }
    input {
      height: 2em;
      font-size: 1em;
      padding-left: .4em;
    }
    button {
      margin-top: 20px;
      font-family: Arial;
      background-color: #eee;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      cursor: hand;
    }
    button:hover {
      background-color: #cfd8dc;
    }
    button:disabled {
      background-color: #eee;
      color: #ccc;
      cursor: auto;
    }
   |            +---- note-details.component.html

    <div *ngIf="note">
        <h2>{{ note.title | uppercase }} [id: {{ note.id }}]</h2>
        <h3>{{ note.content }}</h3>
        <div>
            <label>title:
            <input [(ngModel)]="note.title" placeholder="title"/>
            </label>
        </div>
        <button (click)="save()">save()</button><br>
        <button (click)="goBack()">goBack()</button>
        <button (click)="gotoNotes(note)">gotoNotes(note)</button>
    </div>
   |            +---- note-details.component.ts

    import { Component, OnInit /*,Input*/ } from '@angular/core';
    import { Location } from '@angular/common';
    import { Router, ActivatedRoute /*,ParamMap*/ } from '@angular/router';
    // import { Observable } from 'rxjs';
    // import { switchMap } from 'rxjs/operators';

    import { NoteService }  from '../note.service';
    import { Note } from '../note';

    @Component({
      selector: 'app-note-details',
      templateUrl: './note-details.component.html',
      styleUrls: ['./note-details.component.css']
    })
    export class NoteDetailsComponent implements OnInit {
      // @Input() note: Note;
      note: Note;
      // note$: Observable<Note>;
      constructor(
        private route: ActivatedRoute,
        private router: Router,
        private noteService: NoteService,
        private location: Location
      ) { }
      ngOnInit(): void {
        this.getNote();
        // // paramMap - detect when the route parameters change from within the same instance
        // this.note$ = this.route.paramMap.pipe(
        //  // cancel previous in-flight requests
        //   switchMap((params: ParamMap) =>
        //     this.noteService.getNote(params.get('id'))
        // ));
      }
      getNote(): void {
        const id = +this.route.snapshot.paramMap.get('id');
        this.noteService.getNote(id)
          .subscribe(note => this.note = note);
      }
      save(): void {
        this.noteService.updateNote(this.note)
          .subscribe(() => this.goBack());
      }

      gotoNotes(note: Note) {
        let noteId = note ? note.id : null;
        // pass note id if available
        // include a junk 'foo' property
        this.router.navigate(['/currentnotes', { id: noteId, foo: 'foo' }]);
      }
      goBack(): void {
        this.location.back();
      }
    }
    
   |      +---- note-list
   |            +---- note-list.component.html

    <h2>Notes</h2>

    <div>
      <label>Note title:
        <input #noteTitle (keyup.enter)="add(noteTitle.value); noteTitle.value=''" />
      </label>
      <!-- (click) passes input value to add() and then clears the input -->
      <button (click)="add(noteTitle.value); noteTitle.value=''">
        add
      </button>
    </div>

    <ul class="notes">
      <li
        *ngFor="let note of notes"
        [@flyInOut]="'in'"
        [class.selected]="note.id === selectedId">
        <!-- [class.selected]="note === selectedNote" -->
        <!-- (click)="onSelect(note)" -->
        <a routerLink="/note/{{note.id}}">
        <!-- <a [routerLink]="['/note', note.id]"> -->
          <span class="id">{{note.id}}</span> {{note.title}}
        </a>
        <button class="delete" title="delete note"
          (click)="delete(note)">x</button>
      </li>
    </ul>
    <app-note-search></app-note-search>

    <!-- <app-note-details [note]="selectedNote"></app-note-details> -->
   |            +---- note-list.component.ts

    import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import {
      state,
      style,
      trigger,
      animate,
      transition,
      group,
      query,
      stagger,
      AnimationEvent
    } from '@angular/animations';

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

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

        // trigger('flyInOut', [
        //   state('in', style({ transform: 'translateX(0)' })),
        //   transition('void => *', [
        //     style({ transform: 'translateX(-100%)' }),
        //     animate(100)
        //   ]),
        //   transition('* => void', [
        //     animate(100, style({ transform: 'translateX(100%)' }))
        //   ])
        // ]),

        trigger('flyInOut', [
          state('in', style({
            width: 120,
            transform: 'translateX(0)', opacity: 1
          })),
          transition('void => *', [
            style({
              width: 10,
              transform: 'translateX(50px)',
              opacity: 0
            }),
            group([
              animate('0.3s 0.1s ease', style({
                transform: 'translateX(0)',
                width: 120
              })),
              animate('0.3s ease', style({
                opacity: 1
              }))
            ])
          ]),
          transition('* => void', [
            group([
              animate('0.3s ease', style({
                transform: 'translateX(50px)',
                width: 10
              })),
              animate('0.3s 0.2s ease', style({
                opacity: 0
              }))
            ])
          ])
        ]),

      ]
    })
    export class NoteListComponent implements OnInit {
      // note: Note = {
      //   id: 1,
      //   title: 'Note 1 title',
      //   content: 'this is content sample'
      // };
      // notes = NOTES;
      notes: Note[];
      selectedId: number;
      // selectedNote: Note;
      // notes$: Observable<Hero[]>;
      constructor(
        private noteService: NoteService,
        private route: ActivatedRoute
      ) {}
      ngOnInit() {
        // (+) before `params.get()` turns the string into a number
        this.selectedId = +this.route.snapshot.paramMap.get('id');
        this.getNotes();
        // this.notes$ = this.route.paramMap.pipe(
        //   switchMap(params => {
        //     // (+) before `params.get()` turns the string into a number
        //     this.selectedId = +params.get('id');
        //     return this.noteService.getNotes();
        //   })
        // );
      }
      // onSelect(note: Note): void {
      //   this.selectedNote = note;
      // }
      getNotes(): void {
        this.noteService.getNotes()
          .subscribe(notes => this.notes = notes);
      }
      add(title: string): void {
        title = title.trim();
        if (!title) { return; }
        this.noteService.addNote({ title } as Note)
          .subscribe(note => {
            this.notes.push(note);
          });
      }
      delete(note: Note): void {
        this.notes = this.notes.filter(h => h !== note);
        this.noteService.deleteNote(note).subscribe();
      }
    }
    
   |      +---- note-search
   |            +---- note-search.component.css

    .search-result li {
      border-bottom: 1px solid gray;
      border-left: 1px solid gray;
      border-right: 1px solid gray;
      width: 195px;
      height: 16px;
      padding: 5px;
      background-color: white;
      cursor: pointer;
      list-style-type: none;
    }

    .search-result li:hover {
      background-color: #607D8B;
    }

    .search-result li a {
      color: #888;
      display: block;
      text-decoration: none;
    }

    .search-result li a:hover {
      color: white;
    }
    .search-result li a:active {
      color: white;
    }
    #search-box {
      width: 200px;
      height: 20px;
    }

    ul.search-result {
      margin-top: 0;
      padding-left: 0;
    }
   |            +---- note-search.component.html

    <div id="search-component">
      <h4>Note Search</h4>
      <input #searchBox id="search-box" (input)="search(searchBox.value)" />
      <input type="checkbox" id="refresh" [checked]="withRefresh" (click)="toggleRefresh()">
      <label for="refresh">with refresh</label>
      <ul class="search-result">
        <li *ngFor="let note of notes$ | async" >
          <a routerLink="/note/{{note.id}}">
            {{note.title}}
          </a>
        </li>
      </ul>
    </div>
   |            +---- note-search.component.ts

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

    @Component({
      selector: 'app-note-search',
      templateUrl: './note-search.component.html',
      styleUrls: ['./note-search.component.css']
    })
    export class NoteSearchComponent implements OnInit {
      withRefresh = false;
      // packages$: Observable<NpmPackageInfo[]>;
      notes$: Observable<Note[]>; // $ - as an Observable
      private searchTerms$ = new Subject<string>();
      constructor(private noteService: NoteService) { }
      ngOnInit(): void {
        this.notes$ = this.searchTerms$.pipe(
          // wait 300ms after each keystroke before considering the term
          debounceTime(300),
          // ignore new term if same as previous term
          distinctUntilChanged(),
          // switch to new search observable each time the term changes
          switchMap(
            (term: string) => this.noteService.searchNotes(term, this.withRefresh)
          ),
        );
      }
      // Push a search term into the observable stream.
      search(term: string): void {
        this.searchTerms$.next(term);
      }
      toggleRefresh() { this.withRefresh = ! this.withRefresh; }
    }
    
   |      +---- note.service.ts

    import { Injectable } from '@angular/core';
    import { Observable, of } from 'rxjs';

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

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

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

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

    export const notesUrl = 'api/notes';

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

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

    export class Note { // class for usage in components/services
        id: number;
        title: string;
        content: string;
        // constructor(
        //     public id: number,
        //     public title: string
        //     public content: string;
        // ) { }
    }
    
   |      +---- notes-routing.module.ts

    import { NgModule } from '@angular/core';
    import { Routes, RouterModule } from '@angular/router';

    import { NoteListComponent } from './note-list/note-list.component';
    import { NoteDetailsComponent  } from './note-details/note-details.component';
    // import { NoteSearchComponent  } from './note-search/note-search.component';

    const routes: Routes = [
      {
        path: 'notes',
        redirectTo: '/currentnotes'
      },{
        path: 'note/:id',
        redirectTo: '/currentnote/:id'
      },{
        path: 'currentnotes',
        component: NoteListComponent,
        data: { animation: 'notes' }
      },{
        path: 'currentnote/:id',
        component: NoteDetailsComponent,
        data: { animation: 'note' }
      }
    ];

    @NgModule({
      imports: [RouterModule.forChild(routes)],
      exports: [RouterModule]
    })
    export class NotesRoutingModule { }
    
   |      +---- notes.module.ts

    import { NgModule } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { FormsModule } from '@angular/forms'; // <-- NgModel lives here

    import { NoteListComponent } from './note-list/note-list.component';
    import { NoteDetailsComponent  } from './note-details/note-details.component';
    import { NoteSearchComponent  } from './note-search/note-search.component';

    import { NotesRoutingModule } from './notes-routing.module';

    @NgModule({
      declarations: [
        NoteListComponent,
        NoteDetailsComponent,
        NoteSearchComponent
      ],
      imports: [
        CommonModule,
        FormsModule,
        NotesRoutingModule
      ]
    })
    export class NotesModule { }
    
   +---- page-not-found.component.ts

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

    @Component({
      selector: 'app-page-not-found',
      template: `
        <h2>Page not found</h2>
      `
    })
    export class PageNotFoundComponent implements OnInit {

      constructor() { }

      ngOnInit() {
      }

    }
    
   +---- popup
   |      +---- popup.component.ts

    import { Component, EventEmitter, Input, Output } from '@angular/core';
    import { animate, state, style, transition, trigger } from '@angular/animations';

    @Component({
      selector: 'my-popup',
      template: `
        <span>Popup: {{message}}</span>
        <button (click)="closed.next()">&#x2716;</button>
      `,
      host: {
        '[@state]': 'state',
      },
      animations: [
        trigger('state', [
          state('opened', style({transform: 'translateY(0%)'})),
          state('void, closed', style({transform: 'translateY(100%)', opacity: 0})),
          transition('* => *', animate('500ms ease-in')),
        ])
      ],
      styles: [`
        :host {
          position: absolute;
          bottom: 0;
          left: 0;
          right: 0;
          background: #009cff;
          height: 48px;
          padding: 16px;
          display: flex;
          justify-content: space-between;
          align-items: center;
          border-top: 1px solid black;
          font-size: 24px;
        }
        button {
          border-radius: 50%;
        }
      `]
    })
    export class PopupComponent {
      private state: 'opened' | 'closed' = 'closed';
      @Input()
      set message(message: string) {
        this._message = message;
        this.state = 'opened';
      }
      get message(): string { return this._message; }
      _message: string;
      @Output()
      closed = new EventEmitter();
    }
   |      +---- popup.service.ts

    import { ApplicationRef, ComponentFactoryResolver, Injectable, Injector } from '@angular/core';
    import { NgElement, WithProperties } from '@angular/elements';
    import { PopupComponent } from './popup.component';
    @Injectable()
    export class PopupService {
      constructor(private injector: Injector,
                  private applicationRef: ApplicationRef,
                  private componentFactoryResolver: ComponentFactoryResolver) {}
      // Previous dynamic-loading method required you to set up infrastructure
      // before adding the popup to the DOM.
      showAsComponent(message: string) {
        // Create element
        const popup = document.createElement('popup-component');
        // Create the component and wire it up with the element
        const factory = this.componentFactoryResolver.resolveComponentFactory(PopupComponent);
        const popupComponentRef = factory.create(this.injector, [], popup);
        // Attach to the view so that the change detector knows to run
        this.applicationRef.attachView(popupComponentRef.hostView);
        // Listen to the close event
        popupComponentRef.instance.closed.subscribe(() => {
          document.body.removeChild(popup);
          this.applicationRef.detachView(popupComponentRef.hostView);
        });
        // Set the message
        popupComponentRef.instance.message = message;
        // Add to the DOM
        document.body.appendChild(popup);
      }
      // This uses the new custom-element method to add the popup to the DOM.
      showAsElement(message: string) {
        // Create element
        const popupEl: NgElement & WithProperties<PopupComponent> = document.createElement('popup-element') as any;
        // Listen to the close event
        popupEl.addEventListener('closed', () => document.body.removeChild(popupEl));
        // Set the message
        popupEl.message = message;
        // Add to the DOM
        document.body.appendChild(popupEl);
      }
    }
   +---- request-cache.service.ts

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

    import { Injectable } from '@angular/core';
    import { PreloadingStrategy, Route } from '@angular/router';
    import { Observable, of } from 'rxjs';

    @Injectable({
      providedIn: 'root',
    })
    export class SelectivePreloadingStrategyService implements PreloadingStrategy {
      preloadedModules: string[] = [];
      preload(
        route: Route, // route to consider
        // loader function that can load the routed module asynchronously
        load: () => Observable<any>
      ): Observable<any> {
        if (route.data && route.data['preload']) { // check flag from routes
          // add the route path to the preloaded module array
          this.preloadedModules.push(route.path);
          // console.log('Preloaded: ' + route.path);
          return load();
        } else {
          return of(null); // route should not preload, Observable of null
        }
      }
    }
   +---- testing
   |      +---- http-client.spec.ts

    // 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();
      // });
    });
   +---- translations
   |      +---- translations.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>
      
   |      +---- translations.component.ts

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

    @Component({
      selector: 'app-translations',
      templateUrl: './translations.component.html'
    })
    export class TranslationsComponent implements OnInit {
      constructor() { }
      ngOnInit() { }

      minutes = 0;
      gender = 'female';
      fly = true;
      logo = 'https://angular.io/assets/images/logos/angular/angular.png';
      heroes: string[] = ['Magneta', 'Celeritas', 'Dynama'];
      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'; }

    }
    
   +---- unless.directive.ts

    import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

    /**
    * Add the template content to the DOM unless the condition is true.
    *
    * If the expression assigned to `appUnless` evaluates to a truthy value
    * then the templated elements are removed removed from the DOM,
    * the templated elements are (re)inserted into the DOM.
    *
    * <div *ngUnless="errorCount" class="success">
    *   Congrats! Everything is great!
    * </div>
    *
    * ### Syntax
    *
    * - `<div *appUnless="condition">...</div>`
    * - `<ng-template [appUnless]="condition"><div>...</div></ng-template>`
    *
    */
    @Directive({ selector: '[appUnless]'})
    export class UnlessDirective {
      private hasView = false;

      constructor(
        private templateRef: TemplateRef<any>,
        private viewContainer: ViewContainerRef) { }

      @Input() set appUnless(condition: boolean) {
        if (!condition && !this.hasView) {
          this.viewContainer.createEmbeddedView(this.templateRef);
          this.hasView = true;
        } else if (condition && this.hasView) {
          this.viewContainer.clear();
          this.hasView = false;
        }
      }
    }
   assets
   +---- config.json

    {
      "apiUrl": "https://reqres.in/api",
      "textfile": "tests-files.txt"
    }
    
   +---- icons
   +---- textfile.txt

    file content for example:
    Requesting non-JSON data
    []{};':",.<>/?=-+_
   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/dist/zone-error';  // Included with Angular CLI.
    
   index.html

    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="utf-8">
      <title>AngularTour</title>
      <base href="/">

      <meta
        name="viewport"
        content="width=device-width, initial-scale=1"
      >
      <link rel="icon" type="image/x-icon" href="favicon.ico">
      <link rel="manifest" href="manifest.webmanifest">
      <meta name="theme-color" content="#1976d2">
    </head>
    <body>
      <app-root></app-root>
      <noscript>Please enable JavaScript to continue using this application.</noscript>
    </body>
    </html>
    
   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: '',
        // set as a testing framework
        frameworks: ['jasmine', '@angular-devkit/build-angular'],
        plugins: [
          require('karma-jasmine'),
          require('karma-chrome-launcher'),
          require('karma-jasmine-html-reporter'),
          require('karma-coverage-istanbul-reporter'),
          require('@angular-devkit/build-angular/plugins/karma')
        ],
        client: {
          clearContext: false // leave Jasmine Spec Runner output visible in browser
        },
        coverageIstanbulReporter: {
          dir: require('path').join(__dirname, '../coverage'),
          reports: ['html', 'lcovonly', 'text-summary'],
          fixWebpackSourcePaths: true
          // ,thresholds: {
          //   statements: 80,
          //   lines: 80,
          //   branches: 80,
          //   functions: 80
          // }
        },
        reporters: ['progress', 'kjhtml'],
        port: 9876,
        colors: true,
        logLevel: config.LOG_INFO,
        autoWatch: true, // tests run in watch mode
        browsers: ['Chrome'], // browser where the test should run
        singleRun: false
      });
    };
   locale
   +---- messages.ru.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>Привет i18n!</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">1</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="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
            <source>I don&apos;t output any element</source>
            <target>Я не вывожу элементов</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">5</context>
            </context-group>
          </trans-unit>

          <trans-unit id="701174153757adf13e7c24a248c8a873ac9f5193" datatype="html">
            <source>Angular logo</source>
            <target>Логотип</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">9</context>
            </context-group>
          </trans-unit>

          <trans-unit id="d69f6b42305f49332026fef24b40227f02e34594" datatype="html">
            <source>Updated <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
            <target>Обновленно <x id="ICU" equiv-text="{minutes, plural, =0 {сейчас} =1 {1 минуту назад} other {много минут назад}}"/></target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">12</context>
            </context-group>
          </trans-unit>
          <trans-unit id="5a134dee893586d02bffc9611056b9cadf9abfad" 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 {сейчас} =1 {1 минуту назад} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> минут назад} }</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">12</context>
            </context-group>
          </trans-unit>

          <trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" datatype="html">
            <source>The author is <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></source>
            <target>Автор <x id="ICU" equiv-text="{gender, select, male {...} female {...} other {...}}"/></target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">16</context>
            </context-group>
          </trans-unit>
          <trans-unit id="eff74b75ab7364b6fa888f1cbfae901aaaf02295" datatype="html">
            <source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
            <target>{VAR_SELECT, select, male {мужик} female {баба} other {ой,пиздец...} }</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">16</context>
            </context-group>
          </trans-unit>

          <trans-unit id="972cb0cf3e442f7b1c00d7dab168ac08d6bdf20c" datatype="html">
            <source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/>
            </source>
            <target>Обновленно: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">18</context>
            </context-group>
          </trans-unit>
          <trans-unit id="7151c2e67748b726f0864fc443861d45df21d706" 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 {сичас} =1 {уже} other {<x id="INTERPOLATION" equiv-text="{{minutes}}"/> недавно {VAR_SELECT, select, male {мужиком} female {бабой какой то} other {ХЗ кто} }} }</target>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">18</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/translations/translations.component.html</context>
              <context context-type="linenumber">1</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="ba0cc104d3d69bf669f97b8d96a4c5d8d9559aa3" datatype="html">
            <source>I don&apos;t output any element</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">5</context>
            </context-group>
          </trans-unit>
          <trans-unit id="701174153757adf13e7c24a248c8a873ac9f5193" datatype="html">
            <source>Angular logo</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">9</context>
            </context-group>
          </trans-unit>
          <trans-unit id="d69f6b42305f49332026fef24b40227f02e34594" datatype="html">
            <source>Updated <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/></source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">12</context>
            </context-group>
          </trans-unit>
          <trans-unit id="5a134dee893586d02bffc9611056b9cadf9abfad" 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/translations/translations.component.html</context>
              <context context-type="linenumber">12</context>
            </context-group>
          </trans-unit>
          <trans-unit id="f99f34ac9bd4606345071bd813858dec29f3b7d1" 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/translations/translations.component.html</context>
              <context context-type="linenumber">16</context>
            </context-group>
          </trans-unit>
          <trans-unit id="eff74b75ab7364b6fa888f1cbfae901aaaf02295" datatype="html">
            <source>{VAR_SELECT, select, male {male} female {female} other {other} }</source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">16</context>
            </context-group>
          </trans-unit>
          <trans-unit id="972cb0cf3e442f7b1c00d7dab168ac08d6bdf20c" datatype="html">
            <source>Updated: <x id="ICU" equiv-text="{minutes, plural, =0 {...} =1 {...} other {...}}"/>
    </source>
            <context-group purpose="location">
              <context context-type="sourcefile">app/translations/translations.component.html</context>
              <context context-type="linenumber">18</context>
            </context-group>
          </trans-unit>
          <trans-unit id="7151c2e67748b726f0864fc443861d45df21d706" 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/translations/translations.component.html</context>
              <context context-type="linenumber">18</context>
            </context-group>
          </trans-unit>
        </body>
      </file>
    </xliff>
    
   main.server.ts

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

    import { environment } from './environments/environment';

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

    export { AppServerModule } from './app/app.server.module';
    
   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();
    }

    document.addEventListener('DOMContentLoaded', () => {
      platformBrowserDynamic().bootstrapModule(AppModule)
      .catch(err => console.error(err));
    });
    
   polyfills.ts

    /**
    * This file includes polyfills needed by Angular and is loaded before the app.
    * You can add your own extra polyfills to this file.
    *
    * This file is divided into 2 sections:
    *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
    *   2. Application imports. Files imported after ZoneJS that should be loaded before your main
    *      file.
    *
    * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
    * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
    * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
    *
    * Learn more in https://angular.io/guide/browser-support
    */

    /***************************************************************************************************
    * BROWSER POLYFILLS
    */

    /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills.
    *  This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot
    */

    // import 'core-js/es6/symbol';
    // import 'core-js/es6/object';
    // import 'core-js/es6/function';
    // import 'core-js/es6/parse-int';
    // import 'core-js/es6/parse-float';
    // import 'core-js/es6/number';
    // import 'core-js/es6/math';
    // import 'core-js/es6/string';
    // import 'core-js/es6/date';
    // import 'core-js/es6/array';
    // import 'core-js/es6/regexp';
    // import 'core-js/es6/map';
    // import 'core-js/es6/weak-map';
    // import 'core-js/es6/set';

    /** IE10 and IE11 requires the following for NgClass support on SVG elements */
    // import 'classlist.js';  // Run `npm install --save classlist.js`.

    /** IE10 and IE11 requires the following for the Reflect API. */
    // import 'core-js/es6/reflect';

    /**
    * Web Animations `@angular/platform-browser/animations`
    * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
    * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
    */
    import 'web-animations-js';  // Run `npm install --save web-animations-js`.

    /**
    * By default, zone.js will patch all possible macroTask and DomEvents
    * user can disable parts of macroTask/DomEvents patch by setting following flags
    * because those flags need to be set before `zone.js` being loaded, and webpack
    * will put import in the top of bundle, so user need to create a separate file
    * in this directory (for example: zone-flags.ts), and put the following flags
    * into that file, and then add the following code before importing zone.js.
    * import './zone-flags.ts';
    *
    * The flags allowed in zone-flags.ts are listed here.
    *
    * The following flags will work for all browsers.
    *
    * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
    * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
    * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
    *
    *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
    *  with the following flag, it will bypass `zone.js` patch for IE/Edge
    *
    *  (window as any).__Zone_enable_cross_context_check = true;
    *
    */

    /***************************************************************************************************
    * Zone JS is required by default for Angular itself.
    */
    import 'zone.js/dist/zone';  // Included with Angular CLI.


    /***************************************************************************************************
    * APPLICATION IMPORTS
    */
    
   styles.css

    /* Master Styles */
    h1 {
    color: #369;
    font-family: Arial, Helvetica, sans-serif;
    font-size: 250%;
    }
    h2, h3 {
      color: #444;
      font-family: Arial, Helvetica, sans-serif;
      font-weight: lighter;
    }
    body {
      margin: 2em;
    }
    body, input[text], button {
      font-family: Cambria, Georgia;
    }
    a {
      cursor: pointer;
      cursor: hand;
    }

    /* Navigation link styles */
    nav a {
      padding: 5px 10px;
      text-decoration: none;
      margin: 10px 10px 10px 0px;
      display: inline-block;
      background-color: #eee;
      border-radius: 4px;
    }
    nav a:visited, a:link {
      color: #607D8B;
    }
    nav a:hover {
      color: #039be5;
      background-color: #CFD8DC;
    }
    nav a.active {
      color: #039be5;
    }

    /* everywhere else */
    * {
      font-family: Arial, Helvetica, sans-serif;
    }

    .notes {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 15em;
    }
    .notes li {
      position: relative;
      cursor: pointer;
      background-color: #EEE;
      margin: .5em;
      padding: .3em 0;
      height: 1.6em;
      border-radius: 4px;
      overflow:hidden;
    }
    .notes li:hover {
      color: #607D8B;
      background-color: #DDD;
    }
    .notes li.selected {
      background-color: #BBD8DC;
    }
    .notes a {
      color: #888;
      text-decoration: none;
      position: relative;
      display: block;
      width: 250px;
    }
    .notes a:hover {
    color:#607D8B;
    }
    .notes .id {
      display: inline-block;
      font-size: small;
      color: white;
      padding: 0.8em 0.7em 0 0.7em;
      background-color: #607D8B;
      line-height: 1em;
      position: relative;
      left: -1px;
      top: -4px;
      height: 1.8em;
      min-width: 16px;
      text-align: right;
      margin-right: .8em;
      border-radius: 4px 0 0 4px;
    }

    .notes li > .inner {
      cursor: pointer;
      background-color: #EEE;
      height: 1.6em;
      border-radius: 4px;
      width: 19em;
    }
    .notes li:hover > .inner {
      color: #607D8B;
      background-color: #DDD;
      transform: translateX(.1em);
    }

    button {
      background-color: #eee;
      border: none;
      padding: 5px 10px;
      border-radius: 4px;
      cursor: pointer;
      cursor: hand;
      font-family: Arial;
    }
    button:hover {
      background-color: #cfd8dc;
    }
    button.delete {
      position: relative;
      left: 194px;
      top: -32px;
      background-color: gray !important;
      color: white;
    }
    button:disabled {
      background-color: #eee;
      color: #aaa;
      cursor: auto;
    }

    .w_100pc {
      position: relative;
      float: left;
      width: 100%;
    }
    .w_32pc{
      position: relative;
      float: left;
      width: 32%;
    }
    .w_24pc{
      position: relative;
      float: left;
      width: 24%;
    }
    
   test.ts

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

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

    declare const require: any;

    // 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

    {
      "extends": "../tsconfig.json",
      "compilerOptions": {
        "outDir": "../out-tsc/app",
        "types": []
      },
      "exclude": [
        "test.ts",
        "**/*.spec.ts"
      ]
    }
    
   tsconfig.server.json

    {
      "extends": "./tsconfig.app.json",
      "compilerOptions": {
        "outDir": "../out-tsc/app-server",
        "baseUrl": "."
      },
      "angularCompilerOptions": {
        "entryModule": "app/app.server.module#AppServerModule"
      }
    }
    
   tsconfig.spec.json

    {
      "extends": "../tsconfig.json",
      "compilerOptions": {
        "outDir": "../out-tsc/spec",
        "types": [
          "jasmine",
          "node"
        ]
      },
      "files": [
        "test.ts",
        "polyfills.ts"
      ],
      "include": [
        "**/*.spec.ts",
        "**/*.d.ts"
      ]
    }
    
   tslint.json

    {
      "extends": "../tslint.json",
      "rules": {
        "directive-selector": [
          true,
          "attribute",
          "app",
          "camelCase"
        ],
        "component-selector": [
          true,
          "element",
          "app",
          "kebab-case"
        ]
      }
    }
    

Back to Main Page