diff --git a/aio/content/examples/.gitignore b/aio/content/examples/.gitignore
index 50e978c89f..87b99e2128 100644
--- a/aio/content/examples/.gitignore
+++ b/aio/content/examples/.gitignore
@@ -72,6 +72,9 @@ aot-compiler/**/*.factory.d.ts
# styleguide
!styleguide/src/systemjs.custom.js
+# universal
+!universal/**/webpack.config.universal.js
+
# plunkers
*plnkr.no-link.html
diff --git a/aio/content/examples/universal/package.steve.json b/aio/content/examples/universal/package.steve.json
deleted file mode 100644
index 15ef45e849..0000000000
--- a/aio/content/examples/universal/package.steve.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "name": "toh-universal",
- "version": "1.0.0",
- "description": "Tour-of-Heroes application with ng-universal server-side rendering",
- "scripts": {
- "build": "tsc -p src/",
- "build:watch": "tsc -w",
- "build:aot": "webpack --config webpack.config.aot.js",
- "build:uni": "webpack --config webpack.config.uni.js",
-
- "serve": "lite-server -c=bs-config.json",
- "serve:aot": "lite-server -c=bs-config.aot.js",
- "serve:uni": "node src/dist/server.js",
- "serve:uni2": "lite-server -c bs-config.uni.js",
-
- "prestart": "npm run build",
- "start": "concurrently \"npm run build:watch\" \"npm run serve\"",
-
- "lint": "tslint ./src/**/*.ts -t verbose",
- "ngc": "ngc",
- "clean": "rimraf src/dist && rimraf src/app/*.js* && rimraf src/uni/*.js* && rimraf src/main.js*"
- },
- "keywords": [],
- "author": "",
- "license": "MIT",
- "dependencies": {
- "@angular/common": "angular/common-builds",
- "@angular/compiler": "angular/compiler-builds",
- "@angular/compiler-cli": "angular/compiler-cli-builds",
- "@angular/core": "angular/core-builds",
- "@angular/forms": "angular/forms-builds",
- "@angular/http": "angular/http-builds",
- "@angular/platform-browser": "angular/platform-browser-builds",
- "@angular/platform-browser-dynamic": "angular/platform-browser-dynamic-builds",
- "@angular/platform-server": "angular/platform-server-builds",
- "@angular/router": "angular/router-builds",
-
- "angular-in-memory-web-api": "^0.3.1",
- "systemjs": "0.19.40",
- "core-js": "^2.4.1",
- "rxjs": "5.1.1",
- "zone.js": "^0.7.7"
- },
- "devDependencies": {
- "concurrently": "^3.2.0",
- "lite-server": "^2.2.2",
- "typescript": "~2.1.6",
-
- "canonical-path": "0.0.2",
- "tslint": "^3.15.1",
- "lodash": "^4.16.4",
- "rimraf": "^2.5.4",
-
- "@types/node": "^6.0.46",
-
- "@ngtools/webpack": "^1.2.11",
- "@types/express": "^4.0.35",
- "raw-loader": "^0.5.1",
- "webpack": "^2.2.1"
- },
- "repository": {}
-}
diff --git a/aio/content/examples/universal/src/app/app-routing.module.ts b/aio/content/examples/universal/src/app/app-routing.module.ts
index bc070f6c31..96b5a4a691 100644
--- a/aio/content/examples/universal/src/app/app-routing.module.ts
+++ b/aio/content/examples/universal/src/app/app-routing.module.ts
@@ -9,7 +9,8 @@ const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'detail/:id', component: HeroDetailComponent },
- { path: 'heroes', component: HeroesComponent }
+ { path: 'heroes', component: HeroesComponent },
+ { path: '**', redirectTo: '/dashboard' }
];
@NgModule({
diff --git a/aio/content/examples/universal/src/app/app.component.css b/aio/content/examples/universal/src/app/app.component.css
index 071e665767..40e1aba36d 100644
--- a/aio/content/examples/universal/src/app/app.component.css
+++ b/aio/content/examples/universal/src/app/app.component.css
@@ -1,4 +1,3 @@
-/* #docregion */
h1 {
font-size: 1.2em;
color: #999;
diff --git a/aio/content/examples/universal/src/app/app.component.ts b/aio/content/examples/universal/src/app/app.component.ts
index a9fe05a9a8..68079538a2 100644
--- a/aio/content/examples/universal/src/app/app.component.ts
+++ b/aio/content/examples/universal/src/app/app.component.ts
@@ -1,5 +1,3 @@
-// #docplaster
-// #docregion
import { Component } from '@angular/core';
@Component({
diff --git a/aio/content/examples/universal/src/app/app.module.ts b/aio/content/examples/universal/src/app/app.module.ts
index e078044928..5de3de00c0 100644
--- a/aio/content/examples/universal/src/app/app.module.ts
+++ b/aio/content/examples/universal/src/app/app.module.ts
@@ -1,54 +1,62 @@
// #docplaster
-// #docregion
-// #docregion v1, v2
-import { NgModule } from '@angular/core';
-import { BrowserModule } from '@angular/platform-browser';
-import { FormsModule } from '@angular/forms';
-import { HttpModule } from '@angular/http';
+// #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 { AppRoutingModule } from './app-routing.module';
-// #enddocregion v1
// Imports for loading & configuring the in-memory web api
-import { InMemoryWebApiModule } from 'angular-in-memory-web-api';
-import { InMemoryDataService } from './in-memory-data.service';
+import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
+import { InMemoryDataService } from './in-memory-data.service';
-// #docregion v1
import { AppComponent } from './app.component';
import { DashboardComponent } from './dashboard.component';
import { HeroesComponent } from './heroes.component';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroService } from './hero.service';
-// #enddocregion v1, v2
import { HeroSearchComponent } from './hero-search.component';
-// #docregion v1, v2
+// #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: [
- BrowserModule.withServerTransition({
- appId: 'toh-universal'
- }),
+ // #docregion browsermodule
+ BrowserModule.withServerTransition({ appId: 'uni' }),
+ // #enddocregion browsermodule
FormsModule,
- HttpModule,
- // #enddocregion v1
- // #docregion in-mem-web-api
- InMemoryWebApiModule.forRoot(InMemoryDataService),
- // #enddocregion in-mem-web-api
- // #docregion v1
+ HttpClientModule,
+ HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService),
AppRoutingModule
],
- // #docregion search
declarations: [
AppComponent,
DashboardComponent,
HeroDetailComponent,
HeroesComponent,
- // #enddocregion v1, v2
HeroSearchComponent
- // #docregion v1, v2
],
- // #enddocregion search
providers: [ HeroService ],
bootstrap: [ AppComponent ]
})
-export class AppModule { }
+export class AppModule {
+
+// #enddocregion simple
+// #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 simple
diff --git a/aio/content/examples/universal/src/app/dashboard.component.css b/aio/content/examples/universal/src/app/dashboard.component.css
index dc7fb7ce06..096cec7621 100644
--- a/aio/content/examples/universal/src/app/dashboard.component.css
+++ b/aio/content/examples/universal/src/app/dashboard.component.css
@@ -1,4 +1,3 @@
-/* #docregion */
[class*='col-'] {
float: left;
padding-right: 20px;
diff --git a/aio/content/examples/universal/src/app/dashboard.component.html b/aio/content/examples/universal/src/app/dashboard.component.html
index db8546ccd2..8852696c84 100644
--- a/aio/content/examples/universal/src/app/dashboard.component.html
+++ b/aio/content/examples/universal/src/app/dashboard.component.html
@@ -1,7 +1,6 @@
-
Top Heroes
-
+
{{hero.name}}
diff --git a/aio/content/examples/universal/src/app/dashboard.component.ts b/aio/content/examples/universal/src/app/dashboard.component.ts
index 9960aa77d4..2f9cbeb3ec 100644
--- a/aio/content/examples/universal/src/app/dashboard.component.ts
+++ b/aio/content/examples/universal/src/app/dashboard.component.ts
@@ -1,22 +1,23 @@
-// #docregion , search
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' ]
})
-// #enddocregion search
export class DashboardComponent implements OnInit {
- heroes: Hero[] = [];
+ heroes: Observable;
constructor(private heroService: HeroService) { }
ngOnInit(): void {
- this.heroService.getHeroes()
- .then(heroes => this.heroes = heroes.slice(1, 5));
+ this.heroes = this.heroService.getHeroes()
+ .map(heroes => heroes.slice(1, 5));
}
}
diff --git a/aio/content/examples/universal/src/app/hero-detail.component.css b/aio/content/examples/universal/src/app/hero-detail.component.css
index ab2437efd8..f6139ba274 100644
--- a/aio/content/examples/universal/src/app/hero-detail.component.css
+++ b/aio/content/examples/universal/src/app/hero-detail.component.css
@@ -1,4 +1,3 @@
-/* #docregion */
label {
display: inline-block;
width: 3em;
@@ -25,6 +24,6 @@ button:hover {
}
button:disabled {
background-color: #eee;
- color: #ccc;
+ color: #ccc;
cursor: auto;
}
diff --git a/aio/content/examples/universal/src/app/hero-detail.component.html b/aio/content/examples/universal/src/app/hero-detail.component.html
index 32fe6d4391..af377615c3 100644
--- a/aio/content/examples/universal/src/app/hero-detail.component.html
+++ b/aio/content/examples/universal/src/app/hero-detail.component.html
@@ -1,4 +1,3 @@
-
{{hero.name}} details!
@@ -8,7 +7,5 @@
Back
-
Save
-
diff --git a/aio/content/examples/universal/src/app/hero-detail.component.ts b/aio/content/examples/universal/src/app/hero-detail.component.ts
index 676c4c21b0..6e6cdace49 100644
--- a/aio/content/examples/universal/src/app/hero-detail.component.ts
+++ b/aio/content/examples/universal/src/app/hero-detail.component.ts
@@ -1,4 +1,3 @@
-// #docregion
import 'rxjs/add/operator/switchMap';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
@@ -27,12 +26,11 @@ export class HeroDetailComponent implements OnInit {
.subscribe(hero => this.hero = hero);
}
- // #docregion save
save(): void {
- this.heroService.update(this.hero)
- .then(() => this.goBack());
+ this.heroService
+ .update(this.hero)
+ .subscribe(() => this.goBack());
}
- // #enddocregion save
goBack(): void {
this.location.back();
diff --git a/aio/content/examples/universal/src/app/hero-search.component.css b/aio/content/examples/universal/src/app/hero-search.component.css
index 9bf8d13457..85f7da408e 100644
--- a/aio/content/examples/universal/src/app/hero-search.component.css
+++ b/aio/content/examples/universal/src/app/hero-search.component.css
@@ -1,4 +1,3 @@
-/* #docregion */
.search-result{
border-bottom: 1px solid gray;
border-left: 1px solid gray;
diff --git a/aio/content/examples/universal/src/app/hero-search.component.html b/aio/content/examples/universal/src/app/hero-search.component.html
index 08c0560c5b..43186da3b3 100644
--- a/aio/content/examples/universal/src/app/hero-search.component.html
+++ b/aio/content/examples/universal/src/app/hero-search.component.html
@@ -1,4 +1,3 @@
-
Hero Search
diff --git a/aio/content/examples/universal/src/app/hero-search.component.ts b/aio/content/examples/universal/src/app/hero-search.component.ts
index 8b2d32f06b..465f73834c 100644
--- a/aio/content/examples/universal/src/app/hero-search.component.ts
+++ b/aio/content/examples/universal/src/app/hero-search.component.ts
@@ -1,20 +1,14 @@
-// #docplaster
-// #docregion
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
-// #docregion rxjs-imports
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
-// Observable class extensions
import 'rxjs/add/observable/of';
-// Observable operators
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
-// #enddocregion rxjs-imports
import { HeroSearchService } from './hero-search.service';
import { Hero } from './hero';
@@ -26,27 +20,20 @@ import { Hero } from './hero';
providers: [HeroSearchService]
})
export class HeroSearchComponent implements OnInit {
- // #docregion search
heroes: Observable
;
- // #enddocregion search
- // #docregion searchTerms
private searchTerms = new Subject();
- // #enddocregion searchTerms
constructor(
private heroSearchService: HeroSearchService,
private router: Router) {}
- // #docregion searchTerms
// Push a search term into the observable stream.
search(term: string): void {
this.searchTerms.next(term);
}
- // #enddocregion searchTerms
- // #docregion search
ngOnInit(): void {
- this.heroes = this.searchTerms
+ 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
@@ -60,7 +47,6 @@ export class HeroSearchComponent implements OnInit {
return Observable.of([]);
});
}
- // #enddocregion search
gotoDetail(hero: Hero): void {
let link = ['/detail', hero.id];
diff --git a/aio/content/examples/universal/src/app/hero-search.service.ts b/aio/content/examples/universal/src/app/hero-search.service.ts
index 52bf95a158..2843d4e3b1 100644
--- a/aio/content/examples/universal/src/app/hero-search.service.ts
+++ b/aio/content/examples/universal/src/app/hero-search.service.ts
@@ -1,20 +1,28 @@
-// #docregion
-import { Injectable } from '@angular/core';
-import { Http } from '@angular/http';
+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 { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
-import { Hero } from './hero';
+import { Hero } from './hero';
+// #docregion class
@Injectable()
export class HeroSearchService {
- constructor(private http: Http) {}
+ 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 {
return this.http
- .get(`api/heroes/?name=${term}`)
- .map(response => response.json().data as Hero[]);
+ .get(this.searchUrl + term)
+ .map((data: any) => data.data as Hero[]);
}
}
+// #enddocregion class
diff --git a/aio/content/examples/universal/src/app/hero.service.ts b/aio/content/examples/universal/src/app/hero.service.ts
index 18af476123..6dc05481e5 100644
--- a/aio/content/examples/universal/src/app/hero.service.ts
+++ b/aio/content/examples/universal/src/app/hero.service.ts
@@ -1,87 +1,64 @@
-// #docplaster
-// #docregion , imports
-import { Injectable } from '@angular/core';
-import { Headers, Http } from '@angular/http';
+import { Injectable, Inject, Optional } from '@angular/core';
+import { APP_BASE_HREF } from '@angular/common';
+import { HttpHeaders, HttpClient } from '@angular/common/http';
-// #docregion rxjs
-import 'rxjs/add/operator/toPromise';
-// #enddocregion rxjs
+import { Observable } from 'rxjs/Observable';
+import 'rxjs/add/operator/catch';
+import 'rxjs/add/operator/do';
+import 'rxjs/add/operator/map';
import { Hero } from './hero';
-// #enddocregion imports
+
+const headers = new HttpHeaders({'Content-Type': 'application/json'});
@Injectable()
export class HeroService {
- // #docregion update
- private headers = new Headers({'Content-Type': 'application/json'});
- // #enddocregion update
- // #docregion getHeroes
private heroesUrl = 'api/heroes'; // URL to web api
- constructor(private http: Http) { }
+ // #docregion ctor
+ constructor(
+ private http: HttpClient,
+ @Optional() @Inject(APP_BASE_HREF) origin: string) {
+ this.heroesUrl = (origin || '') + this.heroesUrl;
+ }
+ // #enddocregion ctor
- getHeroes(): Promise {
+ getHeroes(): Observable {
return this.http.get(this.heroesUrl)
- // #docregion to-promise
- .toPromise()
- // #enddocregion to-promise
- // #docregion to-data
- .then(response => response.json().data as Hero[])
- // #enddocregion to-data
- // #docregion catch
+ .map((data: any) => data.data as Hero[])
.catch(this.handleError);
- // #enddocregion catch
}
- // #enddocregion getHeroes
-
- // #docregion getHero
- getHero(id: number): Promise {
+ getHero(id: number): Observable {
const url = `${this.heroesUrl}/${id}`;
return this.http.get(url)
- .toPromise()
- .then(response => response.json().data as Hero)
+ .map((data: any) => data.data as Hero)
.catch(this.handleError);
}
- // #enddocregion getHero
- // #docregion delete
- delete(id: number): Promise {
+ delete(id: number): Observable {
const url = `${this.heroesUrl}/${id}`;
- return this.http.delete(url, {headers: this.headers})
- .toPromise()
- .then(() => null)
+ return this.http.delete(url, { headers })
.catch(this.handleError);
}
- // #enddocregion delete
- // #docregion create
- create(name: string): Promise {
+ create(name: string): Observable {
return this.http
- .post(this.heroesUrl, JSON.stringify({name: name}), {headers: this.headers})
- .toPromise()
- .then(res => res.json().data)
+ .post(this.heroesUrl, { name: name }, { headers })
+ .map((data: any) => data.data)
.catch(this.handleError);
}
- // #enddocregion create
- // #docregion update
- update(hero: Hero): Promise {
+ update(hero: Hero): Observable {
const url = `${this.heroesUrl}/${hero.id}`;
return this.http
- .put(url, JSON.stringify(hero), {headers: this.headers})
- .toPromise()
- .then(() => hero)
+ .put(url, hero, { headers })
.catch(this.handleError);
}
- // #enddocregion update
- // #docregion getHeroes, handleError
- private handleError(error: any): Promise {
+ private handleError(error: any): Observable {
console.error('An error occurred', error); // for demo purposes only
- return Promise.reject(error.message || error);
+ throw error;
}
- // #enddocregion getHeroes, handleError
}
-
diff --git a/aio/content/examples/universal/src/app/heroes.component.ts b/aio/content/examples/universal/src/app/heroes.component.ts
index 6350b803c4..acd2dde1d2 100644
--- a/aio/content/examples/universal/src/app/heroes.component.ts
+++ b/aio/content/examples/universal/src/app/heroes.component.ts
@@ -21,31 +21,27 @@ export class HeroesComponent implements OnInit {
getHeroes(): void {
this.heroService
.getHeroes()
- .then(heroes => this.heroes = heroes);
+ .subscribe(heroes => this.heroes = heroes);
}
- // #docregion add
add(name: string): void {
name = name.trim();
if (!name) { return; }
this.heroService.create(name)
- .then(hero => {
+ .subscribe(hero => {
this.heroes.push(hero);
this.selectedHero = null;
});
}
- // #enddocregion add
- // #docregion delete
delete(hero: Hero): void {
this.heroService
.delete(hero.id)
- .then(() => {
+ .subscribe(() => {
this.heroes = this.heroes.filter(h => h !== hero);
if (this.selectedHero === hero) { this.selectedHero = null; }
});
}
- // #enddocregion delete
ngOnInit(): void {
this.getHeroes();
diff --git a/aio/content/examples/universal/src/index-aot.html b/aio/content/examples/universal/src/index-aot.html
deleted file mode 100644
index 096bfbb512..0000000000
--- a/aio/content/examples/universal/src/index-aot.html
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
- Angular Tour of Heroes
-
-
-
-
-
-
-
- Loading...
-
-
-
-
-
diff --git a/aio/content/examples/universal/src/main-aot.ts b/aio/content/examples/universal/src/main-aot.ts
deleted file mode 100644
index fcb35dc9dc..0000000000
--- a/aio/content/examples/universal/src/main-aot.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// #docregion
-import { platformBrowser } from '@angular/platform-browser';
-import { AppModuleNgFactory } from '../aot/src/app/app.module.ngfactory';
-
-platformBrowser().bootstrapModuleFactory(AppModuleNgFactory);
diff --git a/aio/content/examples/universal/src/uni/server-aot.ts b/aio/content/examples/universal/src/uni/server-aot.ts
deleted file mode 100644
index 38eb3f6b33..0000000000
--- a/aio/content/examples/universal/src/uni/server-aot.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import 'zone.js/dist/zone-node';
-import { enableProdMode } from '@angular/core';
-// import { AppServerModule } from './app.server';
-import { AppServerModuleNgFactory } from '../../aot/src/uni/app.server.ngfactory';
-import * as express from 'express';
-import { ngUniversalEngine } from './universal-engine';
-
-enableProdMode();
-
-const server = express();
-
-// set our angular engine as the handler for html files, so it will be used to render them.
-server.engine('html', ngUniversalEngine({
- bootstrap: [AppServerModuleNgFactory]
-}));
-
-// set default view directory
-server.set('views', 'src');
-
-// handle requests for routes in the app. ngExpressEngine does the rendering.
-server.get(['/', '/dashboard', '/heroes', '/detail/:id'], (req, res) => {
- res.render('index-aot.html', {req});
-});
-
-// handle requests for static files
-server.get(['/*.js', '/*.css'], (req, res, next) => {
- let fileName: string = req.originalUrl;
- console.log(fileName);
- let root = fileName.startsWith('/node_modules/') ? '.' : 'src';
- res.sendFile(fileName, { root: root }, function (err) {
- if (err) {
- next(err);
- }
- });
-});
-
-// start the server
-server.listen(3200, () => {
- console.log('listening on port 3200...');
-});
diff --git a/aio/content/examples/universal/src/uni/universal-engine.ts b/aio/content/examples/universal/src/uni/universal-engine.ts
deleted file mode 100644
index 3c0ac6b528..0000000000
--- a/aio/content/examples/universal/src/uni/universal-engine.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Express/Connect middleware for rendering pages using Angular Universal
- */
-import * as fs from 'fs';
-import { renderModuleFactory } from '@angular/platform-server';
-
-const templateCache = {}; // cache for page templates
-const outputCache = {}; // cache for rendered pages
-
-export function ngUniversalEngine(setupOptions: any) {
-
- return function (filePath: string, options: { req: Request }, callback: (err: Error, html: string) => void) {
- let url: string = options.req.url;
- let html: string = outputCache[url];
- if (html) {
- // return already-built page for this url
- console.log('from cache: ' + url);
- callback(null, html);
- return;
- }
-
- console.log('building: ' + url);
- if (!templateCache[filePath]) {
- let file = fs.readFileSync(filePath);
- templateCache[filePath] = file.toString();
- }
-
- // render the page via angular platform-server
- let appModuleFactory = setupOptions.bootstrap[0];
- renderModuleFactory(appModuleFactory, {
- document: templateCache[filePath],
- url: url
- }).then(str => {
- outputCache[url] = str;
- callback(null, str);
- });
- };
-}
diff --git a/aio/content/examples/universal/src/uni/app.server.ts b/aio/content/examples/universal/src/universal/app-server.module.ts
similarity index 59%
rename from aio/content/examples/universal/src/uni/app.server.ts
rename to aio/content/examples/universal/src/universal/app-server.module.ts
index 585470cd09..1ae883e2ed 100644
--- a/aio/content/examples/universal/src/uni/app.server.ts
+++ b/aio/content/examples/universal/src/universal/app-server.module.ts
@@ -1,21 +1,19 @@
+// #docregion
import { NgModule } from '@angular/core';
-import { APP_BASE_HREF } from '@angular/common';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from '../app/app.component';
import { AppModule } from '../app/app.module';
@NgModule({
imports: [
+ AppModule,
ServerModule,
- AppModule
+ ],
+ providers: [
+ // Add universal-only providers here
],
bootstrap: [
AppComponent
- ],
- providers: [
- {provide: APP_BASE_HREF, useValue: '/'}
- // { provide: NgModuleFactoryLoader, useClass: ServerRouterLoader }
]
})
-export class AppServerModule {
-}
+export class AppServerModule {}
diff --git a/aio/content/examples/universal/src/universal/server.ts b/aio/content/examples/universal/src/universal/server.ts
new file mode 100644
index 0000000000..e307c7d622
--- /dev/null
+++ b/aio/content/examples/universal/src/universal/server.ts
@@ -0,0 +1,65 @@
+// 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 compiler, is not available 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 (like index.html) in 'src/' by default
+server.set('views', 'src');
+// #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.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 node_modules or src
+ const root = fileName.startsWith('/node_modules/') ? '.' : 'src';
+ res.sendFile(fileName, { root }, err => {
+ if (err) { next(err); }
+ });
+});
+// #enddocregion static
+
+// start the server
+server.listen(port, () => {
+ console.log(`listening on port ${port}...`);
+});
diff --git a/aio/content/examples/universal/src/universal/universal-engine.ts b/aio/content/examples/universal/src/universal/universal-engine.ts
new file mode 100644
index 0000000000..bab955b8de
--- /dev/null
+++ b/aio/content/examples/universal/src/universal/universal-engine.ts
@@ -0,0 +1,62 @@
+/**
+ * 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
+const outputCache: { [key: string]: string } = {}; // rendered pages
+
+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;
+
+ const html = outputCache[routeUrl];
+ if (html) {
+ // return already-built page for this url
+ console.log('from cache: ' + routeUrl);
+ callback(null, html);
+ return;
+ }
+
+ console.log('building: ' + routeUrl);
+ let template = templateCache[filePath];
+ if (!template) {
+ template = fs.readFileSync(filePath).toString();
+ templateCache[filePath] = template;
+ }
+
+ const { appModuleFactory } = setupOptions;
+ const origin = getOrigin(req);
+
+ // render the page
+ // #docregion render
+ renderModuleFactory(appModuleFactory, {
+ document: template,
+ url: routeUrl,
+ extraProviders: [
+ { provide: APP_BASE_HREF, useValue: origin }
+ ]
+ })
+ .then(page => {
+ outputCache[routeUrl] = page;
+ callback(null, page);
+ });
+ // #enddocregion render
+ };
+}
+
+function getOrigin(req: Request) {
+ // e.g., http://localhost:3200/
+ return `${req.protocol}://${req.hostname}:${req.connection.address().port}/`;
+}
diff --git a/aio/content/examples/universal/tsconfig-aot.json b/aio/content/examples/universal/tsconfig-aot.json
deleted file mode 100644
index fd4632d05f..0000000000
--- a/aio/content/examples/universal/tsconfig-aot.json
+++ /dev/null
@@ -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/app/app.module.ts",
- "src/main-aot.ts"
- ],
-
- "angularCompilerOptions": {
- "genDir": "aot",
- "entryModule": "./src/app/app.module#AppModule",
- "skipMetadataEmit" : true
- }
-}
diff --git a/aio/content/examples/universal/tsconfig-uni.json b/aio/content/examples/universal/tsconfig-universal.json
similarity index 87%
rename from aio/content/examples/universal/tsconfig-uni.json
rename to aio/content/examples/universal/tsconfig-universal.json
index fcb6a7c92d..f23de99091 100644
--- a/aio/content/examples/universal/tsconfig-uni.json
+++ b/aio/content/examples/universal/tsconfig-universal.json
@@ -15,8 +15,8 @@
},
"files": [
- "src/uni/app.server.ts",
- "src/uni/server-aot.ts"
+ "src/universal/app-server.module.ts",
+ "src/universal/server.ts"
],
"angularCompilerOptions": {
diff --git a/aio/content/examples/universal/webpack.config.universal.js b/aio/content/examples/universal/webpack.config.universal.js
new file mode 100644
index 0000000000..f2d31f85ee
--- /dev/null
+++ b/aio/content/examples/universal/webpack.config.universal.js
@@ -0,0 +1,40 @@
+const ngtools = require('@ngtools/webpack');
+const webpack = require('webpack');
+
+module.exports = {
+ devtool: 'source-map',
+// #docregion entry
+ entry: {
+ main: [
+ './src/universal/app-server.module.ts',
+ './src/universal/server.ts'
+ ]
+ },
+// #enddocregion entry
+ resolve: {
+ extensions: ['.ts', '.js']
+ },
+ target: 'node',
+// #docregion output
+ output: {
+ path: 'src/dist',
+ filename: 'server.js'
+ },
+// #enddocregion output
+// #docregion plugins
+ plugins: [
+ new ngtools.AotPlugin({
+ tsConfigPath: './tsconfig-universal.json'
+ })
+ ],
+// #enddocregion plugins
+// #docregion rules
+ module: {
+ rules: [
+ { test: /\.css$/, loader: 'raw-loader' },
+ { test: /\.html$/, loader: 'raw-loader' },
+ { test: /\.ts$/, loader: '@ngtools/webpack' }
+ ]
+ }
+// #enddocregion rules
+}
diff --git a/aio/content/examples/universal/zipper.json b/aio/content/examples/universal/zipper.json
new file mode 100644
index 0000000000..a493091fd5
--- /dev/null
+++ b/aio/content/examples/universal/zipper.json
@@ -0,0 +1,9 @@
+{
+ "files":[
+ "!**/*.d.ts",
+ "!**/src/**/*.js",
+ "!**/universal/**/*.js"
+ ],
+ "removeSystemJsConfig": false,
+ "type": "universal"
+}
diff --git a/aio/content/guide/universal.md b/aio/content/guide/universal.md
new file mode 100644
index 0000000000..31d72768de
--- /dev/null
+++ b/aio/content/guide/universal.md
@@ -0,0 +1,849 @@
+# Angular Universal
+
+This guide describes **Angular Universal**, a technology that runs your Angular application on the server.
+
+A normal Angular application executes in the _browser_, rendering pages in the DOM in response to user actions.
+
+**Angular Universal** generates _static_ application pages on the _server_
+through a process called **server-side rendering (SSR)**.
+
+It can generate and serve those pages in response to requests from browsers.
+It can also pre-generate pages as HTML files that you serve later.
+
+Universal's server-side rendering has several potential benefits:
+
+* [Facilitate web crawlers (SEO)](#web-crawlers).
+* [Show content sooner](#startup-performance).
+* [Perform well on mobile and low power devices](#no-javascript).
+
+This guide describes a Universal sample application that launches quickly as a server-rendered page.
+Meanwhile, the browser downloads the full client version and switches to it automatically after the code loads.
+
+
+
+[Download the finished sample code](generated/zips/universal/universal.zip),
+which runs in a [node express](https://expressjs.com/) server.
+
+Almost _any_ web server technology can serve a Universal app.
+See this advanced example written for
+[ASP.NET Core](https://github.com/MarkPieszak/aspnetcore-angular2-universal).
+
+
+
+
+The build setup described in this guide is experimental and subject to change.
+
+
+
+## Overview
+
+This overview explains the benefits of a Universal application, how it works, and the limitations of server-side rendering. Then it describes the sample application that goes with this guide.
+
+Subsequent sections describe a sample Universal application derived from the Tour of Heroes tutorial
+and explain how to build and run that app.
+
+{@a why-do-it}
+
+### Why Universal
+
+There are three main reasons to create a Universal version of your app.
+
+1. Facilitate web crawlers (SEO)
+1. Improve performance on mobile and low-powered devices
+1. Show the first page quickly
+
+{@a seo}
+{@a web-crawlers}
+#### Facilitate web crawlers
+
+Google, Bing, Facebook, twitter and other social media sites rely on web crawlers to index your application content and make that content searchable on the web.
+
+These web crawlers may be unable to navigate and index your highly-interactive, Angular application as a human user could do.
+
+Angular Universal can generate a static version of your app that is easy searchable, linkable, and navigable without JavaScript.
+It also makes a site preview available since each URL returns a fully-rendered page.
+
+Enabling web crawlers is often referred to as
+[Search Engine Optimization (SEO)](https://static.googleusercontent.com/media/www.google.com/en//webmasters/docs/search-engine-optimization-starter-guide.pdf).
+
+{@a no-javascript}
+
+#### Performance on mobile and low performance devices
+
+Some devices don't support JavaScript or execute JavaScript so poorly that the user experience is unacceptable.
+For these cases, you may require a server-rendered, no-JavaScript version of the app.
+This version, however limited, may be the only practical alternative for
+people who otherwise would not be able to use the app at all.
+
+{@a startup-performance}
+
+#### Show the first page quickly
+
+Displaying the first page quickly can be critical for user engagement.
+
+Captive users of a line-of-business app may have to wait.
+But a casual visitor will switch to a faster site if your app takes "too long" to show the first page.
+
+While [AOT](guide/aot-compiler) compilation speeds up application start times, it may not be fast enough, especially on mobile devices with slow connections.
+[53% of mobile site visits are abandoned](https://www.doubleclickbygoogle.com/articles/mobile-speed-matters/) if pages take longer than 3 seconds to load.
+Your app needs to load quickly, to engage users before they decide to do something else.
+
+With Angular Universal, you can generate landing pages for the app that look like the complete app.
+The pages are pure HTML, and can display even if JavaScript is disabled.
+The pages do not handle browser events, but they _do_ support navigation through the site using [routerLink](guide/router.html#router-link).
+
+Of course most Angular apps are highly interactive.
+The landing page looks real and is far more useful than a "loading" spinner.
+But it won't fool anyone for long.
+
+In practice, you'll serve a static version of the landing page to hold the user's attention.
+At the same time, you'll load the full Angular app behind it in the manner [explained below](#transition).
+The user perceives near-instant performance from the landing page
+and gets the full interactive experience after the full app loads.
+
+
+Another tool called
Preboot can record browser events such as user keystrokes during the transition and play them back in the full Angular app once it is loaded.
+
+
+{@a how-does-it-work}
+### How it works
+
+To make a Universal app, you install the `platform-server` package.
+The `platform-server` package has server implementations of the DOM, `XMLHttpRequest`, and other low-level features that do not rely on a browser.
+
+You compile the client application with the `platform-server` module instead of the `platform-browser` module.
+and run the resulting Universal app on a web server.
+
+The server (a [Node Express](https://expressjs.com/) server in _this_ guide's example)
+passes client requests for application pages to Universal's `renderModuleFactory` function.
+
+The `renderModuleFactory` function takes as inputs a *template* HTML page (usually `index.html`),
+an Angular *module* containing components,
+and a *route* that determines which components to display.
+
+The route comes from the client's request to the server.
+Each request results in the appropriate view for the requested route.
+
+The `renderModuleFactory` renders that view within the `` tag of the template, creating a finished HTML page for the client.
+
+Finally, the server returns the rendered page to the client.
+
+{@a limitations}
+
+### Working around the browser APIs
+
+Because a Universal `platform-server` app doesn't execute in the browser, you may have to work around some of the APIs and capabilities that you otherwise take for granted on the client.
+
+You won't be able reference browser-only native objects such as `window`, `document`, `navigator` or `location`.
+If you don't need them on the server-rendered page, side-step them with conditional logic.
+
+Alternatively, look for an injectable Angular abstraction over the object you need such as `Location` or `Document`;
+it may substitute adequately for the specific API that you're calling.
+If Angular doesn't provide it, you may be able to write your own abstraction that delegates to the browser API while in the browser and to a satisfactory alternative implementation while on the server.
+
+Without mouse or keyboard events, a universal app can't rely on a user clicking a button to show a component.
+A universal app should determine what to render based solely on the incoming client request.
+This is a good argument for making the app [routeable](guide/router).
+
+Http requests with _relative_ URLs don't work.
+You should convert them to _absolute_ URLs on the server which means you'll need to know the server origin.
+You can pass the server origin into your app with a [provider](guide/dependency-injection#injector-providers) "universal/*"
+ as you'll see in the [example below](#http-urls).
+
+Because the user of a server-rendered page can't do much more than click links,
+you should [swap in the real client app](#transition) as quickly as possible for a proper interactive experience.
+
+{@a the-example}
+
+## The example
+
+The _Tour of Heroes_ tutorial is the foundation for the Universal sample described in this guide.
+
+The core application files are mostly untouched, with a few exceptions described below.
+You'll add more files to support building and serving with Universal.
+
+In this example, Webpack tools compile and bundle the Universal version of the app with the
+[AOT (Ahead-of-Time) compiler](guide/aot-compiler).
+A node/express web server turns client requests into the HTML pages rendered by Universal.
+
+You will create:
+
+ * a server-side app module, `app.server.module.ts`
+ * a Universal app renderer, `universal-engine.ts`
+ * an express web server to handle requests, `server.ts`
+ * a TypeScript config file, `tsconfig-universal.json`
+ * a Webpack config file, `webpack.config.universal.js`
+
+When you're done, the folder structure will look like this:
+
+
+src/
+ index.html app web page
+ main.ts bootstrapper for client app
+ style.css styles for the app
+ systemjs.config.js SystemJS client configuration
+ systemjs-angular-loader.js SystemJS add-in
+ tsconfig.json TypeScript client configuration
+ app/ ... application code
+ dist/
+ server.js * AOT-compiled server bundle
+ universal/ * folder for universal code
+ app-server.module.ts * server-side application module
+ server.ts * express web server
+ universal-engine.ts * express template engine
+bs-config.json config file for lite server
+package.json npm configuration
+tsconfig-universal.json * TypeScript Universal configuration
+webpack.config.universal.js * Webpack Universal configuration
+
+
+The files marked with `*` are new and not in the original tutorial sample.
+This guide covers them in the sections below.
+
+{@a preparation}
+
+## Preparation
+
+{@a install-the-tools}
+
+### Install the tools
+
+To get started, install these Universal and Webpack packages.
+
+ * `@angular/compiler-cli` - contains the AOT compiler
+ * `@angular/platform-server` - Universal server-side components
+ * `webpack` - Webpack JavaScript bundler
+ * `@ngtools/webpack` - Webpack loader and plugin for bundling compiled applications
+ * `raw-loader` - Webpack loader for text files
+ * `express` - node web server
+ * `@types/express` - TypeScript type definitions for express
+
+Install them with the following commands:
+
+
+npm install @angular/compiler-cli @angular/platform-server express --save
+npm install webpack @ngtools/webpack raw-loader @types/express --save-dev
+
+
+### Modify the client app
+
+You'll have to modify the client application in a few small ways to enable server-side rendering and
+to facilitate the transition from the Universal app to the client app.
+
+{@a transition}
+
+#### Enable transition to the client app
+
+A Universal app can act as a dynamic "splash screen" that shows a view of your app while the real client app loads behind it.
+This gives the appearance of a near-instant application.
+
+Meanwhile, the browser downloads the client app scripts in background.
+Once loaded, Angular transitions from the static server-rendered page to the dynamically rendered views of the live client app.
+
+To make this work, the template for server-side rendering contains the `