[ 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()">✖</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()"
>♂</button>
<button
(click)="female()"
>♀</button>
<button
(click)="other()"
>⚧</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'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'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