docs(aio): update universal for CLI

PR Close #20039
This commit is contained in:
Jesus Rodriguez
2017-10-30 23:39:58 +01:00
committed by Matias Niemelä
parent a363be6b5c
commit 851e1cfcfd
62 changed files with 1062 additions and 1149 deletions

View File

@ -74,8 +74,7 @@ aot-compiler/**/*.factory.d.ts
!styleguide/src/systemjs.custom.js
# universal
!universal/webpack.config.client.js
!universal/webpack.config.universal.js
!universal/webpack.server.config.js
# plunkers
*plnkr.no-link.html

View File

@ -1,3 +1,3 @@
{
"projectType": "systemjs"
"projectType": "universal"
}

View File

@ -0,0 +1,61 @@
// These are important and needed before anything else
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import * as express from 'express';
import { join } from 'path';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Express server
const app = express();
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist');
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main.bundle');
// Express Engine
import { ngExpressEngine } from '@nguniversal/express-engine';
// Import module map for lazy loading
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader';
// #docregion ngExpressEngine
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
// #enddocregion ngExpressEngine
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// #docregion data-request
// TODO: implement data requests securely
app.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});
// #enddocregion data-request
// #docregion static
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser')));
// #enddocregion static
// #docregion navigation-request
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render(join(DIST_FOLDER, 'browser', 'index.html'), { req });
});
// #enddocregion navigation-request
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node server listening on http://localhost:${PORT}`);
});

View File

@ -1,16 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
{ path: 'heroes', component: HeroesComponent },
{ path: '**', redirectTo: '/dashboard' }
{ path: 'heroes', component: HeroesComponent }
];
@NgModule({

View File

@ -1,3 +1,4 @@
/* AppComponent's private CSS styles */
h1 {
font-size: 1.2em;
color: #999;

View File

@ -0,0 +1,7 @@
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

View File

@ -1,15 +1,8 @@
import { Component } from '@angular/core';
import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `
<h1>{{title}}</h1>
<nav>
<a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>
`,
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {

View File

@ -1,62 +1,60 @@
// #docplaster
// #docregion simple
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
// Imports for loading & configuring the in-memory web api
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
import { InMemoryDataService } from './in-memory-data.service';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { HeroesComponent } from './heroes/heroes.component';
import { HeroSearchComponent } from './hero-search/hero-search.component';
import { HeroService } from './hero.service';
import { HeroSearchComponent } from './hero-search.component';
import { MessageService } from './message.service';
import { MessagesComponent } from './messages/messages.component';
// #enddocregion simple
// #docregion platform-detection
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
// #enddocregion platform-detection
// #docregion simple
@NgModule({
imports: [
// #docregion browsermodule
BrowserModule.withServerTransition({ appId: 'uni' }),
BrowserModule.withServerTransition({ appId: 'tour-of-heroes' }),
// #enddocregion browsermodule
FormsModule,
AppRoutingModule,
HttpClientModule,
HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService),
AppRoutingModule
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
],
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent,
HeroDetailComponent,
MessagesComponent,
HeroSearchComponent
],
providers: [ HeroService ],
providers: [ HeroService, MessageService ],
bootstrap: [ AppComponent ]
})
export class AppModule {
// #enddocregion simple
// #docregion platform-detection
constructor(
// #docregion platform-detection
constructor(
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string) {
const platform = isPlatformBrowser(platformId) ?
'on the server' : 'in the browser';
console.log(`Running ${platform} with appId=${appId}`);
}
// #enddocregion platform-detection
// #docregion simple
// #enddocregion platform-detection
}
// #enddocregion simple

View File

@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {ServerModule} from '@angular/platform-server';
import {ModuleMapLoaderModule} from '@nguniversal/module-map-ngfactory-loader';
import {AppModule} from './app.module';
import {AppComponent} from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ModuleMapLoaderModule
],
providers: [
// Add universal-only providers here
],
bootstrap: [ AppComponent ],
})
export class AppServerModule {}

View File

@ -1,23 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
import 'rxjs/add/operator/map';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'my-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Observable<Hero[]>;
constructor(private heroService: HeroService) { }
ngOnInit(): void {
this.heroes = this.heroService.getHeroes()
.map(heroes => heroes.slice(1, 5));
}
}

View File

@ -1,3 +1,4 @@
/* DashboardComponent's private CSS styles */
[class*='col-'] {
float: left;
padding-right: 20px;

View File

@ -1,9 +1,11 @@
<h3>Top Heroes</h3>
<div class="grid grid-pad">
<a *ngFor="let hero of heroes | async" [routerLink]="['/detail', hero.id]" class="col-1-4">
<a *ngFor="let hero of heroes" class="col-1-4"
routerLink="/detail/{{hero.id}}">
<div class="module hero">
<h4>{{hero.name}}</h4>
</div>
</a>
</div>
<hero-search></hero-search>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DashboardComponent } from './dashboard.component';
describe('DashboardComponent', () => {
let component: DashboardComponent;
let fixture: ComponentFixture<DashboardComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DashboardComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: [ './dashboard.component.css' ]
})
export class DashboardComponent implements OnInit {
heroes: Hero[] = [];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
}

View File

@ -1,11 +0,0 @@
<div *ngIf="hero">
<h2>{{hero.name}} details!</h2>
<div>
<label>id: </label>{{hero.id}}</div>
<div>
<label>name: </label>
<input [(ngModel)]="hero.name" placeholder="name" />
</div>
<button (click)="goBack()">Back</button>
<button (click)="save()">Save</button>
</div>

View File

@ -1,38 +0,0 @@
import 'rxjs/add/operator/switchMap';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'my-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
hero: Hero;
constructor(
private heroService: HeroService,
private route: ActivatedRoute,
private location: Location
) {}
ngOnInit(): void {
this.route.paramMap
.switchMap((params: ParamMap) => this.heroService.getHero(+params.get('id')))
.subscribe(hero => this.hero = hero);
}
save(): void {
this.heroService
.update(this.hero)
.subscribe(() => this.goBack());
}
goBack(): void {
this.location.back();
}
}

View File

@ -1,3 +1,4 @@
/* HeroDetailComponent's private CSS styles */
label {
display: inline-block;
width: 3em;

View File

@ -0,0 +1,11 @@
<div *ngIf="hero">
<h2>{{ hero.name | uppercase }} Details</h2>
<div><span>id: </span>{{hero.id}}</div>
<div>
<label>name:
<input [(ngModel)]="hero.name" placeholder="name"/>
</label>
</div>
<button (click)="goBack()">go back</button>
<button (click)="save()">save</button>
</div>

View File

@ -0,0 +1,40 @@
import { Component, OnInit, Input } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: [ './hero-detail.component.css' ]
})
export class HeroDetailComponent implements OnInit {
@Input() hero: Hero;
constructor(
private route: ActivatedRoute,
private heroService: HeroService,
private location: Location
) {}
ngOnInit(): void {
this.getHero();
}
getHero(): void {
const id = +this.route.snapshot.paramMap.get('id');
this.heroService.getHero(id)
.subscribe(hero => this.hero = hero);
}
goBack(): void {
this.location.back();
}
save(): void {
this.heroService.updateHero(this.hero)
.subscribe(() => this.goBack());
}
}

View File

@ -1,20 +0,0 @@
.search-result{
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;
}
.search-result:hover {
color: #eee;
background-color: #607D8B;
}
#search-box{
width: 200px;
height: 20px;
}

View File

@ -1,10 +0,0 @@
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<div>
<div *ngFor="let hero of heroes | async"
(click)="gotoDetail(hero)" class="search-result" >
{{hero.name}}
</div>
</div>
</div>

View File

@ -1,55 +0,0 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';
@Component({
selector: 'hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ],
providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(
private heroSearchService: HeroSearchService,
private router: Router) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes = this.searchTerms.asObservable()
.debounceTime(300) // wait 300ms after each keystroke before considering the term
.distinctUntilChanged() // ignore if next search term is same as previous
.switchMap(term => term // switch to new observable each time the term changes
// return the http search observable
? this.heroSearchService.search(term)
// or the observable of empty heroes if there was no search term
: Observable.of<Hero[]>([]))
.catch(error => {
// TODO: add real error handling
console.log(error);
return Observable.of<Hero[]>([]);
});
}
gotoDetail(hero: Hero): void {
let link = ['/detail', hero.id];
this.router.navigate(link);
}
}

View File

@ -1,28 +0,0 @@
import { Inject, Injectable, Optional } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import { Hero } from './hero';
// #docregion class
@Injectable()
export class HeroSearchService {
private searchUrl = 'api/heroes/?name='; // URL to web api
constructor(
private http: HttpClient,
@Optional() @Inject(APP_BASE_HREF) origin: string) {
this.searchUrl = (origin || '') + this.searchUrl;
}
search(term: string): Observable<Hero[]> {
return this.http
.get(this.searchUrl + term)
.map((data: any) => data.data as Hero[]);
}
}
// #enddocregion class

View File

@ -0,0 +1,39 @@
/* HeroSearch private styles */
.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;
}

View File

@ -0,0 +1,13 @@
<div id="search-component">
<h4>Hero Search</h4>
<input #searchBox id="search-box" (keyup)="search(searchBox.value)" />
<ul class="search-result">
<li *ngFor="let hero of heroes | async" >
<a routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</li>
</ul>
</div>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroSearchComponent } from './hero-search.component';
describe('HeroSearchComponent', () => {
let component: HeroSearchComponent;
let fixture: ComponentFixture<HeroSearchComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroSearchComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroSearchComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { of } from 'rxjs/observable/of';
import {
debounceTime, distinctUntilChanged, switchMap
} from 'rxjs/operators';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'hero-search',
templateUrl: './hero-search.component.html',
styleUrls: [ './hero-search.component.css' ]
})
export class HeroSearchComponent implements OnInit {
heroes: Observable<Hero[]>;
private searchTerms = new Subject<string>();
constructor(private heroService: HeroService) {}
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
ngOnInit(): void {
this.heroes = 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.heroService.searchHeroes(term)),
);
}
}

View File

@ -1,15 +1,17 @@
import { Injectable, Inject, Optional } from '@angular/core';
import { APP_BASE_HREF } from '@angular/common';
import { HttpHeaders, HttpClient } from '@angular/common/http';
import { HttpClient, HttpHeaders }from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/map';
import { of } from 'rxjs/observable/of';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
import { MessageService } from './message.service';
const headers = new HttpHeaders({'Content-Type': 'application/json'});
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
@Injectable()
export class HeroService {
@ -19,46 +21,109 @@ export class HeroService {
// #docregion ctor
constructor(
private http: HttpClient,
private messageService: MessageService,
@Optional() @Inject(APP_BASE_HREF) origin: string) {
this.heroesUrl = (origin || '') + this.heroesUrl;
}
this.heroesUrl = `${origin}${this.heroesUrl}`;
}
// #enddocregion ctor
getHeroes(): Observable<Hero[]> {
return this.http.get(this.heroesUrl)
.map((data: any) => data.data as Hero[])
.catch(this.handleError);
/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => this.log(`fetched heroes`)),
catchError(this.handleError('getHeroes', []))
);
}
/** GET hero by id. Return `undefined` when id not found */
getHeroNo404<Data>(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? `fetched` : `did not find`;
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
/** GET hero by id. Will 404 if id not found */
getHero(id: number): Observable<Hero> {
const url = `${this.heroesUrl}/${id}`;
return this.http.get(url)
.map((data: any) => data.data as Hero)
.catch(this.handleError);
return this.http.get<Hero>(url).pipe(
tap(_ => this.log(`fetched hero id=${id}`)),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
delete(id: number): Observable<void> {
/* GET heroes whose name contains search term */
searchHeroes(term: string): Observable<Hero[]> {
if (!term.trim()) {
// if not search term, return empty hero array.
return of([]);
}
return this.http.get<Hero[]>(`api/heroes/?name=${term}`).pipe(
tap(_ => this.log(`found heroes matching "${term}"`)),
catchError(this.handleError<Hero[]>('searchHeroes', []))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero (name: string): Observable<Hero> {
const hero = { name };
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero (hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete(url, { headers })
.catch(this.handleError);
return this.http.delete<Hero>(url, httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
);
}
create(name: string): Observable<Hero> {
return this.http
.post(this.heroesUrl, { name: name }, { headers })
.map((data: any) => data.data)
.catch(this.handleError);
/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
update(hero: Hero): Observable<Hero> {
const url = `${this.heroesUrl}/${hero.id}`;
return this.http
.put(url, hero, { headers })
.catch(this.handleError);
/**
* 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);
}
}
private handleError(error: any): Observable<any> {
console.error('An error occurred', error); // for demo purposes only
throw error;
/** Log a HeroService message with the MessageService */
private log(message: string) {
this.messageService.add('HeroService: ' + message);
}
}

View File

@ -1,29 +0,0 @@
<!-- #docregion -->
<h2>My Heroes</h2>
<!-- #docregion add -->
<div>
<label>Hero name:</label> <input #heroName />
<button (click)="add(heroName.value); heroName.value=''">
Add
</button>
</div>
<!-- #enddocregion add -->
<ul class="heroes">
<!-- #docregion li-element -->
<li *ngFor="let hero of heroes" (click)="onSelect(hero)"
[class.selected]="hero === selectedHero">
<span class="badge">{{hero.id}}</span>
<span>{{hero.name}}</span>
<!-- #docregion delete -->
<button class="delete"
(click)="delete(hero); $event.stopPropagation()">x</button>
<!-- #enddocregion delete -->
</li>
<!-- #enddocregion li-element -->
</ul>
<div *ngIf="selectedHero">
<h2>
{{selectedHero.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>

View File

@ -1,57 +0,0 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'my-heroes',
templateUrl: './heroes.component.html',
styleUrls: [ './heroes.component.css' ]
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
selectedHero: Hero;
constructor(
private heroService: HeroService,
private router: Router) { }
getHeroes(): void {
this.heroService
.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.create(name)
.subscribe(hero => {
this.heroes.push(hero);
this.selectedHero = null;
});
}
delete(hero: Hero): void {
this.heroService
.delete(hero.id)
.subscribe(() => {
this.heroes = this.heroes.filter(h => h !== hero);
if (this.selectedHero === hero) { this.selectedHero = null; }
});
}
ngOnInit(): void {
this.getHeroes();
}
onSelect(hero: Hero): void {
this.selectedHero = hero;
}
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedHero.id]);
}
}

View File

@ -1,8 +1,4 @@
/* #docregion */
.selected {
background-color: #CFD8DC !important;
color: white;
}
/* HeroesComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
@ -10,28 +6,33 @@
width: 15em;
}
.heroes li {
cursor: pointer;
position: relative;
left: 0;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes li.selected:hover {
background-color: #BBD8DC !important;
color: white;
}
.heroes .text {
.heroes a {
color: #888;
text-decoration: none;
position: relative;
top: -3px;
display: block;
width: 250px;
}
.heroes a:hover {
color:#607D8B;
}
.heroes .badge {
display: inline-block;
font-size: small;
@ -43,26 +44,31 @@
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
font-family: Arial;
.button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
/* #docregion additions */
button.delete {
float:right;
margin-top: 2px;
margin-right: .8em;
position: relative;
left: 194px;
top: -32px;
background-color: gray !important;
color:white;
color: white;
}

View File

@ -0,0 +1,21 @@
<h2>My Heroes</h2>
<div>
<label>Hero name:
<input #heroName />
</label>
<!-- (click) passes input value to add() and then clears the input -->
<button (click)="add(heroName.value); heroName.value=''">
add
</button>
</div>
<ul class="heroes">
<li *ngFor="let hero of heroes">
<a routerLink="/detail/{{hero.id}}">
<span class="badge">{{hero.id}}</span> {{hero.name}}
</a>
<button class="delete" title="delete hero"
(click)="delete(hero);$event.stopPropagation()">x</button>
</li>
</ul>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeroesComponent } from './heroes.component';
describe('HeroesComponent', () => {
let component: HeroesComponent;
let fixture: ComponentFixture<HeroesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeroesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeroesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,41 @@
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.addHero(name)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroService.deleteHero(hero)
.subscribe(() => {
this.heroes = this.heroes.filter(h => h !== hero);
});
}
}

View File

@ -1,18 +1,18 @@
// #docregion , init
import { InMemoryDbService } from 'angular-in-memory-web-api';
export class InMemoryDataService implements InMemoryDbService {
createDb() {
let heroes = [
{id: 11, name: 'Mr. Nice'},
{id: 12, name: 'Narco'},
{id: 13, name: 'Bombasto'},
{id: 14, name: 'Celeritas'},
{id: 15, name: 'Magneta'},
{id: 16, name: 'RubberMan'},
{id: 17, name: 'Dynama'},
{id: 18, name: 'Dr IQ'},
{id: 19, name: 'Magma'},
{id: 20, name: 'Tornado'}
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return {heroes};
}

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { MessageService } from './message.service';
describe('MessageService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [MessageService]
});
});
it('should be created', inject([MessageService], (service: MessageService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,14 @@
import { Injectable } from '@angular/core';
@Injectable()
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages.length = 0;
}
}

View File

@ -0,0 +1,35 @@
/* 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;
}

View File

@ -0,0 +1,8 @@
<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>

View File

@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MessagesComponent } from './messages.component';
describe('MessagesComponent', () => {
let component: MessagesComponent;
let fixture: ComponentFixture<MessagesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MessagesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MessagesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
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() {
}
}

View File

@ -0,0 +1,14 @@
import { Hero } from './hero';
export const HEROES: Hero[] = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

View File

@ -1,22 +0,0 @@
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Angular Universal Tour of Heroes</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css">
<script src="shim.min.js"></script>
<script src="zone.min.js"></script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
<!-- #docregion client-app-bundle -->
<script src="client.js"></script>
<!-- #enddocregion client-app-bundle -->
</html>

View File

@ -1,27 +1,14 @@
<!-- #docregion -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Angular Universal Tour of Heroes</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="stylesheet" href="styles.css">
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tour of Heroes</title>
<base href="/">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('main.js')
.catch(function(err){ console.error(err); });
</script>
</head>
<body>
<my-app>Loading...</my-app>
</body>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

View File

@ -0,0 +1 @@
export { AppServerModule } from './app/app.server.module';

View File

@ -1,6 +1,11 @@
// #docregion
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
platformBrowserDynamic().bootstrapModule(AppModule);

View File

@ -0,0 +1,16 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"baseUrl": "./",
"module": "commonjs",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
],
"angularCompilerOptions": {
"entryModule": "app/app.server.module#AppServerModule"
}
}

View File

@ -1,19 +0,0 @@
// #docregion
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from '../app/app.component';
import { AppModule } from '../app/app.module';
@NgModule({
imports: [
AppModule,
ServerModule,
],
providers: [
// Add universal-only providers here
],
bootstrap: [
AppComponent
]
})
export class AppServerModule {}

View File

@ -1,68 +0,0 @@
// Express Server for Angular Universal app
import 'zone.js/dist/zone-node';
import * as express from 'express';
import { enableProdMode } from '@angular/core';
// #docregion import-app-server-factory
// AppServerModuleNgFactory, generated by AOT webpack plug-in,
// exists in-memory during build.
// It is not available in the file system at design time
import { AppServerModuleNgFactory } from '../../aot/src/universal/app-server.module.ngfactory';
// #enddocregion import-app-server-factory
import { universalEngine } from './universal-engine';
enableProdMode();
const port = 3200;
const server = express();
// #docregion universal-engine
// Render HTML files with the universal template engine
server.engine('html', universalEngine({
appModuleFactory: AppServerModuleNgFactory
}));
// engine should find templates in 'dist/' by default
server.set('views', 'dist');
// #enddocregion universal-engine
// CRITICAL TODO: add authentication/authorization middleware
// #docregion data-request
// TODO: implement data requests securely
server.get('/api/*', (req, res) => {
res.status(404).send('data requests are not supported');
});
// #enddocregion data-request
// #docregion navigation-request
// simplistic regex matches any path without a '.'
const pathWithNoExt = /^([^.]*)$/;
// treat any path without an extension as in-app navigation
server.get(pathWithNoExt, (req, res) => {
// render with the universal template engine
res.render('index-universal.html', { req });
});
// #enddocregion navigation-request
// #docregion static
// remaining requests are for static files
server.use((req, res, next) => {
const fileName = req.originalUrl;
console.log(fileName);
// security: only serve files from dist
const root = 'dist';
res.sendFile(fileName, { root }, err => {
if (err) { next(err); }
});
});
// #enddocregion static
// start the server
server.listen(port, () => {
console.log(`listening on port ${port}...`);
});

View File

@ -1,49 +0,0 @@
/**
* Node Express template engine for Universal apps
*/
import * as fs from 'fs';
import { Request } from 'express';
import { renderModuleFactory } from '@angular/platform-server';
import { APP_BASE_HREF } from '@angular/common';
const templateCache: { [key: string]: string } = {}; // page templates
export function universalEngine(setupOptions: any) {
// Express template engine middleware
return function (
filePath: string,
options: { req: Request },
callback: (err: Error, html: string) => void) {
const { req } = options;
const routeUrl = req.url;
let template = templateCache[filePath];
if (!template) {
template = fs.readFileSync(filePath).toString();
templateCache[filePath] = template;
}
const { appModuleFactory } = setupOptions;
const origin = getOrigin(req);
// #docregion render
// render the page
renderModuleFactory(appModuleFactory, {
document: template,
url: routeUrl,
extraProviders: [
{ provide: APP_BASE_HREF, useValue: origin }
]
})
.then(page => callback(null, page));
// #enddocregion render
};
}
function getOrigin(req: Request) {
// e.g., http://localhost:3200/
return `${req.protocol}://${req.hostname}:${req.connection.address().port}/`;
}

View File

@ -1,13 +0,0 @@
{
"extends": "./tsconfig.universal.json",
"files": [
"src/main.ts"
],
"angularCompilerOptions": {
"genDir": "aot",
"entryModule": "./src/app/app.module#AppModule",
"skipMetadataEmit" : true
}
}

View File

@ -1,27 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "es2015",
"moduleResolution": "node",
"sourceMap": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["es2015", "dom"],
"noImplicitAny": true,
"suppressImplicitAnyIndexErrors": true,
"typeRoots": [
"./node_modules/@types/"
]
},
"files": [
"src/universal/app-server.module.ts",
"src/universal/server.ts"
],
"angularCompilerOptions": {
"genDir": "aot",
"entryModule": "./src/app/app.module#AppModule",
"skipMetadataEmit" : true
}
}

View File

@ -1,34 +0,0 @@
// #docregion
const ngtools = require('@ngtools/webpack');
const webpack = require('webpack');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
devtool: 'source-map',
entry: {
main: [ './src/main.ts' ]
},
resolve: {
extensions: ['.ts', '.js']
},
output: {
path: __dirname + '/dist',
filename: 'client.js'
},
plugins: [
// compile with AOT
new ngtools.AotPlugin({
tsConfigPath: './tsconfig.client.json'
}),
// minify
new UglifyJSPlugin()
],
module: {
rules: [
{ test: /\.css$/, loader: 'raw-loader' },
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.ts$/, loader: '@ngtools/webpack' }
]
}
}

View File

@ -1,43 +0,0 @@
// #docregion
const ngtools = require('@ngtools/webpack');
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
devtool: 'source-map',
entry: {
main: [
'./src/universal/app-server.module.ts',
'./src/universal/server.ts'
]
},
resolve: {
extensions: ['.ts', '.js']
},
target: 'node',
output: {
path: __dirname + '/dist',
filename: 'server.js'
},
plugins: [
// compile with AOT
new ngtools.AotPlugin({
tsConfigPath: './tsconfig.universal.json'
}),
// copy assets to the output (/dist) folder
new CopyWebpackPlugin([
{from: 'src/index-universal.html'},
{from: 'src/styles.css'},
{from: 'node_modules/core-js/client/shim.min.js'},
{from: 'node_modules/zone.js/dist/zone.min.js'},
])
],
module: {
rules: [
{ test: /\.css$/, loader: 'raw-loader' },
{ test: /\.html$/, loader: 'raw-loader' },
{ test: /\.ts$/, loader: '@ngtools/webpack' }
]
}
}

View File

@ -0,0 +1,31 @@
const path = require('path');
const webpack = require('webpack');
module.exports = {
entry: { server: './server.ts' },
resolve: { extensions: ['.js', '.ts'] },
target: 'node',
// this makes sure we include node_modules and other 3rd party libraries
externals: [/(node_modules|main\..*\.js)/],
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
module: {
rules: [{ test: /\.ts$/, loader: 'ts-loader' }]
},
plugins: [
// Temporary Fix for issue: https://github.com/angular/angular/issues/11580
// for 'WARNING Critical dependency: the request of a dependency is an expression'
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src'), // location of your src
{} // a map of your routes
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src'),
{}
)
]
};