diff --git a/templates/Angular2Spa/ClientApp/app/app.module.client.ts b/templates/Angular2Spa/ClientApp/app/app.module.client.ts new file mode 100644 index 0000000..ee77812 --- /dev/null +++ b/templates/Angular2Spa/ClientApp/app/app.module.client.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; +import { sharedConfig } from './app.module.shared'; + +@NgModule({ + bootstrap: sharedConfig.bootstrap, + declarations: sharedConfig.declarations, + imports: [ + BrowserModule, + FormsModule, + HttpModule, + ...sharedConfig.imports + ], + providers: [ + { provide: 'ORIGIN_URL', useValue: location.origin } + ] +}) +export class AppModule { +} diff --git a/templates/Angular2Spa/ClientApp/app/app.module.server.ts b/templates/Angular2Spa/ClientApp/app/app.module.server.ts new file mode 100644 index 0000000..f2b6eb4 --- /dev/null +++ b/templates/Angular2Spa/ClientApp/app/app.module.server.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { sharedConfig } from './app.module.shared'; + +@NgModule({ + bootstrap: sharedConfig.bootstrap, + declarations: sharedConfig.declarations, + imports: [ + ServerModule, + ...sharedConfig.imports + ] +}) +export class AppModule { +} diff --git a/templates/Angular2Spa/ClientApp/app/app.module.ts b/templates/Angular2Spa/ClientApp/app/app.module.shared.ts similarity index 81% rename from templates/Angular2Spa/ClientApp/app/app.module.ts rename to templates/Angular2Spa/ClientApp/app/app.module.shared.ts index 6d912da..1861501 100644 --- a/templates/Angular2Spa/ClientApp/app/app.module.ts +++ b/templates/Angular2Spa/ClientApp/app/app.module.shared.ts @@ -1,13 +1,13 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { UniversalModule } from 'angular2-universal'; + import { AppComponent } from './components/app/app.component' import { NavMenuComponent } from './components/navmenu/navmenu.component'; import { HomeComponent } from './components/home/home.component'; import { FetchDataComponent } from './components/fetchdata/fetchdata.component'; import { CounterComponent } from './components/counter/counter.component'; -@NgModule({ +export const sharedConfig: NgModule = { bootstrap: [ AppComponent ], declarations: [ AppComponent, @@ -17,7 +17,6 @@ import { CounterComponent } from './components/counter/counter.component'; HomeComponent ], imports: [ - UniversalModule, // Must be first import. This automatically imports BrowserModule, HttpModule, and JsonpModule too. RouterModule.forRoot([ { path: '', redirectTo: 'home', pathMatch: 'full' }, { path: 'home', component: HomeComponent }, @@ -26,6 +25,4 @@ import { CounterComponent } from './components/counter/counter.component'; { path: '**', redirectTo: 'home' } ]) ] -}) -export class AppModule { -} +}; diff --git a/templates/Angular2Spa/ClientApp/app/components/fetchdata/fetchdata.component.ts b/templates/Angular2Spa/ClientApp/app/components/fetchdata/fetchdata.component.ts index e93d4fa..9c98b76 100644 --- a/templates/Angular2Spa/ClientApp/app/components/fetchdata/fetchdata.component.ts +++ b/templates/Angular2Spa/ClientApp/app/components/fetchdata/fetchdata.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, Inject } from '@angular/core'; import { Http } from '@angular/http'; @Component({ @@ -8,8 +8,8 @@ import { Http } from '@angular/http'; export class FetchDataComponent { public forecasts: WeatherForecast[]; - constructor(http: Http) { - http.get('/api/SampleData/WeatherForecasts').subscribe(result => { + constructor(http: Http, @Inject('ORIGIN_URL') originUrl: string) { + http.get(originUrl + '/api/SampleData/WeatherForecasts').subscribe(result => { this.forecasts = result.json() as WeatherForecast[]; }); } diff --git a/templates/Angular2Spa/ClientApp/boot-client.ts b/templates/Angular2Spa/ClientApp/boot-client.ts index 963598a..45c471c 100644 --- a/templates/Angular2Spa/ClientApp/boot-client.ts +++ b/templates/Angular2Spa/ClientApp/boot-client.ts @@ -1,29 +1,22 @@ -import 'angular2-universal-polyfills/browser'; +import 'reflect-metadata'; +import 'zone.js'; import { enableProdMode } from '@angular/core'; -import { platformUniversalDynamic } from 'angular2-universal'; -import { AppModule } from './app/app.module'; -import 'bootstrap'; -const rootElemTagName = 'app'; // Update this if you change your root component selector +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { AppModule } from './app/app.module.client'; -// Enable either Hot Module Reloading or production mode if (module['hot']) { module['hot'].accept(); module['hot'].dispose(() => { // Before restarting the app, we create a new root element and dispose the old one - const oldRootElem = document.querySelector(rootElemTagName); - const newRootElem = document.createElement(rootElemTagName); + const oldRootElem = document.querySelector('app'); + const newRootElem = document.createElement('app'); oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem); - platform.destroy(); + modulePromise.then(appModule => appModule.destroy()); }); } else { enableProdMode(); } -// Boot the application, either now or when the DOM content is loaded -const platform = platformUniversalDynamic(); -const bootApplication = () => { platform.bootstrapModule(AppModule); }; -if (document.readyState === 'complete') { - bootApplication(); -} else { - document.addEventListener('DOMContentLoaded', bootApplication); -} +// Note: @ng-tools/webpack looks for the following expression when performing production +// builds. Don't change how this line looks, otherwise you may break tree-shaking. +const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/templates/Angular2Spa/ClientApp/boot-server.ts b/templates/Angular2Spa/ClientApp/boot-server.ts index b39c256..08474cf 100644 --- a/templates/Angular2Spa/ClientApp/boot-server.ts +++ b/templates/Angular2Spa/ClientApp/boot-server.ts @@ -1,34 +1,36 @@ -import 'angular2-universal-polyfills'; -import 'angular2-universal-patch'; +import 'reflect-metadata'; import 'zone.js'; +import 'rxjs/add/operator/first'; +import { enableProdMode, ApplicationRef, NgZone, ValueProvider } from '@angular/core'; +import { platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server'; import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; -import { enableProdMode } from '@angular/core'; -import { platformNodeDynamic } from 'angular2-universal'; -import { AppModule } from './app/app.module'; +import { AppModule } from './app/app.module.server'; enableProdMode(); -const platform = platformNodeDynamic(); export default createServerRenderer(params => { - return new Promise((resolve, reject) => { - const requestZone = Zone.current.fork({ - name: 'angular-universal request', - properties: { - baseUrl: '/', - requestUrl: params.url, - originUrl: params.origin, - preboot: false, - document: '' - }, - onHandleError: (parentZone, currentZone, targetZone, error) => { - // If any error occurs while rendering the module, reject the whole operation - reject(error); - return true; - } - }); + const providers = [ + { provide: INITIAL_CONFIG, useValue: { document: '', url: params.url } }, + { provide: 'ORIGIN_URL', useValue: params.origin } + ]; - return requestZone.run>(() => platform.serializeModule(AppModule)).then(html => { - resolve({ html: html }); - }, reject); + return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => { + const appRef = moduleRef.injector.get(ApplicationRef); + const state = moduleRef.injector.get(PlatformState); + const zone = moduleRef.injector.get(NgZone); + + return new Promise((resolve, reject) => { + zone.onError.subscribe(errorInfo => reject(errorInfo)); + appRef.isStable.first(isStable => isStable).subscribe(() => { + // Because 'onStable' fires before 'onError', we have to delay slightly before + // completing the request in case there's an error to report + setImmediate(() => { + resolve({ + html: state.renderToString() + }); + moduleRef.destroy(); + }); + }); + }); }); }); diff --git a/templates/Angular2Spa/ClientApp/test/boot-tests.ts b/templates/Angular2Spa/ClientApp/test/boot-tests.ts index 6121b04..fe3591e 100644 --- a/templates/Angular2Spa/ClientApp/test/boot-tests.ts +++ b/templates/Angular2Spa/ClientApp/test/boot-tests.ts @@ -1,5 +1,6 @@ // Load required polyfills and testing libraries -import 'angular2-universal-polyfills'; +import 'reflect-metadata'; +import 'zone.js'; import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test'; diff --git a/templates/Angular2Spa/package.json b/templates/Angular2Spa/package.json index 8324fd5..b34915b 100644 --- a/templates/Angular2Spa/package.json +++ b/templates/Angular2Spa/package.json @@ -5,58 +5,56 @@ "test": "karma start ClientApp/test/karma.conf.js" }, "dependencies": { - "@angular/common": "^2.4.5", - "@angular/compiler": "^2.4.5", - "@angular/core": "^2.4.5", - "@angular/forms": "^2.4.5", - "@angular/http": "^2.4.5", - "@angular/platform-browser": "^2.4.5", - "@angular/platform-browser-dynamic": "^2.4.5", - "@angular/platform-server": "^2.4.5", - "@angular/router": "^3.4.5", - "@types/node": "^6.0.42", - "angular2-platform-node": "~2.0.11", - "angular2-template-loader": "^0.6.2", - "angular2-universal": "^2.1.0-rc.1", - "angular2-universal-patch": "^0.2.1", - "angular2-universal-polyfills": "^2.1.0-rc.1", - "aspnet-prerendering": "^2.0.0", - "aspnet-webpack": "^1.0.17", - "awesome-typescript-loader": "^3.0.0", - "bootstrap": "^3.3.7", - "css": "^2.2.1", - "css-loader": "^0.25.0", - "es6-shim": "^0.35.1", - "event-source-polyfill": "^0.0.7", - "expose-loader": "^0.7.1", - "extract-text-webpack-plugin": "^2.0.0-rc", - "file-loader": "^0.9.0", - "html-loader": "^0.4.4", - "isomorphic-fetch": "^2.2.1", - "jquery": "^2.2.1", - "json-loader": "^0.5.4", - "preboot": "^4.5.2", - "raw-loader": "^0.5.1", - "rxjs": "^5.0.1", - "style-loader": "^0.13.1", - "to-string-loader": "^1.1.5", - "typescript": "^2.2.1", - "url-loader": "^0.5.7", - "webpack": "^2.2.0", - "webpack-hot-middleware": "^2.12.2", - "webpack-merge": "^0.14.1", - "zone.js": "^0.7.6" + "@angular/animations": "4.1.2", + "@angular/common": "4.1.2", + "@angular/compiler": "4.1.2", + "@angular/core": "4.1.2", + "@angular/forms": "4.1.2", + "@angular/http": "4.1.2", + "@angular/platform-browser": "4.1.2", + "@angular/platform-browser-dynamic": "4.1.2", + "@angular/platform-server": "4.1.2", + "@angular/router": "4.1.2", + "@types/node": "7.0.18", + "angular2-template-loader": "0.6.2", + "aspnet-prerendering": "^2.0.5", + "aspnet-webpack": "^1.0.29", + "awesome-typescript-loader": "3.1.3", + "bootstrap": "3.3.7", + "css": "2.2.1", + "css-loader": "0.28.1", + "es6-shim": "0.35.3", + "event-source-polyfill": "0.0.9", + "expose-loader": "0.7.3", + "extract-text-webpack-plugin": "2.1.0", + "file-loader": "0.11.1", + "html-loader": "0.4.5", + "isomorphic-fetch": "2.2.1", + "jquery": "3.2.1", + "json-loader": "0.5.4", + "preboot": "4.5.2", + "raw-loader": "0.5.1", + "reflect-metadata": "0.1.10", + "rxjs": "5.4.0", + "style-loader": "0.17.0", + "to-string-loader": "1.1.5", + "typescript": "2.3.2", + "url-loader": "0.5.8", + "webpack": "2.5.1", + "webpack-hot-middleware": "2.18.0", + "webpack-merge": "4.1.0", + "zone.js": "0.8.10" }, "devDependencies": { - "@types/chai": "^3.4.34", - "@types/jasmine": "^2.5.37", - "chai": "^3.5.0", - "jasmine-core": "^2.5.2", - "karma": "^1.3.0", - "karma-chai": "^0.1.0", - "karma-chrome-launcher": "^2.0.0", - "karma-cli": "^1.0.1", - "karma-jasmine": "^1.0.2", - "karma-webpack": "^1.8.0" + "@types/chai": "3.5.2", + "@types/jasmine": "2.5.47", + "chai": "3.5.0", + "jasmine-core": "2.6.1", + "karma": "1.7.0", + "karma-chai": "0.1.0", + "karma-chrome-launcher": "2.1.1", + "karma-cli": "1.0.1", + "karma-jasmine": "1.1.0", + "karma-webpack": "2.0.3" } } diff --git a/templates/Angular2Spa/webpack.config.vendor.js b/templates/Angular2Spa/webpack.config.vendor.js index df1cfa8..7b0c301 100644 --- a/templates/Angular2Spa/webpack.config.vendor.js +++ b/templates/Angular2Spa/webpack.config.vendor.js @@ -16,16 +16,15 @@ module.exports = (env) => { }, entry: { vendor: [ + '@angular/animations', '@angular/common', '@angular/compiler', '@angular/core', + '@angular/forms', '@angular/http', '@angular/platform-browser', '@angular/platform-browser-dynamic', '@angular/router', - '@angular/platform-server', - 'angular2-universal', - 'angular2-universal-polyfills', 'bootstrap', 'bootstrap/dist/css/bootstrap.css', 'es6-shim', @@ -43,6 +42,7 @@ module.exports = (env) => { plugins: [ new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' }), // Maps these identifiers to the jQuery package (because Bootstrap expects it to be a global variable) new webpack.ContextReplacementPlugin(/\@angular\b.*\b(bundles|linker)/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/11580 + new webpack.ContextReplacementPlugin(/angular(\\|\/)core(\\|\/)@angular/, path.join(__dirname, './ClientApp')), // Workaround for https://github.com/angular/angular/issues/14898 new webpack.IgnorePlugin(/^vertx$/) // Workaround for https://github.com/stefanpenner/es6-promise/issues/100 ] };