Skip to content
This repository has been archived by the owner on Apr 8, 2020. It is now read-only.

Update to Angular 4 (phase 1) #930

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 21 additions & 0 deletions 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';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make more sense to have sharedConfig be an NgModule itself that they import within imports[], since most Apps will have lots of NgModules lazy-loaded etc ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. How does that then work in terms of overriding the config in each of the server/client app modules? If you can post a code sample we can definitely consider this!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW I tried to do this, but hit a roadblock. If the shared NgModule includes the routing config (which of course you would want it to), then it has to be able to compile all the templates for all the components. But to be able to compile them, it needs to reference either BrowserModule or ServerModule (otherwise it fails with errors like ngIf is unknown). But it can't reference either of those, because it needs to work with both server and client.

So is there any reasonable way to do this? Please let me know if there is. It doesn't seem like a compiled NgModule can possibly be portable across both server and client, which is an unfortunate limitation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry just seeing this now! @SteveSandersonMS

Did you have boot-client & boot-server using browser-app.module & server-app.module ?

To get rid of the ngIf unknown errors, you just import CommonModule into your base app.module NgModule (https://github.com/MarkPieszak/aspnetcore-angular2-universal/blob/master/Client/app/app.module.ts#L51)

You can see in there I have the app.module importing the Routes, and all the base declarations etc that can be shared. Browser & Server-app.module's both import the app.module and they each declare the bootstrap: [ AppComponent ] themselves. You want to make sure app.module doesn't declare a bootstrap Component.


@NgModule({
bootstrap: sharedConfig.bootstrap,
declarations: sharedConfig.declarations,
imports: [
BrowserModule,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to make sure you have .withServerTransition({}) as this is how guids (such as those for components and styles) are synced between the server & client.

    BrowserModule.withServerTransition({
        appId: 'my-app-id' // make sure this matches with your Server NgModule
    }),

FormsModule,
HttpModule,
...sharedConfig.imports
],
providers: [
{ provide: 'ORIGIN_URL', useValue: location.origin }
]
})
export class AppModule {
}
14 changes: 14 additions & 0 deletions 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';

@NgModule({
bootstrap: sharedConfig.bootstrap,
declarations: sharedConfig.declarations,
imports: [
ServerModule,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want to make sure you have BrowserModule with serverTransition here as well (with the same appId)

    BrowserModule.withServerTransition({
        appId: 'my-app-id' // make sure this matches with your Server NgModule
    }),

...sharedConfig.imports
]
})
export class AppModule {
}
9 changes: 3 additions & 6 deletions templates/Angular2Spa/ClientApp/app/app.module.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,
Expand All @@ -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 },
Expand All @@ -26,6 +25,4 @@ import { CounterComponent } from './components/counter/counter.component';
{ path: '**', redirectTo: 'home' }
])
]
})
export class AppModule {
}
};
@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, Inject } from '@angular/core';
import { Http } from '@angular/http';

@Component({
Expand All @@ -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[];
});
}
Expand Down
27 changes: 10 additions & 17 deletions templates/Angular2Spa/ClientApp/boot-client.ts
@@ -1,29 +1,22 @@
import 'angular2-universal-polyfills/browser';
import 'reflect-metadata';
import 'zone.js';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could just have (I believe it's a bit smaller) import 'zone.js/dist/zone-node'; here instead, just an idea!

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);
52 changes: 27 additions & 25 deletions 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<RenderResult>((resolve, reject) => {
const requestZone = Zone.current.fork({
name: 'angular-universal request',
properties: {
baseUrl: '/',
requestUrl: params.url,
originUrl: params.origin,
preboot: false,
document: '<app></app>'
},
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: '<app></app>', url: params.url } },
{ provide: 'ORIGIN_URL', useValue: params.origin }
];

return platformDynamicServer(providers).bootstrapModule(AppModule).then(moduleRef => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The team has stressed to never use dynamicServer as JIT compilation just slows down server renders tremendously as you'd really only want ahead of time compiled ones with platformServer.

Just worried we might have Devs use this phase 1 template (and never update it) and get stuck using a non-ideal bootstrapModule serializer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point as for where we're ultimately heading, but right now people are already using the equivalent to dynamicServer with their Angular 2 sites. So this pull request is not making things any slower.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Just making sure :)

const appRef = moduleRef.injector.get(ApplicationRef);
const state = moduleRef.injector.get(PlatformState);
const zone = moduleRef.injector.get(NgZone);

return requestZone.run<Promise<string>>(() => platform.serializeModule(AppModule)).then(html => {
resolve({ html: html });
}, reject);
return new Promise<RenderResult>((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();
});
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could catch errors here in the subscription

});
});
});
3 changes: 2 additions & 1 deletion 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';
Expand Down
25 changes: 12 additions & 13 deletions templates/Angular2Spa/package.json
Expand Up @@ -5,21 +5,19 @@
"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",
"@angular/animations": "^4.0.0",
"@angular/common": "^4.0.0",
"@angular/compiler": "^4.0.0",
"@angular/core": "^4.0.0",
"@angular/forms": "^4.0.0",
"@angular/http": "^4.0.0",
"@angular/platform-browser": "^4.0.0",
"@angular/platform-browser-dynamic": "^4.0.0",
"@angular/platform-server": "^4.0.0",
"@angular/router": "^4.0.0",
"@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",
Expand All @@ -37,6 +35,7 @@
"json-loader": "^0.5.4",
"preboot": "^4.5.2",
"raw-loader": "^0.5.1",
"reflect-metadata": "^0.1.10",
"rxjs": "^5.0.1",
"style-loader": "^0.13.1",
"to-string-loader": "^1.1.5",
Expand All @@ -45,7 +44,7 @@
"webpack": "^2.2.0",
"webpack-hot-middleware": "^2.12.2",
"webpack-merge": "^0.14.1",
"zone.js": "^0.7.6"
"zone.js": "^0.8.4"
},
"devDependencies": {
"@types/chai": "^3.4.34",
Expand Down
6 changes: 3 additions & 3 deletions templates/Angular2Spa/webpack.config.vendor.js
Expand Up @@ -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',
Expand All @@ -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
]
};
Expand Down
2 changes: 1 addition & 1 deletion templates/package-builder/src/yeoman/package.json
@@ -1,6 +1,6 @@
{
"name": "generator-aspnetcore-spa",
"version": "0.9.1",
"version": "0.9.2-beta-0003",
"description": "Single-Page App templates for ASP.NET Core",
"author": "Microsoft",
"license": "Apache-2.0",
Expand Down