
committed by
Matias Niemelä

parent
a363be6b5c
commit
851e1cfcfd
3
aio/content/examples/.gitignore
vendored
3
aio/content/examples/.gitignore
vendored
@ -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
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"projectType": "systemjs"
|
||||
"projectType": "universal"
|
||||
}
|
||||
|
61
aio/content/examples/universal/server.ts
Normal file
61
aio/content/examples/universal/server.ts
Normal 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}`);
|
||||
});
|
@ -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({
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* AppComponent's private CSS styles */
|
||||
h1 {
|
||||
font-size: 1.2em;
|
||||
color: #999;
|
||||
|
@ -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>
|
@ -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 {
|
||||
|
@ -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
|
||||
|
19
aio/content/examples/universal/src/app/app.server.module.ts
Normal file
19
aio/content/examples/universal/src/app/app.server.module.ts
Normal 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 {}
|
@ -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));
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
/* DashboardComponent's private CSS styles */
|
||||
[class*='col-'] {
|
||||
float: left;
|
||||
padding-right: 20px;
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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));
|
||||
}
|
||||
}
|
@ -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>
|
@ -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();
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
/* HeroDetailComponent's private CSS styles */
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 3em;
|
@ -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>
|
@ -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());
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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)),
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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]);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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};
|
||||
}
|
||||
|
@ -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();
|
||||
}));
|
||||
});
|
14
aio/content/examples/universal/src/app/message.service.ts
Normal file
14
aio/content/examples/universal/src/app/message.service.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
@ -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();
|
||||
});
|
||||
});
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
14
aio/content/examples/universal/src/app/mock-heroes.ts
Normal file
14
aio/content/examples/universal/src/app/mock-heroes.ts
Normal 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' }
|
||||
];
|
@ -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>
|
@ -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>
|
||||
|
1
aio/content/examples/universal/src/main.server.ts
Normal file
1
aio/content/examples/universal/src/main.server.ts
Normal file
@ -0,0 +1 @@
|
||||
export { AppServerModule } from './app/app.server.module';
|
@ -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);
|
||||
|
16
aio/content/examples/universal/src/tsconfig.server.json
Normal file
16
aio/content/examples/universal/src/tsconfig.server.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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 {}
|
@ -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}...`);
|
||||
});
|
@ -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}/`;
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.universal.json",
|
||||
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
|
||||
"angularCompilerOptions": {
|
||||
"genDir": "aot",
|
||||
"entryModule": "./src/app/app.module#AppModule",
|
||||
"skipMetadataEmit" : true
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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' }
|
||||
]
|
||||
}
|
||||
}
|
@ -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' }
|
||||
]
|
||||
}
|
||||
}
|
31
aio/content/examples/universal/webpack.server.config.js
Normal file
31
aio/content/examples/universal/webpack.server.config.js
Normal 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'),
|
||||
{}
|
||||
)
|
||||
]
|
||||
};
|
Reference in New Issue
Block a user