From 6e1a51ecd68fa6f3603df4c622b896b0716bd941 Mon Sep 17 00:00:00 2001 From: Michael McDonald Date: Fri, 14 Apr 2017 19:32:42 +0800 Subject: [PATCH 001/142] fix(aot): change build output to main-server.js instead of the default main.js, add missing import from zone.js (#177) Closes #176 --- Client/main.server.aot.ts | 3 ++- webpack.config.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Client/main.server.aot.ts b/Client/main.server.aot.ts index 00744e56..114d9297 100644 --- a/Client/main.server.aot.ts +++ b/Client/main.server.aot.ts @@ -1,4 +1,5 @@ -import './polyfills/server.polyfills'; +import 'zone.js/dist/zone-node'; +import './polyfills/server.polyfills'; import { enableProdMode } from '@angular/core'; import { INITIAL_CONFIG } from '@angular/platform-server'; import { APP_BASE_HREF } from '@angular/common'; diff --git a/webpack.config.js b/webpack.config.js index d5832768..8ec7d827 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,7 +15,7 @@ module.exports = function (options, webpackOptions) { } const serverConfig = webpackMerge({}, commonPartial, serverPartial, { - entry: options.aot ? './client/main.server.aot.ts' : serverPartial.entry, // Temporary + entry: options.aot ? { 'main-server' : './client/main.server.aot.ts' } : serverPartial.entry, // Temporary plugins: [ getAotPlugin('server', !!options.aot) ] From 0527a288b871af49406cb7026e5dd3c29e25efef Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 14 Apr 2017 10:44:40 -0400 Subject: [PATCH 002/142] add http-transfer mechanism (#179) Closes #169 Closes #170 --- Client/app/app.component.ts | 8 +- Client/app/app.module.ts | 6 +- Client/app/browser-app.module.ts | 16 +- .../app/containers/users/users.component.ts | 36 +- Client/app/server-app.module.ts | 39 +- Client/app/shared/constants/request.ts | 3 + Client/main.server.aot.ts | 39 +- Client/main.server.ts | 42 +- .../transfer-http/transfer-http.module.ts | 10 + Client/modules/transfer-http/transfer-http.ts | 152 +++++++ .../browser-transfer-state.module.ts | 20 + .../server-transfer-state.module.ts | 12 + .../transfer-state/server-transfer-state.ts | 36 ++ .../modules/transfer-state/transfer-state.ts | 40 ++ Client/polyfills/polyfills.ts | 26 +- Client/polyfills/server.polyfills.ts | 2 +- .../polyfills/temporary-aspnetcore-engine.ts | 412 +++++++++--------- Server/Controllers/HomeController.cs | 51 ++- Views/Home/Index.cshtml | 1 + Views/Shared/_Layout.cshtml | 5 +- boot-tests.ts | 1 - 21 files changed, 664 insertions(+), 293 deletions(-) create mode 100644 Client/app/shared/constants/request.ts create mode 100644 Client/modules/transfer-http/transfer-http.module.ts create mode 100644 Client/modules/transfer-http/transfer-http.ts create mode 100644 Client/modules/transfer-state/browser-transfer-state.module.ts create mode 100644 Client/modules/transfer-state/server-transfer-state.module.ts create mode 100644 Client/modules/transfer-state/server-transfer-state.ts create mode 100644 Client/modules/transfer-state/transfer-state.ts diff --git a/Client/app/app.component.ts b/Client/app/app.component.ts index ba975f94..02b91787 100644 --- a/Client/app/app.component.ts +++ b/Client/app/app.component.ts @@ -7,6 +7,7 @@ import { LinkService } from './shared/link.service'; // i18n support import { TranslateService } from '@ngx-translate/core'; +import { REQUEST } from './shared/constants/request'; @Component({ selector: 'app', @@ -29,13 +30,18 @@ export class AppComponent implements OnInit, OnDestroy { private title: Title, private meta: Meta, private linkService: LinkService, - public translate: TranslateService + public translate: TranslateService, + @Inject(REQUEST) private request ) { // this language will be used as a fallback when a translation isn't found in the current language translate.setDefaultLang('en'); // the lang to use, if the lang isn't available, it will use the current loader to get them translate.use('en'); + + console.log(`What's our REQUEST Object look like?`); + console.log(`The Request object only really exists on the Server, but on the Browser we can at least see Cookies`); + console.log(this.request); } ngOnInit() { diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index f14d3879..cbf93f75 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -21,9 +21,9 @@ import { Ng2BootstrapComponent } from './containers/ng2-bootstrap-demo/ng2bootst import { LinkService } from './shared/link.service'; import { ConnectionResolver } from './shared/route.resolver'; import { ORIGIN_URL } from './shared/constants/baseurl.constants'; +import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module'; export function createTranslateLoader(http: Http, baseHref) { - console.log('\n\n\n New method? ' + baseHref); // Temporary Azure hack if (baseHref === null && typeof window !== 'undefined') { baseHref = window.location.origin; @@ -41,13 +41,15 @@ export function createTranslateLoader(http: Http, baseHref) { HomeComponent, ChatComponent, Ng2BootstrapComponent - ], + ], imports: [ CommonModule, HttpModule, FormsModule, Ng2BootstrapModule.forRoot(), // You could also split this up if you don't want the Entire Module imported + TransferHttpModule, // Our Http TransferData method + // i18n support TranslateModule.forRoot({ loader: { diff --git a/Client/app/browser-app.module.ts b/Client/app/browser-app.module.ts index 5d6f98bd..0150d22c 100644 --- a/Client/app/browser-app.module.ts +++ b/Client/app/browser-app.module.ts @@ -8,6 +8,8 @@ import { SignalRModule, SignalRConfiguration } from 'ng2-signalr'; import { ORIGIN_URL } from './shared/constants/baseurl.constants'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; +import { REQUEST } from './shared/constants/request'; +import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; export function createConfig(): SignalRConfiguration { const signalRConfig = new SignalRConfiguration(); @@ -24,6 +26,11 @@ export function getOriginUrl() { return window.location.origin; } +export function getRequest() { + // the Request object only lives on the server + return { cookie: document.cookie }; +} + @NgModule({ bootstrap: [AppComponent], imports: [ @@ -31,6 +38,8 @@ export function getOriginUrl() { appId: 'my-app-id' // make sure this matches with your Server NgModule }), BrowserAnimationsModule, + BrowserTransferStateModule, + // Our Common AppModule AppModule, @@ -38,9 +47,14 @@ export function getOriginUrl() { ], providers: [ { - // We need this for our Http calls since they'll be using APP_BASE_HREF (since the Server requires Absolute URLs) + // We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server + // (Also remember the Server requires Absolute URLs) provide: ORIGIN_URL, useFactory: (getOriginUrl) + }, { + // The server provides these in main.server + provide: REQUEST, + useFactory: (getRequest) } ] }) diff --git a/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts index 740a1301..555a6947 100644 --- a/Client/app/containers/users/users.component.ts +++ b/Client/app/containers/users/users.component.ts @@ -7,6 +7,7 @@ import { APP_BASE_HREF } from '@angular/common'; import { Http, URLSearchParams } from '@angular/http'; import { ORIGIN_URL } from '../../shared/constants/baseurl.constants'; +import { TransferHttp } from '../../../modules/transfer-http/transfer-http'; @Component({ selector: 'fetchdata', @@ -33,16 +34,24 @@ export class UsersComponent implements OnInit { // Use "constructor"s only for dependency injection constructor( - private http: Http, + private transferHttp: TransferHttp, // Use only for GETS that you want re-used between Server render -> Client render + private http: Http, // Use for everything else @Inject(ORIGIN_URL) private baseUrl: string ) { } // Here you want to handle anything with @Input()'s @Output()'s // Data retrieval / etc - this is when the Component is "ready" and wired up ngOnInit() { - this.newUserName = ""; - this.http.get(`${this.baseUrl}/api/user/all`).map(res => res.json()).subscribe(result => { - console.log(result); + + this.newUserName = ''; + + // ** TransferHttp example / concept ** + // - Here we make an Http call on the server, save the result on the window object and pass it down with the SSR, + // The Client then re-uses this Http result instead of hitting the server again! + + // NOTE : transferHttp also automatically does .map(res => res.json()) for you, so no need for these calls + this.transferHttp.get(`${this.baseUrl}/api/user/all`).subscribe(result => { + console.log('TransferHttp [GET] /api/user/allresult', result); this.users = result as IUser[]; }); } @@ -52,9 +61,8 @@ export class UsersComponent implements OnInit { if (result.ok) { let position = this.users.indexOf(user); this.users.splice(position, 1); - } - else { - alert("There was an issue, Could not delete user"); + } else { + alert('There was an issue, Could not delete user'); } }); } @@ -65,8 +73,9 @@ export class UsersComponent implements OnInit { urlSearchParams.append('name', user.name); this.http.put(`${this.baseUrl}/api/user/update`, urlSearchParams).subscribe(result => { - if (!result.ok) { - alert("There was an issue, Could not edit user"); + console.log('result: ', result); + if (!result) { + alert('There was an issue, Could not edit user'); } }); } @@ -76,12 +85,11 @@ export class UsersComponent implements OnInit { urlSearchParams.append('name', newUserName); this.http.post(`${this.baseUrl}/api/user/insert`, urlSearchParams).subscribe(result => { - if (result.ok) { + if (result) { this.users.push(result.json()); - this.newUserName = ""; - } - else { - alert("There was an issue, Could not edit user"); + this.newUserName = ''; + } else { + alert('There was an issue, Could not edit user'); } }); } diff --git a/Client/app/server-app.module.ts b/Client/app/server-app.module.ts index 98ceda56..ac91990a 100644 --- a/Client/app/server-app.module.ts +++ b/Client/app/server-app.module.ts @@ -1,31 +1,34 @@ import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { BrowserModule } from '@angular/platform-browser'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; - +import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module'; +import { TransferState } from '../modules/transfer-state/transfer-state'; @NgModule({ - bootstrap: [ AppComponent ], - imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Browser NgModule - }), - ServerModule, - NoopAnimationsModule, - - // Our Common AppModule - AppModule - ] + bootstrap: [AppComponent], + imports: [ + BrowserModule.withServerTransition({ + appId: 'my-app-id' // make sure this matches with your Browser NgModule + }), + ServerModule, + NoopAnimationsModule, + + ServerTransferStateModule, + + // Our Common AppModule + AppModule + ] }) export class ServerAppModule { - // constructor(private transferState: TransferState) { } + constructor(private transferState: TransferState) { } - // // Gotcha - // ngOnBootstrap = () => { - // this.transferState.inject(); - // } + // Gotcha (needs to be an arrow function) + ngOnBootstrap = () => { + this.transferState.inject(); + } } diff --git a/Client/app/shared/constants/request.ts b/Client/app/shared/constants/request.ts new file mode 100644 index 00000000..4c553d8a --- /dev/null +++ b/Client/app/shared/constants/request.ts @@ -0,0 +1,3 @@ +import { InjectionToken } from '@angular/core'; + +export const REQUEST = new InjectionToken('REQUEST'); diff --git a/Client/main.server.aot.ts b/Client/main.server.aot.ts index 114d9297..eb4f0699 100644 --- a/Client/main.server.aot.ts +++ b/Client/main.server.aot.ts @@ -9,32 +9,33 @@ import { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; // Grab the (Node) server-specific NgModule import { ServerAppModuleNgFactory } from './ngfactory/app/server-app.module.ngfactory'; // Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngineAoT } from './polyfills/temporary-aspnetcore-engine'; +import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; enableProdMode(); export default createServerRenderer(params => { - // Platform-server provider configuration - const providers = [ - { - provide: INITIAL_CONFIG, - useValue: { - document: '', // Our Root application document - url: params.url - } - }, { - provide: ORIGIN_URL, - useValue: params.origin - } - ]; + // Platform-server provider configuration + const setupOptions: IEngineOptions = { + appSelector: '', + ngModule: ServerAppModuleNgFactory, + request: params, + providers: [ + // Optional - Any other Server providers you want to pass (remember you'll have to provide them for the Browser as well) + ] + }; - return ngAspnetCoreEngineAoT(providers, ServerAppModuleNgFactory).then(response => { - return ({ - html: response.html, - globals: response.globals - }); + return ngAspnetCoreEngine(setupOptions).then(response => { + // Apply your transferData to response.globals + response.globals.transferData = createTransferScript({ + someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object' }); + + return ({ + html: response.html, + globals: response.globals + }); + }); }); /* -------- THIS FILE IS TEMPORARY and will be gone when @ngtools/webpack can handle dual files (w server) ---------- */ diff --git a/Client/main.server.ts b/Client/main.server.ts index dc8fe720..e20e4f9f 100644 --- a/Client/main.server.ts +++ b/Client/main.server.ts @@ -9,30 +9,32 @@ import { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; // Grab the (Node) server-specific NgModule import { ServerAppModule } from './app/server-app.module'; // Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngine } from './polyfills/temporary-aspnetcore-engine'; +import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; enableProdMode(); -export default createServerRenderer(params => { +export default createServerRenderer((params: BootFuncParams) => { - // Platform-server provider configuration - const providers = [ - { - provide: INITIAL_CONFIG, - useValue: { - document: '', // Our Root application document - url: params.url - } - }, { - provide: ORIGIN_URL, - useValue: params.origin - } - ]; + // Platform-server provider configuration + const setupOptions: IEngineOptions = { + appSelector: '', + ngModule: ServerAppModule, + request: params, + providers: [ + // Optional - Any other Server providers you want to pass (remember you'll have to provide them for the Browser as well) + ] + }; - return ngAspnetCoreEngine(providers, ServerAppModule).then(response => { - return ({ - html: response.html, - globals: response.globals - }); + return ngAspnetCoreEngine(setupOptions).then(response => { + // Apply your transferData to response.globals + response.globals.transferData = createTransferScript({ + someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object', + fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController }); + + return ({ + html: response.html, + globals: response.globals + }); + }); }); diff --git a/Client/modules/transfer-http/transfer-http.module.ts b/Client/modules/transfer-http/transfer-http.module.ts new file mode 100644 index 00000000..c2875b33 --- /dev/null +++ b/Client/modules/transfer-http/transfer-http.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { Http, HttpModule } from '@angular/http'; +import { TransferHttp } from './transfer-http'; + +@NgModule({ + providers: [ + TransferHttp + ] +}) +export class TransferHttpModule {} diff --git a/Client/modules/transfer-http/transfer-http.ts b/Client/modules/transfer-http/transfer-http.ts new file mode 100644 index 00000000..e8667b58 --- /dev/null +++ b/Client/modules/transfer-http/transfer-http.ts @@ -0,0 +1,152 @@ +import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; +import { ConnectionBackend, Http, Request, RequestOptions, RequestOptionsArgs, Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import { Subject } from 'rxjs/Subject'; +import { TransferState } from '../transfer-state/transfer-state'; +import { isPlatformServer } from '@angular/common'; + +import 'rxjs/add/operator/map'; +import 'rxjs/add/operator/do'; +import 'rxjs/add/observable/fromPromise'; + +@Injectable() +export class TransferHttp { + + private isServer = isPlatformServer(this.platformId); + + constructor( + @Inject(PLATFORM_ID) private platformId, + private http: Http, + protected transferState: TransferState + ) { } + + request(uri: string | Request, options?: RequestOptionsArgs): Observable { + return this.getData(uri, options, (url: string, options: RequestOptionsArgs) => { + return this.http.request(url, options); + }); + } + /** + * Performs a request with `get` http method. + */ + get(url: string, options?: RequestOptionsArgs): Observable { + return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { + return this.http.get(url, options); + }); + } + /** + * Performs a request with `post` http method. + */ + post(url: string, body: any, options?: RequestOptionsArgs): Observable { + return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { + return this.http.post(url, body, options); + }); + } + /** + * Performs a request with `put` http method. + */ + put(url: string, body: any, options?: RequestOptionsArgs): Observable { + + return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { + return this.http.put(url, body, options); + }); + } + /** + * Performs a request with `delete` http method. + */ + delete(url: string, options?: RequestOptionsArgs): Observable { + return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { + return this.http.delete(url, options); + }); + } + /** + * Performs a request with `patch` http method. + */ + patch(url: string, body: any, options?: RequestOptionsArgs): Observable { + return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { + return this.http.patch(url, body.options); + }); + } + /** + * Performs a request with `head` http method. + */ + head(url: string, options?: RequestOptionsArgs): Observable { + return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { + return this.http.head(url, options); + }); + } + /** + * Performs a request with `options` http method. + */ + options(url: string, options?: RequestOptionsArgs): Observable { + return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { + return this.http.options(url, options); + }); + } + + private getData(uri: string | Request, options: RequestOptionsArgs, callback: (uri: string | Request, options?: RequestOptionsArgs) => Observable) { + + let url = uri; + + if (typeof uri !== 'string') { + url = uri.url; + } + + const key = url + JSON.stringify(options); + + try { + return this.resolveData(key); + + } catch (e) { + return callback(url, options) + .map(res => res.json()) + .do(data => { + if (this.isServer) { + this.setCache(key, data); + } + }); + } + } + + private getPostData(uri: string | Request, body: any, options: RequestOptionsArgs, callback: (uri: string | Request, body: any, options?: RequestOptionsArgs) => Observable) { + + let url = uri; + + if (typeof uri !== 'string') { + url = uri.url; + } + + const key = url + JSON.stringify(body); + + try { + + return this.resolveData(key); + + } catch (e) { + return callback(uri, body, options) + .map(res => res.json()) + .do(data => { + if (this.isServer) { + this.setCache(key, data); + } + }); + } + } + + private resolveData(key: string) { + const data = this.getFromCache(key); + + if (!data) { + throw new Error(); + } + + return Observable.fromPromise(Promise.resolve(data)); + } + + private setCache(key, data) { + return this.transferState.set(key, data); + } + + private getFromCache(key): any { + return this.transferState.get(key); + } +} diff --git a/Client/modules/transfer-state/browser-transfer-state.module.ts b/Client/modules/transfer-state/browser-transfer-state.module.ts new file mode 100644 index 00000000..20e11421 --- /dev/null +++ b/Client/modules/transfer-state/browser-transfer-state.module.ts @@ -0,0 +1,20 @@ +import { NgModule, PLATFORM_ID } from '@angular/core'; +import { TransferState } from './transfer-state'; + +export function getTransferState(): TransferState { + const transferState = new TransferState(); + transferState.initialize(window['TRANSFER_STATE'] || {}); + return transferState; +} + +@NgModule({ + providers: [ + { + provide: TransferState, + useFactory: getTransferState + } + ] +}) +export class BrowserTransferStateModule { + +} diff --git a/Client/modules/transfer-state/server-transfer-state.module.ts b/Client/modules/transfer-state/server-transfer-state.module.ts new file mode 100644 index 00000000..1a77f653 --- /dev/null +++ b/Client/modules/transfer-state/server-transfer-state.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { ServerTransferState } from './server-transfer-state'; +import { TransferState } from './transfer-state'; + +@NgModule({ + providers: [ + { provide: TransferState, useClass: ServerTransferState } + ] +}) +export class ServerTransferStateModule { + +} diff --git a/Client/modules/transfer-state/server-transfer-state.ts b/Client/modules/transfer-state/server-transfer-state.ts new file mode 100644 index 00000000..b2890b26 --- /dev/null +++ b/Client/modules/transfer-state/server-transfer-state.ts @@ -0,0 +1,36 @@ +import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject, PLATFORM_ID } from '@angular/core'; +import { TransferState } from './transfer-state'; +import { PlatformState } from '@angular/platform-server'; +@Injectable() +export class ServerTransferState extends TransferState { + constructor(private state: PlatformState, private rendererFactory: RendererFactory2) { + super(); + } + + /** + * Inject the State into the bottom of the + */ + inject() { + try { + const document: any = this.state.getDocument(); + const transferStateString = JSON.stringify(this.toJson()); + const renderer = this.rendererFactory.createRenderer(document, { + id: '-1', + encapsulation: ViewEncapsulation.None, + styles: [], + data: {} + }); + + const body = document.body; + + const script = renderer.createElement('script'); + renderer.setValue(script, `window['TRANSFER_STATE'] = ${transferStateString}`); + renderer.appendChild(body, script); + } catch (e) { + console.log('Failed to append TRANSFER_STATE to body'); + console.error(e); + } + } + + +} diff --git a/Client/modules/transfer-state/transfer-state.ts b/Client/modules/transfer-state/transfer-state.ts new file mode 100644 index 00000000..cc963b8f --- /dev/null +++ b/Client/modules/transfer-state/transfer-state.ts @@ -0,0 +1,40 @@ +import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; + +@Injectable() +export class TransferState { + private _map = new Map(); + + constructor() { } + + keys() { + return this._map.keys(); + } + + get(key: string): any { + const cachedValue = this._map.get(key); + this._map.delete(key); + return cachedValue; + } + + set(key: string, value: any): Map { + return this._map.set(key, value); + } + + toJson(): any { + const obj = {}; + Array.from(this.keys()) + .forEach(key => { + obj[key] = this.get(key); + }); + return obj; + } + + initialize(obj: any): void { + Object.keys(obj) + .forEach(key => { + this.set(key, obj[key]); + }); + } + + inject(): void { } +} diff --git a/Client/polyfills/polyfills.ts b/Client/polyfills/polyfills.ts index dc7cea9f..e2e900bd 100644 --- a/Client/polyfills/polyfills.ts +++ b/Client/polyfills/polyfills.ts @@ -4,19 +4,19 @@ */ /** IE9, IE10 and IE11 requires all of the following polyfills. **/ -//import 'core-js/es6/symbol'; -//import 'core-js/es6/object'; -//import 'core-js/es6/function'; -//import 'core-js/es6/parse-int'; -//import 'core-js/es6/parse-float'; -//import 'core-js/es6/number'; -//import 'core-js/es6/math'; -//import 'core-js/es6/string'; -//import 'core-js/es6/date'; -//import 'core-js/es6/array'; -//import 'core-js/es6/regexp'; -//import 'core-js/es6/map'; -//import 'core-js/es6/set'; +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/set'; /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. diff --git a/Client/polyfills/server.polyfills.ts b/Client/polyfills/server.polyfills.ts index 87aef3d1..7d3dc3bf 100644 --- a/Client/polyfills/server.polyfills.ts +++ b/Client/polyfills/server.polyfills.ts @@ -1,4 +1,4 @@ import './polyfills.ts'; import 'reflect-metadata'; -//import 'zone.js'; +import 'zone.js'; diff --git a/Client/polyfills/temporary-aspnetcore-engine.ts b/Client/polyfills/temporary-aspnetcore-engine.ts index e5a7c2fa..d9b19b95 100644 --- a/Client/polyfills/temporary-aspnetcore-engine.ts +++ b/Client/polyfills/temporary-aspnetcore-engine.ts @@ -1,220 +1,240 @@ - -/* ********* TEMPORARILY HERE ************** +/* ********* TEMPORARILY HERE ************** * - will be on npm soon - * import { ngAspnetCoreEngine } from `@ng-universal/ng-aspnetcore-engine`; */ +import { Type, NgModuleFactory, NgModuleRef, ApplicationRef, Provider, CompilerFactory, Compiler } from '@angular/core'; +import { platformServer, platformDynamicServer, PlatformState, INITIAL_CONFIG, renderModuleFactory } from '@angular/platform-server'; +import { ResourceLoader } from '@angular/compiler'; +import * as fs from 'fs'; -import { Type, NgModuleFactory, NgModuleRef, ApplicationRef, Provider } from '@angular/core'; -import { platformServer, platformDynamicServer, PlatformState } from '@angular/platform-server'; +import { REQUEST } from '../app/shared/constants/request'; +import { ORIGIN_URL } from '../app/shared/constants/baseurl.constants'; -export function ngAspnetCoreEngine( - providers: Provider[], - ngModule: Type<{}> -): Promise<{ html: string, globals: { styles: string, title: string, meta: string, [key: string]: any } }> { - - return new Promise((resolve, reject) => { - - const platform = platformDynamicServer(providers); - - return platform.bootstrapModule(>ngModule).then((moduleRef: NgModuleRef<{}>) => { - - const state: PlatformState = moduleRef.injector.get(PlatformState); - const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); +// import { FileLoader } from './file-loader'; - appRef.isStable - .filter((isStable: boolean) => isStable) - .first() - .subscribe((stable) => { - - // Fire the TransferCache - const bootstrap = moduleRef.instance['ngOnBootstrap']; - bootstrap && bootstrap(); - - // The parse5 Document itself - const AST_DOCUMENT = state.getDocument(); - - // Strip out the Angular application - const htmlDoc = state.renderToString(); - - const APP_HTML = htmlDoc.substring( - htmlDoc.indexOf('') + 6, - htmlDoc.indexOf('') - ); - - // Strip out Styles / Meta-tags / Title - const STYLES = []; - const META = []; - const LINKS = []; - let TITLE = ''; - - const STYLES_STRING = htmlDoc.substring( - htmlDoc.indexOf('`; - // STYLES.push(styleTag); - // } - - if (element.name === 'meta') { - count = count + 1; - let metaString = '\n`); - } - - if (element.name === 'link') { - let linkString = '\n`); - } - } - - resolve({ - html: APP_HTML, - globals: { - styles: STYLES_STRING, - title: TITLE, - meta: META.join(' '), - links: LINKS.join(' ') - } - }); - - moduleRef.destroy(); +export function createTransferScript(transferData: Object): string { + return ``; +} - }); - }).catch(err => { - reject(err); - }); +export class FileLoader implements ResourceLoader { + get(url: string): Promise { + return new Promise((resolve, reject) => { + fs.readFile(url, (err: NodeJS.ErrnoException, buffer: Buffer) => { + if (err) { + return reject(err); + } + resolve(buffer.toString()); + }); }); + } } -// Temporary - these will be combined in what will be the official npm version in `angular/universal` repo - -export function ngAspnetCoreEngineAoT( - providers: Provider[], - ngModule: NgModuleFactory<{}> -): Promise<{ html: string, globals: { styles: string, title: string, meta: string, [key: string]: any } }> { - - return new Promise((resolve, reject) => { +export interface IRequestParams { + location: any; // e.g., Location object containing information '/some/path' + origin: string; // e.g., 'https://example.com:1234/' + url: string; // e.g., '/some/path' + baseUrl: string; // e.g., '' or '/myVirtualDir' + absoluteUrl: string; // e.g., 'https://example.com:1234/some/path' + domainTasks: Promise; + data: any; // any custom object passed through from .NET +} - const platform = platformServer(providers); +export interface IEngineOptions { + appSelector: string; + request: IRequestParams; + ngModule: Type<{}> | NgModuleFactory<{}>; + providers?: Provider[]; +}; - return platform.bootstrapModuleFactory(ngModule).then((moduleRef: NgModuleRef<{}>) => { +export function ngAspnetCoreEngine( + options: IEngineOptions +): Promise<{ html: string, globals: { styles: string, title: string, meta: string, transferData?: {}, [key: string]: any } }> { + + options.providers = options.providers || []; + + const compilerFactory: CompilerFactory = platformDynamicServer().injector.get(CompilerFactory); + const compiler: Compiler = compilerFactory.createCompiler([ + { + providers: [ + { provide: ResourceLoader, useClass: FileLoader } + ] + } + ]); + + return new Promise((resolve, reject) => { + + try { + const moduleOrFactory = options.ngModule; + if (!moduleOrFactory) { + throw new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped'); + } + + const extraProviders = options.providers.concat( + options.providers, + [ + { + provide: INITIAL_CONFIG, + useValue: { + document: options.appSelector, + url: options.request.url + } + }, + { + provide: ORIGIN_URL, + useValue: options.request.origin + }, { + provide: REQUEST, + useValue: options.request.data.request + } + ] + ); + + const platform = platformServer(extraProviders); + + getFactory(moduleOrFactory, compiler) + .then((factory: NgModuleFactory<{}>) => { + + return platform.bootstrapModuleFactory(factory).then((moduleRef: NgModuleRef<{}>) => { const state: PlatformState = moduleRef.injector.get(PlatformState); const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); appRef.isStable - .filter((isStable: boolean) => isStable) - .first() - .subscribe((stable) => { - - // Fire the TransferCache - const bootstrap = moduleRef.instance['ngOnBootstrap']; - bootstrap && bootstrap(); - - // The parse5 Document itself - const AST_DOCUMENT = state.getDocument(); - - // Strip out the Angular application - const htmlDoc = state.renderToString(); - - const APP_HTML = htmlDoc.substring( - htmlDoc.indexOf('') + 6, - htmlDoc.indexOf('') - ); - - // Strip out Styles / Meta-tags / Title - const STYLES = []; - const META = []; - const LINKS = []; - let TITLE = ''; - - const STYLES_STRING = htmlDoc.substring( - htmlDoc.indexOf('`; - // STYLES.push(styleTag); - // } - - if (element.name === 'meta') { - count = count + 1; - let metaString = '\n`); - } - - if (element.name === 'link') { - let linkString = '\n`); - } + .filter((isStable: boolean) => isStable) + .first() + .subscribe((stable) => { + + // Fire the TransferState Cache + const bootstrap = moduleRef.instance['ngOnBootstrap']; + bootstrap && bootstrap(); + + // The parse5 Document itself + const AST_DOCUMENT = state.getDocument(); + + // Strip out the Angular application + const htmlDoc = state.renderToString(); + + const APP_HTML = htmlDoc.substring( + htmlDoc.indexOf('') + 6, + htmlDoc.indexOf('') + ); + + // Strip out Styles / Meta-tags / Title + const STYLES = []; + const META = []; + const LINKS = []; + let TITLE = ''; + + let STYLES_STRING = htmlDoc.substring( + htmlDoc.indexOf('`; + // STYLES.push(styleTag); + // } + + if (element.name === 'meta') { + count = count + 1; + let metaString = '\n`); + } + + if (element.name === 'link') { + let linkString = '\n`); + } + } + + resolve({ + html: APP_HTML, + globals: { + styles: STYLES_STRING, + title: TITLE, + meta: META.join(' '), + links: LINKS.join(' ') + } + }); - resolve({ - html: APP_HTML, - globals: { - styles: STYLES_STRING, - title: TITLE, - meta: META.join(' '), - links: LINKS.join(' ') - } - }); + moduleRef.destroy(); - moduleRef.destroy(); + }, (err) => { + reject(err); + }); - }); - }).catch(err => { - reject(err); + }); }); - }); -} \ No newline at end of file + } catch (ex) { + reject(ex); + } + + }); +} + +/* ********************** Private / Internal ****************** */ + +const factoryCacheMap = new Map, NgModuleFactory<{}>>(); +function getFactory( + moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler +): Promise> { + return new Promise>((resolve, reject) => { + // If module has been compiled AoT + if (moduleOrFactory instanceof NgModuleFactory) { + console.log('Already AoT?'); + resolve(moduleOrFactory); + return; + } else { + let moduleFactory = factoryCacheMap.get(moduleOrFactory); + + // If module factory is cached + if (moduleFactory) { + console.log('\n\n\n WE FOUND ONE!! USE IT!!\n\n\n'); + resolve(moduleFactory); + return; + } + + // Compile the module and cache it + compiler.compileModuleAsync(moduleOrFactory) + .then((factory) => { + console.log('\n\n\n\n MAP THIS THING!!!!\n\n\n '); + factoryCacheMap.set(moduleOrFactory, factory); + resolve(factory); + }, (err => { + reject(err); + })); + } + }); +} diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 8480e40f..3feb71ce 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -6,6 +6,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http; +using System.Diagnostics; +using System; namespace AspCoreServer.Controllers { @@ -21,6 +24,15 @@ public async Task Index() var unencodedPathAndQuery = requestFeature.RawTarget; var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + + // By default we're passing down Cookies, Headers, Host from the Request object here + TransferData transferData = new TransferData(); + transferData.request = AbstractHttpContextRequestInfo(Request); + transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; + // Add more customData here, add it to the TransferData class + // Prerender / Serialize application (with Universal) var prerenderResult = await Prerenderer.RenderToString( "/", @@ -28,16 +40,17 @@ public async Task Index() new JavaScriptModuleExport(applicationBasePath + "/Client/dist/main-server"), unencodedAbsoluteUrl, unencodedPathAndQuery, - null, + transferData, // Our simplified Request object & any other CustommData you want to send! 30000, Request.PathBase.ToString() ); - ViewData["SpaHtml"] = prerenderResult.Html; - ViewData["Title"] = prerenderResult.Globals["title"]; - ViewData["Styles"] = prerenderResult.Globals["styles"]; - ViewData["Meta"] = prerenderResult.Globals["meta"]; - ViewData["Links"] = prerenderResult.Globals["links"]; + ViewData["SpaHtml"] = prerenderResult.Html; // our from Angular + ViewData["Title"] = prerenderResult.Globals["title"]; // set our from Angular + ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place + ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags + ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags + ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; return View(); } @@ -46,5 +59,31 @@ public IActionResult Error() { return View(); } + + private IRequest AbstractHttpContextRequestInfo(HttpRequest request) + { + + IRequest requestSimplified = new IRequest(); + requestSimplified.cookies = request.Cookies; + requestSimplified.headers = request.Headers; + requestSimplified.host = request.Host; + + return requestSimplified; + } + } + + public class IRequest + { + public object cookies { get; set; } + public object headers { get; set; } + public object host { get; set; } + } + + public class TransferData + { + public dynamic request { get; set; } + + // Your data here ? + public object thisCameFromDotNET { get; set; } } } diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index c37ece34..418bb1fd 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,5 +1,6 @@ @Html.Raw(ViewData["SpaHtml"]) @section scripts { + <!-- Our webpack bundle --> <script src="http://github.com/~/dist/main-browser.js" asp-append-version="true"></script> } diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index c611e38f..8d8fb071 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -8,7 +8,7 @@ <meta name="viewport" content="width=device-width, initial-scale=1.0" /> @Html.Raw(ViewData["Meta"]) @Html.Raw(ViewData["Links"]) - + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/flag-icon-css/0.8.2/css/flag-icon.min.css" /> @Html.Raw(ViewData["Styles"]) @@ -24,6 +24,9 @@ <script src="http://ajax.aspnetcdn.com/ajax/signalr/jquery.signalr-2.2.0.min.js"></script> + <!-- Here we're passing down any data to be used by grabbed and parsed by Angular --> + @Html.Raw(ViewData["TransferData"]) + @RenderSection("scripts", required: false) </body> </html> diff --git a/boot-tests.ts b/boot-tests.ts index ff89f596..7940a29d 100644 --- a/boot-tests.ts +++ b/boot-tests.ts @@ -1,7 +1,6 @@ // Load required polyfills and testing libraries import '../polyfills/server.polyfills'; -import 'angular2-universal-polyfills'; import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test'; From 0ff9afc4c444760742dc34268e58abe928a60879 Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Sun, 16 Apr 2017 18:46:37 +0100 Subject: [PATCH 003/142] Removed no longer used API (#180) --- Server/RestAPI/TestController.cs | 56 -------------------------------- 1 file changed, 56 deletions(-) delete mode 100644 Server/RestAPI/TestController.cs diff --git a/Server/RestAPI/TestController.cs b/Server/RestAPI/TestController.cs deleted file mode 100644 index 5d732231..00000000 --- a/Server/RestAPI/TestController.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.IO; - -namespace AspCoreServer.Controllers -{ - [Route("api/[controller]")] - public class TestController : Controller - { - /* - private IHubContext _hub; - private ILogger<TestController> _logger; - private string uploadDirectory; - - public TestController(ILogger<TestController> logger, IHostingEnvironment environment, IConnectionManager connectionManager) //, IHostingEnvironment environment - { - _hub = connectionManager.GetHubContext<ChatHub>(); - _logger = logger; - IHostingEnvironment _environment = environment; - var location = System.Reflection.Assembly.GetEntryAssembly().Location; - uploadDirectory = _environment.WebRootPath + $@"/{"uploads"}"; - Directory.CreateDirectory(uploadDirectory); //Should be in startup - } - */ - - private static string[] Names = new[] - { - "Mark Pieszak", "Angular mcAngular", "Redux-man", "Nintendo" - }; - - [HttpGet("[action]")] - public IEnumerable<SampleData> Users() - { - var random = new Random(); - - //Calling a hub function - //_hub.Clients.All.Send("REST Working"); - - return Enumerable.Range(1, 5).Select(index => new SampleData - { - ID = random.Next(0, 2000), - Name = Names[random.Next(Names.Length)] - }); - } - - public class SampleData - { - public int ID { get; set; } - public string Name { get; set; } - } - } -} From 906af4f28ab263aa96250c48a33492f87879c0ba Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Sun, 16 Apr 2017 14:06:44 -0400 Subject: [PATCH 004/142] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f433417..0782d572 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - **Productivity** - Typescript 2 - - Codelyzer (for Real-Sime static code analysis) + - Codelyzer (for Real-time static code analysis) - VSCode & Atom provide real-time analysis out of the box. - **NOTE**: Does not fully work with Visual Studio yet. (Even with VS2017 and .NET core 1.0) From b64fe65d7676d78dee3f72833245dac224a6aa4d Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Sun, 16 Apr 2017 20:01:09 +0100 Subject: [PATCH 005/142] Web API improvements (#182) * Removed no longer used API * Pluralised API to "Users" and depluralised model to "User". All UsersAPI's now point to same URI. * Updated web API status codes --- .../app/containers/users/users.component.ts | 22 ++-- Server/Data/DbInitializer.cs | 26 ++-- Server/Data/SpaDbContext.cs | 2 +- Server/Models/{Users.cs => User.cs} | 4 +- Server/RestAPI/UserController.cs | 105 --------------- Server/RestAPI/UsersController.cs | 123 ++++++++++++++++++ 6 files changed, 148 insertions(+), 134 deletions(-) rename Server/Models/{Users.cs => User.cs} (90%) delete mode 100644 Server/RestAPI/UserController.cs create mode 100644 Server/RestAPI/UsersController.cs diff --git a/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts index 555a6947..795ceb36 100644 --- a/Client/app/containers/users/users.component.ts +++ b/Client/app/containers/users/users.component.ts @@ -50,14 +50,16 @@ export class UsersComponent implements OnInit { // The Client then re-uses this Http result instead of hitting the server again! // NOTE : transferHttp also automatically does .map(res => res.json()) for you, so no need for these calls - this.transferHttp.get(`${this.baseUrl}/api/user/all`).subscribe(result => { - console.log('TransferHttp [GET] /api/user/allresult', result); + this.transferHttp.get(`${this.baseUrl}/api/users`).subscribe(result => { + console.log('Get user result: ', result); + console.log('TransferHttp [GET] /api/users/allresult', result); this.users = result as IUser[]; }); } deleteUser(user) { - this.http.delete(`${this.baseUrl}/api/user/delete/` + user.id).subscribe(result => { + this.http.delete(`${this.baseUrl}/api/users/` + user.id).subscribe(result => { + console.log('Delete user result: ', result); if (result.ok) { let position = this.users.indexOf(user); this.users.splice(position, 1); @@ -68,12 +70,8 @@ export class UsersComponent implements OnInit { } editUser(user) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('id', user.id); - urlSearchParams.append('name', user.name); - - this.http.put(`${this.baseUrl}/api/user/update`, urlSearchParams).subscribe(result => { - console.log('result: ', result); + this.http.put(`${this.baseUrl}/api/users/` + user.id, user).subscribe(result => { + console.log('Put user result: ', result); if (!result) { alert('There was an issue, Could not edit user'); } @@ -81,10 +79,8 @@ export class UsersComponent implements OnInit { } addUser(newUserName) { - let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('name', newUserName); - - this.http.post(`${this.baseUrl}/api/user/insert`, urlSearchParams).subscribe(result => { + this.http.post(`${this.baseUrl}/api/users`, { name: newUserName }).subscribe(result => { + console.log('Post user result: ', result); if (result) { this.users.push(result.json()); this.newUserName = ''; diff --git a/Server/Data/DbInitializer.cs b/Server/Data/DbInitializer.cs index 526b9911..bca0eed3 100644 --- a/Server/Data/DbInitializer.cs +++ b/Server/Data/DbInitializer.cs @@ -17,22 +17,22 @@ public static void Initialize(SpaDbContext context) { return; // DB has been seeded } - var users = new Users[] + var users = new User[] { - new Users(){Name = "Mark Pieszak"}, - new Users(){Name = "Abrar Jahin"}, - new Users(){Name = "hakonamatata"}, - new Users(){Name = "LiverpoolOwen"}, - new Users(){Name = "Ketrex"}, - new Users(){Name = "markwhitfeld"}, - new Users(){Name = "daveo1001"}, - new Users(){Name = "paonath"}, - new Users(){Name = "nalex095"}, - new Users(){Name = "ORuban"}, - new Users(){Name = "Gaulomatic"} + new User(){Name = "Mark Pieszak"}, + new User(){Name = "Abrar Jahin"}, + new User(){Name = "hakonamatata"}, + new User(){Name = "LiverpoolOwen"}, + new User(){Name = "Ketrex"}, + new User(){Name = "markwhitfeld"}, + new User(){Name = "daveo1001"}, + new User(){Name = "paonath"}, + new User(){Name = "nalex095"}, + new User(){Name = "ORuban"}, + new User(){Name = "Gaulomatic"} }; - foreach (Users s in users) + foreach (User s in users) { context.User.Add(s); } diff --git a/Server/Data/SpaDbContext.cs b/Server/Data/SpaDbContext.cs index c42f8fa9..e6fee5f7 100644 --- a/Server/Data/SpaDbContext.cs +++ b/Server/Data/SpaDbContext.cs @@ -12,6 +12,6 @@ public SpaDbContext(DbContextOptions<SpaDbContext> options) } //List of DB Models - Add your DB models here - public DbSet<Users> User { get; set; } + public DbSet<User> User { get; set; } } } diff --git a/Server/Models/Users.cs b/Server/Models/User.cs similarity index 90% rename from Server/Models/Users.cs rename to Server/Models/User.cs index d608a9ce..fb3d74da 100644 --- a/Server/Models/Users.cs +++ b/Server/Models/User.cs @@ -3,7 +3,7 @@ namespace AspCoreServer.Models { - public class Users + public class User { public int ID { get; set; } public string Name { get; set; } @@ -13,7 +13,7 @@ public class Users public DateTime EntryTime { get; set; } //Setting Default value - public Users() + public User() { EntryTime = DateTime.Now; } diff --git a/Server/RestAPI/UserController.cs b/Server/RestAPI/UserController.cs deleted file mode 100644 index 34587286..00000000 --- a/Server/RestAPI/UserController.cs +++ /dev/null @@ -1,105 +0,0 @@ -using AspCoreServer.Data; -using AspCoreServer.Models; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using System; -using System.Linq; -using System.Threading.Tasks; - -namespace AspCoreServer.Controllers -{ - [Route("api/user/[action]")] - public class UserController : Controller - { - private readonly SpaDbContext _context; - - public UserController(SpaDbContext context) - { - _context = context; - } - - [HttpGet] - public async Task<IActionResult> All(int currentPageNo = 1, int pageSize = 20) - { - var users = await _context.User - .OrderByDescending(u => u.EntryTime) - .Skip((currentPageNo - 1) * pageSize) - .Take(pageSize) - .ToArrayAsync(); - - return Ok(users); - } - - [HttpGet] - public async Task<IActionResult> Details(int? id) - { - var user = await _context.User - .Where(u => u.ID == id) - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); - - if (user == null) - { - return NotFound("User not Found"); - } - else - { - return Ok(user); - } - } - - [HttpPost] - public async Task<IActionResult> Insert([Bind("Name")] Users user) - { - if (user.Name != null) - { - _context.Add(user); - await _context.SaveChangesAsync(); - return Ok(user); - } - else - { - return NotFound("Name not given"); - } - } - - [HttpPut] - public async Task<IActionResult> Update([Bind("ID,Name")] Users userUpdateValue) - { - try - { - userUpdateValue.EntryTime = DateTime.Now; - _context.Update(userUpdateValue); - await _context.SaveChangesAsync(); - return Ok(userUpdateValue); - } - catch (DbUpdateException) - { - //Log the error (uncomment ex variable name and write a log.) - ModelState.AddModelError("", "Unable to save changes. " + - "Try again, and if the problem persists, " + - "see your system administrator."); - return NotFound("User not Found"); - } - } - - [HttpDelete] - [Route("{id:int}")] - public async Task<IActionResult> Delete(int? id) - { - var userToRemove = await _context.User - .AsNoTracking() - .SingleOrDefaultAsync(m => m.ID == id); - if (userToRemove == null) - { - return NotFound("User not Found"); - } - else - { - _context.User.Remove(userToRemove); - await _context.SaveChangesAsync(); - return Ok("Deleted user - " + userToRemove.Name); - } - } - } -} diff --git a/Server/RestAPI/UsersController.cs b/Server/RestAPI/UsersController.cs new file mode 100644 index 00000000..2938eb8e --- /dev/null +++ b/Server/RestAPI/UsersController.cs @@ -0,0 +1,123 @@ +using AspCoreServer.Data; +using AspCoreServer.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace AspCoreServer.Controllers +{ + [Route("api/[controller]")] + public class UsersController : Controller + { + private readonly SpaDbContext _context; + + public UsersController(SpaDbContext context) + { + _context = context; + } + + [HttpGet] + public async Task<IActionResult> Get(int currentPageNo = 1, int pageSize = 20) + { + var users = await _context.User + .OrderByDescending(u => u.EntryTime) + .Skip((currentPageNo - 1) * pageSize) + .Take(pageSize) + .ToArrayAsync(); + + if (!users.Any()) + { + return NotFound("Users not Found"); + } + else + { + return Ok(users); + } + } + + [HttpGet("{id}")] + public async Task<IActionResult> Get(int id) + { + var user = await _context.User + .Where(u => u.ID == id) + .AsNoTracking() + .SingleOrDefaultAsync(m => m.ID == id); + + if (user == null) + { + return NotFound("User not Found"); + } + else + { + return Ok(user); + } + } + + [HttpPost] + public async Task<IActionResult> Post([FromBody]User user) + { + if (!string.IsNullOrEmpty(user.Name)) + { + _context.Add(user); + await _context.SaveChangesAsync(); + return CreatedAtAction("Post", user); + } + else + { + return BadRequest("User's name was not given"); + } + } + + [HttpPut("{id}")] + public async Task<IActionResult> Put(int id, [FromBody]User userUpdateValue) + { + try + { + userUpdateValue.EntryTime = DateTime.Now; + + var userToEdit = await _context.User + .AsNoTracking() + .SingleOrDefaultAsync(m => m.ID == id); + + if (userToEdit == null) + { + return NotFound("Could not update user as it was not Found"); + } + else + { + _context.Update(userUpdateValue); + await _context.SaveChangesAsync(); + return Ok("Updated user - " + userUpdateValue.Name); + } + } + catch (DbUpdateException) + { + //Log the error (uncomment ex variable name and write a log.) + ModelState.AddModelError("", "Unable to save changes. " + + "Try again, and if the problem persists, " + + "see your system administrator."); + return NotFound("User not Found"); + } + } + + [HttpDelete("{id}")] + public async Task<IActionResult> Delete(int id) + { + var userToRemove = await _context.User + .AsNoTracking() + .SingleOrDefaultAsync(m => m.ID == id); + if (userToRemove == null) + { + return NotFound("Could not delete user as it was not Found"); + } + else + { + _context.User.Remove(userToRemove); + await _context.SaveChangesAsync(); + return Ok("Deleted user - " + userToRemove.Name); + } + } + } +} From 0b7aa25934174aa7d0986d8fb3725b7c10cc9599 Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Sun, 16 Apr 2017 22:45:01 +0100 Subject: [PATCH 006/142] Created User service (#185) * Removed no longer used API * Pluralised API to "Users" and depluralised model to "User". All UsersAPI's now point to same URI. * Updated web API status codes * Created user service --- Client/app/app.module.ts | 2 + .../app/containers/users/users.component.html | 2 +- .../app/containers/users/users.component.ts | 41 +++++------------- Client/app/models/User.ts | 4 ++ Client/app/shared/user.service.ts | 42 +++++++++++++++++++ 5 files changed, 60 insertions(+), 31 deletions(-) create mode 100644 Client/app/models/User.ts create mode 100644 Client/app/shared/user.service.ts diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index cbf93f75..581cba5b 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -19,6 +19,7 @@ import { ChatComponent } from './containers/chat/chat.component'; import { Ng2BootstrapComponent } from './containers/ng2-bootstrap-demo/ng2bootstrap.component'; import { LinkService } from './shared/link.service'; +import { UserService } from './shared/user.service'; import { ConnectionResolver } from './shared/route.resolver'; import { ORIGIN_URL } from './shared/constants/baseurl.constants'; import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module'; @@ -136,6 +137,7 @@ export function createTranslateLoader(http: Http, baseHref) { ], providers: [ LinkService, + UserService, ConnectionResolver, TranslateModule ] diff --git a/Client/app/containers/users/users.component.html b/Client/app/containers/users/users.component.html index 288ff148..e52eadc2 100644 --- a/Client/app/containers/users/users.component.html +++ b/Client/app/containers/users/users.component.html @@ -23,7 +23,7 @@ <input class="form-control" [(ngModel)]="user.name" #name="ngModel" /> </td> <td> - <button *ngIf="name.dirty" class="btn btn-success" (click)="editUser(user)">Save</button> + <button *ngIf="name.dirty" class="btn btn-success" (click)="updateUser(user)">Save</button> </td> <td> <button class="btn btn-danger" (click)="deleteUser(user)">Delete</button> diff --git a/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts index 795ceb36..e477d31b 100644 --- a/Client/app/containers/users/users.component.ts +++ b/Client/app/containers/users/users.component.ts @@ -3,11 +3,8 @@ // animation imports trigger, state, style, transition, animate, Inject } from '@angular/core'; -import { APP_BASE_HREF } from '@angular/common'; - -import { Http, URLSearchParams } from '@angular/http'; -import { ORIGIN_URL } from '../../shared/constants/baseurl.constants'; -import { TransferHttp } from '../../../modules/transfer-http/transfer-http'; +import { IUser } from '../../models/User'; +import { UserService } from '../../shared/user.service'; @Component({ selector: 'fetchdata', @@ -33,24 +30,13 @@ export class UsersComponent implements OnInit { public users: IUser[]; // Use "constructor"s only for dependency injection - constructor( - private transferHttp: TransferHttp, // Use only for GETS that you want re-used between Server render -> Client render - private http: Http, // Use for everything else - @Inject(ORIGIN_URL) private baseUrl: string - ) { } + constructor(private userService: UserService) { } // Here you want to handle anything with @Input()'s @Output()'s // Data retrieval / etc - this is when the Component is "ready" and wired up ngOnInit() { - this.newUserName = ''; - - // ** TransferHttp example / concept ** - // - Here we make an Http call on the server, save the result on the window object and pass it down with the SSR, - // The Client then re-uses this Http result instead of hitting the server again! - - // NOTE : transferHttp also automatically does .map(res => res.json()) for you, so no need for these calls - this.transferHttp.get(`${this.baseUrl}/api/users`).subscribe(result => { + this.userService.getUsers().subscribe(result => { console.log('Get user result: ', result); console.log('TransferHttp [GET] /api/users/allresult', result); this.users = result as IUser[]; @@ -58,7 +44,7 @@ export class UsersComponent implements OnInit { } deleteUser(user) { - this.http.delete(`${this.baseUrl}/api/users/` + user.id).subscribe(result => { + this.userService.deleteUser(user).subscribe(result => { console.log('Delete user result: ', result); if (result.ok) { let position = this.users.indexOf(user); @@ -69,19 +55,19 @@ export class UsersComponent implements OnInit { }); } - editUser(user) { - this.http.put(`${this.baseUrl}/api/users/` + user.id, user).subscribe(result => { - console.log('Put user result: ', result); - if (!result) { + updateUser(user) { + this.userService.updateUser(user).subscribe(result => { + console.log('Put user result: ', result); + if (!result.ok) { alert('There was an issue, Could not edit user'); } }); } addUser(newUserName) { - this.http.post(`${this.baseUrl}/api/users`, { name: newUserName }).subscribe(result => { + this.userService.addUser(newUserName).subscribe(result => { console.log('Post user result: ', result); - if (result) { + if (result.ok) { this.users.push(result.json()); this.newUserName = ''; } else { @@ -90,8 +76,3 @@ export class UsersComponent implements OnInit { }); } } - -interface IUser { - id: number; - name: string; -} diff --git a/Client/app/models/User.ts b/Client/app/models/User.ts new file mode 100644 index 00000000..53f9df3a --- /dev/null +++ b/Client/app/models/User.ts @@ -0,0 +1,4 @@ +export interface IUser { + id: number; + name: string; +} \ No newline at end of file diff --git a/Client/app/shared/user.service.ts b/Client/app/shared/user.service.ts new file mode 100644 index 00000000..8bcc3bb3 --- /dev/null +++ b/Client/app/shared/user.service.ts @@ -0,0 +1,42 @@ +import { Injectable, Inject } from '@angular/core'; +import { Http, URLSearchParams } from '@angular/http'; +import { APP_BASE_HREF } from '@angular/common'; +import { ORIGIN_URL } from './constants/baseurl.constants'; +import { IUser } from '../models/User'; +import { TransferHttp } from '../../modules/transfer-http/transfer-http'; +import { Observable } from 'rxjs/Observable'; + +@Injectable() +export class UserService { + constructor( + private transferHttp: TransferHttp, // Use only for GETS that you want re-used between Server render -> Client render + private http: Http, // Use for everything else + @Inject(ORIGIN_URL) private baseUrl: string) { + + } + + getUsers(): Observable<IUser[]> { + // ** TransferHttp example / concept ** + // - Here we make an Http call on the server, save the result on the window object and pass it down with the SSR, + // The Client then re-uses this Http result instead of hitting the server again! + + // NOTE : transferHttp also automatically does .map(res => res.json()) for you, so no need for these calls + return this.transferHttp.get(`${this.baseUrl}/api/users`); + } + + getUser(user: IUser): Observable<IUser> { + return this.transferHttp.get(`${this.baseUrl}/api/users/` + user.id); + } + + deleteUser(user: IUser): Observable<any> { + return this.http.delete(`${this.baseUrl}/api/users/` + user.id); + } + + updateUser(user: IUser): Observable<any> { + return this.http.put(`${this.baseUrl}/api/users/` + user.id, user); + } + + addUser(newUserName: string): Observable<any> { + return this.http.post(`${this.baseUrl}/api/users`, { name: newUserName }) + } +} From 3ce881c6c39a475b60ba527d008ee36aade8bbdf Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Mon, 17 Apr 2017 04:49:23 +0100 Subject: [PATCH 007/142] Swagger changes (#187) * Added Swagger * Remove old API docs --- Asp2017.csproj | 1 + Startup.cs | 156 ++++++++++++++++++++++++----------------- docs/RESTAPI-ENTITY.MD | 43 ------------ docs/all.jpeg | Bin 42569 -> 0 bytes docs/delete.jpeg | Bin 34725 -> 0 bytes docs/details.jpeg | Bin 31981 -> 0 bytes docs/insert.jpeg | Bin 40611 -> 0 bytes docs/update.jpeg | Bin 39830 -> 0 bytes 8 files changed, 91 insertions(+), 109 deletions(-) delete mode 100644 docs/RESTAPI-ENTITY.MD delete mode 100644 docs/all.jpeg delete mode 100644 docs/delete.jpeg delete mode 100644 docs/details.jpeg delete mode 100644 docs/insert.jpeg delete mode 100644 docs/update.jpeg diff --git a/Asp2017.csproj b/Asp2017.csproj index 280e77e9..50717f9e 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -12,6 +12,7 @@ <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="1.1.1" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0-rc3" /> </ItemGroup> <ItemGroup> <!-- Files not to show in IDE --> diff --git a/Startup.cs b/Startup.cs index 5712627d..a0c51958 100644 --- a/Startup.cs +++ b/Startup.cs @@ -13,81 +13,105 @@ using Microsoft.AspNetCore.NodeServices; using AspCoreServer.Data; +using Swashbuckle.AspNetCore.Swagger; namespace AspCoreServer { - public class Startup + public class Startup + { + + public static void Main(string[] args) + { + var host = new WebHostBuilder() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup<Startup>() + .Build(); + + host.Run(); + } + public Startup(IHostingEnvironment env) + { + var builder = new ConfigurationBuilder() + .SetBasePath(env.ContentRootPath) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + // Add framework services. + services.AddMvc(); + services.AddNodeServices(); + + var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; + var connectionString = connectionStringBuilder.ToString(); + + services.AddDbContext<SpaDbContext>(options => + options.UseSqlite(connectionString)); + + // Register the Swagger generator, defining one or more Swagger documents + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new Info { Title = "Angular 4.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) { + loggerFactory.AddConsole(Configuration.GetSection("Logging")); + loggerFactory.AddDebug(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { + HotModuleReplacement = true + }); - public static void Main(string[] args) + DbInitializer.Initialize(context); + + app.UseSwagger(); + + // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. + app.UseSwaggerUI(c => { - var host = new WebHostBuilder() - .UseKestrel() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseIISIntegration() - .UseStartup<Startup>() - .Build(); - - host.Run(); - } - public Startup(IHostingEnvironment env) + c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); + }); + + app.MapWhen(x => !x.Request.Path.Value.StartsWith("/swagger"), builder => { - var builder = new ConfigurationBuilder() - .SetBasePath(env.ContentRootPath) - .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true) - .AddEnvironmentVariables(); - Configuration = builder.Build(); - } - - public IConfigurationRoot Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) + builder.UseMvc(routes => + { + routes.MapSpaFallbackRoute( + name: "spa-fallback", + defaults: new { controller = "Home", action = "Index" }); + }); + }); + } + else + { + app.UseMvc(routes => { - // Add framework services. - services.AddMvc(); - services.AddNodeServices(); + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); - var connectionStringBuilder = new Microsoft.Data.Sqlite.SqliteConnectionStringBuilder { DataSource = "spa.db" }; - var connectionString = connectionStringBuilder.ToString(); + routes.MapSpaFallbackRoute( + name: "spa-fallback", + defaults: new { controller = "Home", action = "Index" }); + }); + app.UseExceptionHandler("/Home/Error"); + } - services.AddDbContext<SpaDbContext>(options => - options.UseSqlite(connectionString)); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, SpaDbContext context) - { - loggerFactory.AddConsole(Configuration.GetSection("Logging")); - loggerFactory.AddDebug(); - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { - HotModuleReplacement = true - }); - - DbInitializer.Initialize(context); - } - else - { - app.UseExceptionHandler("/Home/Error"); - } - - app.UseStaticFiles(); - - app.UseMvc(routes => - { - routes.MapRoute( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); - - routes.MapSpaFallbackRoute( - name: "spa-fallback", - defaults: new { controller = "Home", action = "Index" }); - }); - } + app.UseStaticFiles(); } + } } diff --git a/docs/RESTAPI-ENTITY.MD b/docs/RESTAPI-ENTITY.MD deleted file mode 100644 index 09b41f81..00000000 --- a/docs/RESTAPI-ENTITY.MD +++ /dev/null @@ -1,43 +0,0 @@ -# WebAPI (REST API) - Entity Framework Core CRUD Examples: - -### Get all data - -<p align="center"> - <img src="http://github.com/all.jpeg" alt="Get all Data Api" title="Get all Data Api"> -</p> - -If you need to use pagination, please create your request URL like this- - -- Method - `GET` -- URL- - - /api/user/all?currentPageNo=1&pageSize=5 - -Default value (if u hit - `/api/user/all`) - - currentPageNo=1 - pageSize=20 - -### Get a specific data - -<p align="center"> - <img src="http://github.com/details.jpeg" alt="Get details Data Api" title="Get details Data Api"> -</p> - -### Insert data - -<p align="center"> - <img src="http://github.com/insert.jpeg" alt="insert Data Api" title="insert Data Api"> -</p> - -### Update data - -<p align="center"> - <img src="http://github.com/update.jpeg" alt="Update Data Api" title="Update Data Api"> -</p> - -### Delete data - -<p align="center"> - <img src="http://github.com/delete.jpeg" alt="Delete Data Api" title="Delete Data Api"> -</p> diff --git a/docs/all.jpeg b/docs/all.jpeg deleted file mode 100644 index 12af165b5c5a3ad4642a6694c589d6fc6da56f2f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42569 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 {LYMsrf;x~+avbGe0&X7{$~b< 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 diff --git a/docs/delete.jpeg b/docs/delete.jpeg deleted file mode 100644 index f6263045a61c600ef1ce0206e551844ae6e612be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34725 zcmeIa1zcQBnlF5C0t5>rI0O$G+${+nG`IwJcMCLxAnD))3GNbT2(CebySsaEhhWXu zdFQp=o!On)*}LD~yEMO|>r~gNs^?!%*&}s3dAkT;%ScE|0B~?{fII9DxLv}2EADP# z3IK9)00RI3r~o3I000k*A;BJGf-C?;SR4)(S4>L&`3;}}0Ji-W34j7i!+<>t!va9E z1^b8kGj{j6L*Nd9I|S|!xI^F$f&YyN*twb5!sv$kV~>Dp_!Eb_J;MJ+Zn&fQ4uLxa z?hv>`;0}R5BVY%(0VcrDjl%5`ny$ICvpqj6tF04@fw7&T35$`P4Xe9>Ju4f_Q&vDw z#NFP&$jZc-!qCLb!d8fGzpjyv!opaHPMu5cshs_D6LSkGPe+qip7JV2o>oSD#&jaW z_t6C1`Q2^oZA_dEDBNwVZJqetg(!b1ogWtenaoNlEa+%#%CG!F;@2FoCn3sT+v4Wt z#^T1oV&`ba%ErgX$NH3=m7Sd#mV?>J!`9iro!Qoj>em8Zm^c|ZTG%^V*x6G2EYQHv z&c#`X(#6HXnBUaEl*`cAn2XuafWw%Xjm_AA*}#~CjoFxk!<dJQjhEe&&5-if>Wz(l zuif6o(fXIVjg44MtW9i8Y@MB8Xt1$Tvi_yf|E+Zj3mO~o8#$U7INLe?rTSMU4*wix z))vA)H;RI+KLLY1{3QY_7S?n3?%fk){d0}KWcm}uzvhN@94z*GH^bhf_;2{!Rq_vk zd<WMZT>lUP|4`;T?z)5PA41?C%6!LN|Chn_$5zqA7PdchgKY$FXMpDb3K9}B5+VvR z5;7_Z3Mv{7I&6=DhL44ffkS{#L`Z;7@Q9d<?lCbb4e28SN)}2Q`X`J`j6@Wy9IOoN zbPS9PKMR3FK}AKohlYoaj>kYkK*I13e{P!q98|a@_#*^3N&p@Q4gm-5wiO_UiOP^* z3b~&;xnDnU@Cb-V$SA1y(9mHSDzE`~I0OWEL<A%xM3^=X&KveSfQW;H`<P7(`GJxF z3Z(-cyI*8FD%G=+W_;zL18NRK$G7*;9zG%<B%-0EqkqD{$;HjX%g6uxg}8*Il(ft% z6;(BL4NWa0V-r&|a|=r+XBSsDcMnhhcL9Mx!SBIQ(H~=C<KjOhWMpP#=j7()e=aR6 zuc)l5uBrXj(%RPE(b?5KJTf{qJ~25py|lcty0*UYeRJ#Z==kLH?EJ^Y<xjfc0EEAg z^*d$%iY^=&UGRvA2#6>@>4JlIg9QW}M5M=T$hcxkC<YD>DB1l`@t#Gdmo(p_;!r-o zH*_3Adq~Z>M05C)v|lLu`v`mcpQ7w{!v3IZ8o)q+gLNJP4j=+tGOIB7rUCo_eNK6B zs`r>rF5W`<^IJeD^A>0b0Hnnw&w@c4<)HJy%w6nTV8Hwq*iRS5RZ_aiAivlpzd515 zdLeQ=Epqi;5%Hr6$UF$@C3y>YqX`dTU7v+RFY(}L@X4ni-vTK^x4^=)@c#$OmXeUT zFL`DzvJnD0Z~7A|l7Eoti&Z9e3kVv)Fhn9Z^@qB5+yZa@z{LAE?0_py^5b@qt3{=| z^3w0{yW^L?%o%s%<A2?J@Tb8K2K*gN4MFuSrx}r;4>xDGz+fmdfTtw>9|m^2Q E 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 ZFsb1UJ$&1*_hbB5H|PIUdE~cK{~!MVhwK0V diff --git a/docs/details.jpeg b/docs/details.jpeg deleted file mode 100644 index ec188b91b0671e52e7f0a90c68b848fba7b5d741..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31981 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 diff --git a/docs/insert.jpeg b/docs/insert.jpeg deleted file mode 100644 index 59b563c69a23ab5777826d263a8cf8ce92d84e9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40611 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 X9Pp<-%J+tL`JnOk|H)_YkJ0}FH)56- diff --git a/docs/update.jpeg b/docs/update.jpeg deleted file mode 100644 index 7ed97572fc1fdbdd3be33b4a9870cf0ef2a057ef..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39830 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 Szl1FR$`SM5{!adL;y(b$Gjt39 From a82445457c79815ce3fc5bacb46a0cf9f54d7b85 Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Mon, 17 Apr 2017 11:58:52 +0100 Subject: [PATCH 008/142] Assets not loading bug (#188) --- Startup.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Startup.cs b/Startup.cs index a0c51958..4e3c0510 100644 --- a/Startup.cs +++ b/Startup.cs @@ -69,6 +69,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); + app.UseStaticFiles(); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -91,8 +93,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF builder.UseMvc(routes => { routes.MapSpaFallbackRoute( - name: "spa-fallback", - defaults: new { controller = "Home", action = "Index" }); + name: "spa-fallback", + defaults: new { controller = "Home", action = "Index" }); }); }); } @@ -110,8 +112,6 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF }); app.UseExceptionHandler("/Home/Error"); } - - app.UseStaticFiles(); } } } From b84358eefb684f063a58a17c1429528af990bdd0 Mon Sep 17 00:00:00 2001 From: Tim Harker <tim@rekrah.com> Date: Tue, 18 Apr 2017 21:42:29 -0400 Subject: [PATCH 009/142] Modified FAQ (#192) See this issue link, https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues/190. Closes #190 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 0782d572..a1d190a7 100644 --- a/README.md +++ b/README.md @@ -399,6 +399,10 @@ import * as $ from 'jquery'; **Always make sure to wrap anything jQuery oriented in Angular's `isPlatformBrowser()` conditional!** +### How can I support IE9 through IE11? + +To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` folder and uncomment out the 'import polyfills' as needed. + ---- # Special Thanks From ad05cbc38aeb61dc5d3cbb2a2d10ca9c75c7b4c6 Mon Sep 17 00:00:00 2001 From: mikeysteele <mjwestcott@gmail.com> Date: Wed, 19 Apr 2017 21:27:21 +1000 Subject: [PATCH 010/142] fixed file paths for case sensitive filesystems (#193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🎈🎁 --- tsconfig.json | 2 +- webpack.config.js | 2 +- webpack/webpack.aot.js | 8 ++++---- webpack/webpack.client.js | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 2c8d34f9..468cee8f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,6 @@ "types": [ "node" ] }, "include": [ - "client" + "Client" ] } diff --git a/webpack.config.js b/webpack.config.js index 8ec7d827..76980b9c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -15,7 +15,7 @@ module.exports = function (options, webpackOptions) { } const serverConfig = webpackMerge({}, commonPartial, serverPartial, { - entry: options.aot ? { 'main-server' : './client/main.server.aot.ts' } : serverPartial.entry, // Temporary + entry: options.aot ? { 'main-server' : './Client/main.server.aot.ts' } : serverPartial.entry, // Temporary plugins: [ getAotPlugin('server', !!options.aot) ] diff --git a/webpack/webpack.aot.js b/webpack/webpack.aot.js index 537fc976..5af7ca50 100644 --- a/webpack/webpack.aot.js +++ b/webpack/webpack.aot.js @@ -2,13 +2,13 @@ const { AotPlugin } = require('@ngtools/webpack'); const tsconfigs = { - client: root('./client/tsconfig.browser.json'), - server: root('./client/tsconfig.server.json') + client: root('./Client/tsconfig.browser.json'), + server: root('./Client/tsconfig.server.json') }; const aotTsconfigs = { - client: root('./client/tsconfig.browser.json'), - server: root('./client/tsconfig.server.aot.json') + client: root('./Client/tsconfig.browser.json'), + server: root('./Client/tsconfig.server.aot.json') }; /** diff --git a/webpack/webpack.client.js b/webpack/webpack.client.js index 19aadf2b..1c50c612 100644 --- a/webpack/webpack.client.js +++ b/webpack/webpack.client.js @@ -8,7 +8,7 @@ const clientBundleOutputDir = root('./wwwroot/dist'); */ module.exports = { entry: { - 'main-browser': root('./client/main.browser.ts') + 'main-browser': root('./Client/main.browser.ts') }, output: { path: root('./wwwroot/dist'), From 38f6931b590d577b53f5c2a29e40b5d25cad129e Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 19 Apr 2017 11:17:14 -0400 Subject: [PATCH 011/142] feat(lazy-loading): add lazy demo and fixed HMR (#197) closes #191 updates #165 (still needs to have speed improved) --- Client/app/app.module.ts | 4 +++- .../components/navmenu/navmenu.component.html | 9 +++++-- Client/app/containers/+lazy/lazy.component.ts | 13 ++++++++++ Client/app/containers/+lazy/lazy.module.ts | 15 ++++++++++++ Client/main.browser.ts | 24 +++++++++---------- 5 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 Client/app/containers/+lazy/lazy.component.ts create mode 100644 Client/app/containers/+lazy/lazy.module.ts diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index 581cba5b..fc72421d 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -42,7 +42,7 @@ export function createTranslateLoader(http: Http, baseHref) { HomeComponent, ChatComponent, Ng2BootstrapComponent - ], + ], imports: [ CommonModule, HttpModule, @@ -131,6 +131,8 @@ export function createTranslateLoader(http: Http, baseHref) { } }, + { path: 'lazy', loadChildren: './containers/+lazy/lazy.module#LazyModule'}, + // All else fails - go home! { path: '**', redirectTo: 'home' } ]) diff --git a/Client/app/components/navmenu/navmenu.component.html b/Client/app/components/navmenu/navmenu.component.html index 9e31aaa4..5ead7a82 100644 --- a/Client/app/components/navmenu/navmenu.component.html +++ b/Client/app/components/navmenu/navmenu.component.html @@ -24,12 +24,17 @@ </li> <li [routerLinkActive]="['link-active']"> <a [routerLink]="['/users']"> - <span class='glyphicon glyphicon-user'></span> REST API Demo + <span class='glyphicon glyphicon-user'></span> Rest API Demo </a> </li> <li [routerLinkActive]="['link-active']"> <a [routerLink]="['/ng2-bootstrap']"> - <span class='glyphicon glyphicon-user'></span> ng2-Bootstrap demo + <span class='glyphicon glyphicon-th-large'></span> ngx-Bootstrap demo + </a> + </li> + <li [routerLinkActive]="['link-active']"> + <a [routerLink]="['/lazy']"> + <span class='glyphicon glyphicon-star-empty'></span> Lazy-loaded demo </a> </li> <!--<li [routerLinkActive]="['link-active']"> diff --git a/Client/app/containers/+lazy/lazy.component.ts b/Client/app/containers/+lazy/lazy.component.ts new file mode 100644 index 00000000..25d6d1a4 --- /dev/null +++ b/Client/app/containers/+lazy/lazy.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'lazy-view', + template: ` + <h1>Lazy-Loaded Component!</h1> + <blockquote> + Fun fact: This was lazy-loaded :) + Check your Network tab! + </blockquote> + ` +}) +export class LazyComponent { } diff --git a/Client/app/containers/+lazy/lazy.module.ts b/Client/app/containers/+lazy/lazy.module.ts new file mode 100644 index 00000000..8d468754 --- /dev/null +++ b/Client/app/containers/+lazy/lazy.module.ts @@ -0,0 +1,15 @@ +import { NgModule, Component } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { LazyComponent } from './lazy.component'; + +@NgModule({ + declarations: [LazyComponent], + imports: [ + RouterModule.forChild([ + { path: '', component: LazyComponent, pathMatch: 'full' } + ]) + ] +}) +export class LazyModule { + +} diff --git a/Client/main.browser.ts b/Client/main.browser.ts index c2ab7e10..f1e76b9d 100644 --- a/Client/main.browser.ts +++ b/Client/main.browser.ts @@ -6,17 +6,17 @@ import { BrowserAppModule } from './app/browser-app.module'; const rootElemTagName = 'app'; // Update this if you change your root component selector // // 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); -// oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem); -// modulePromise.then(appModule => appModule.destroy()); -// }); -// } else { -// enableProdMode(); -// } +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); + oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem); + modulePromise.then(appModule => appModule.destroy()); + }); +} else { + enableProdMode(); +} const modulePromise = platformBrowserDynamic().bootstrapModule(BrowserAppModule); From b475fa5b0923d185ab9aeb17316a2c4d80eecfd6 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 19 Apr 2017 11:20:24 -0400 Subject: [PATCH 012/142] chore(package): bump zone.js package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bf86be37..ca18648a 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,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.5" }, "devDependencies": { "@ngtools/webpack": "^1.3.0", From 1ded40a7b7269fac1aabed984723a6f5e4f9bea2 Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Thu, 20 Apr 2017 04:27:04 +0100 Subject: [PATCH 013/142] Updated naming for ngx-bootstrap (#200) --- Client/app/app.module.ts | 8 ++++---- Client/app/components/navmenu/navmenu.component.html | 2 +- .../ngx-bootstrap.component.html} | 4 ++-- .../ngx-bootstrap.component.ts} | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) rename Client/app/containers/{ng2-bootstrap-demo/ng2bootstrap.component.html => ngx-bootstrap-demo/ngx-bootstrap.component.html} (94%) rename Client/app/containers/{ng2-bootstrap-demo/ng2bootstrap.component.ts => ngx-bootstrap-demo/ngx-bootstrap.component.ts} (89%) diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index fc72421d..986d5de3 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -16,7 +16,7 @@ import { HomeComponent } from './containers/home/home.component'; import { UsersComponent } from './containers/users/users.component'; import { CounterComponent } from './containers/counter/counter.component'; import { ChatComponent } from './containers/chat/chat.component'; -import { Ng2BootstrapComponent } from './containers/ng2-bootstrap-demo/ng2bootstrap.component'; +import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; import { LinkService } from './shared/link.service'; import { UserService } from './shared/user.service'; @@ -41,7 +41,7 @@ export function createTranslateLoader(http: Http, baseHref) { UsersComponent, HomeComponent, ChatComponent, - Ng2BootstrapComponent + NgxBootstrapComponent ], imports: [ CommonModule, @@ -120,9 +120,9 @@ export function createTranslateLoader(http: Http, baseHref) { } }, { - path: 'ng2-bootstrap', component: Ng2BootstrapComponent, + path: 'ngx-bootstrap', component: NgxBootstrapComponent, data: { - title: 'Ng2-bootstrap demo!!', + title: 'Ngx-bootstrap demo!!', meta: [{ name: 'description', content: 'This is an Demo Bootstrap page Description!' }], links: [ { rel: 'canonical', href: 'http://blogs.example.com/bootstrap/something' }, diff --git a/Client/app/components/navmenu/navmenu.component.html b/Client/app/components/navmenu/navmenu.component.html index 5ead7a82..277923a1 100644 --- a/Client/app/components/navmenu/navmenu.component.html +++ b/Client/app/components/navmenu/navmenu.component.html @@ -28,7 +28,7 @@ </a> </li> <li [routerLinkActive]="['link-active']"> - <a [routerLink]="['/ng2-bootstrap']"> + <a [routerLink]="['/ngx-bootstrap']"> <span class='glyphicon glyphicon-th-large'></span> ngx-Bootstrap demo </a> </li> diff --git a/Client/app/containers/ng2-bootstrap-demo/ng2bootstrap.component.html b/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html similarity index 94% rename from Client/app/containers/ng2-bootstrap-demo/ng2bootstrap.component.html rename to Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html index cf25b23a..5f7c3820 100644 --- a/Client/app/containers/ng2-bootstrap-demo/ng2bootstrap.component.html +++ b/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html @@ -1,7 +1,7 @@ -<h1>Ng2-bootstrap Demo:</h1> +<h1>Ngx-bootstrap Demo:</h1> <blockquote> - <strong>Here we're using Bootstrap via <a href="https://github.com/valor-software/ng2-bootstrap">ng2-bootstrap</a>, which can even be rendered on the server!</strong> + <strong>Here we're using Bootstrap via <a href="https://github.com/valor-software/ngx-bootstrap">ngx-bootstrap</a>, which can even be rendered on the server!</strong> <br> </blockquote> diff --git a/Client/app/containers/ng2-bootstrap-demo/ng2bootstrap.component.ts b/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts similarity index 89% rename from Client/app/containers/ng2-bootstrap-demo/ng2bootstrap.component.ts rename to Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts index 0ef65145..a8ba7dec 100644 --- a/Client/app/containers/ng2-bootstrap-demo/ng2bootstrap.component.ts +++ b/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts @@ -2,9 +2,9 @@ @Component({ selector: 'app-bootstrap', - templateUrl: './ng2bootstrap.component.html' + templateUrl: './ngx-bootstrap.component.html' }) -export class Ng2BootstrapComponent { +export class NgxBootstrapComponent { public oneAtATime: boolean = true; public items = ['Item 1', 'Item 2', 'Item 3']; From 36df54aa9963ec366ad0dbe194568b06a60972fb Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Thu, 20 Apr 2017 04:28:47 +0100 Subject: [PATCH 014/142] Improved user UI for rest demo (#199) --- Client/app/app.module.ts | 2 + .../user-detail/user-detail.component.html | 9 +++ .../user-detail/user-detail.component.ts | 23 +++++++ .../app/containers/users/users.component.css | 64 +++++++++++++++++++ .../app/containers/users/users.component.html | 56 ++++++---------- .../app/containers/users/users.component.ts | 22 +++---- 6 files changed, 127 insertions(+), 49 deletions(-) create mode 100644 Client/app/components/user-detail/user-detail.component.html create mode 100644 Client/app/components/user-detail/user-detail.component.ts create mode 100644 Client/app/containers/users/users.component.css diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index 986d5de3..13f98f84 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -14,6 +14,7 @@ import { AppComponent } from './app.component'; import { NavMenuComponent } from './components/navmenu/navmenu.component'; import { HomeComponent } from './containers/home/home.component'; import { UsersComponent } from './containers/users/users.component'; +import { UserDetailComponent } from './components/user-detail/user-detail.component'; import { CounterComponent } from './containers/counter/counter.component'; import { ChatComponent } from './containers/chat/chat.component'; import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; @@ -39,6 +40,7 @@ export function createTranslateLoader(http: Http, baseHref) { NavMenuComponent, CounterComponent, UsersComponent, + UserDetailComponent, HomeComponent, ChatComponent, NgxBootstrapComponent diff --git a/Client/app/components/user-detail/user-detail.component.html b/Client/app/components/user-detail/user-detail.component.html new file mode 100644 index 00000000..585bbe40 --- /dev/null +++ b/Client/app/components/user-detail/user-detail.component.html @@ -0,0 +1,9 @@ +<div *ngIf="user"> + <h2>{{user.name}} details:</h2> + <div><label>id: </label>{{user.id}}</div> + <div> + <label>name: </label> + <input [(ngModel)]="user.name" placeholder="name" #details="ngModel" /> + </div> + <button [class.disabled]="details.pristine" class="btn btn-success" (click)="updateUser(user)">Save</button> +</div> \ No newline at end of file diff --git a/Client/app/components/user-detail/user-detail.component.ts b/Client/app/components/user-detail/user-detail.component.ts new file mode 100644 index 00000000..1d50f1f0 --- /dev/null +++ b/Client/app/components/user-detail/user-detail.component.ts @@ -0,0 +1,23 @@ +import { Component, Input } from '@angular/core'; +import { IUser } from '../../models/User'; +import { UserService } from '../../shared/user.service'; + +@Component({ + selector: 'user-detail', + templateUrl: './user-detail.component.html' +}) +export class UserDetailComponent { + @Input() user: IUser; + + constructor(private userService: UserService) { } + + + updateUser(user) { + this.userService.updateUser(user).subscribe(result => { + console.log('Put user result: ', result); + if (!result.ok) { + alert('There was an issue, Could not edit user'); + } + }); + } +} diff --git a/Client/app/containers/users/users.component.css b/Client/app/containers/users/users.component.css new file mode 100644 index 00000000..4e5440e1 --- /dev/null +++ b/Client/app/containers/users/users.component.css @@ -0,0 +1,64 @@ +.selected { + background-color: #CFD8DC !important; + color: white; +} + +.users { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 15em; +} + + .users li { + cursor: pointer; + position: relative; + left: 0; + background-color: #EEE; + margin: .5em; + padding: .3em 0; + height: 2.4em; + border-radius: 4px; + } + + .users li:hover { + color: #607D8B; + background-color: #DDD; + left: .1em; + } + + .users li.selected:hover { + background-color: #BBD8DC !important; + color: white; + } + + .users .text { + position: relative; + top: -3px; + } + + .users .badge { + display: inline-block; + font-size: small; + color: white; + padding: 0.8em 0.7em 0 0.7em; + background-color: #607D8B; + line-height: 1em; + position: relative; + left: -1px; + top: -4px; + height: 2.5em; + margin-right: .8em; + border-radius: 4px 0 0 4px; + } +button.delete { + background-color: #eee; + border: none; + padding: 5px 10px; + border-radius: 4px; + float: right; + margin-top: 2px; + margin-right: .8em; + background-color: gray !important; + color: white; +} \ No newline at end of file diff --git a/Client/app/containers/users/users.component.html b/Client/app/containers/users/users.component.html index e52eadc2..5bb77691 100644 --- a/Client/app/containers/users/users.component.html +++ b/Client/app/containers/users/users.component.html @@ -5,39 +5,25 @@ You can find the Web API Routes in <code>{{ "/Server/RestAPI/ ... "}}</code> </blockquote> -<p *ngIf="!users"><em>Loading...</em></p> +<div> + <label>User name:</label> <input #userName /> + <button class="btn btn-default" (click)="addUser(userName.value); userName.value=''"> + Add + </button> +</div> -<table class="table" *ngIf="users"> - <thead> - <tr> - <th>User ID</th> - <th>Name</th> - <th></th> - <th></th> - </tr> - </thead> - <tbody> - <tr *ngFor="let user of users" [@flyInOut]> - <td>{{ user.id }}</td> - <td> - <input class="form-control" [(ngModel)]="user.name" #name="ngModel" /> - </td> - <td> - <button *ngIf="name.dirty" class="btn btn-success" (click)="updateUser(user)">Save</button> - </td> - <td> - <button class="btn btn-danger" (click)="deleteUser(user)">Delete</button> - </td> - </tr> - <tr> - <td><b>Add new user</b></td> - <td> - <input class="form-control" [(ngModel)]="newUserName" #newUser="ngModel" /> - </td> - <td></td> - <td> - <button *ngIf="newUser.dirty" class="btn glyphicon-plus" (click)="addUser(newUserName)">+ Add New User</button> - </td> - </tr> - </tbody> -</table> +<p *ngIf="!users"><em>Loading...</em></p> +<h2>Users</h2> +<ul class="users"> + <li *ngFor="let user of users" + [class.selected]="user === selectedUser" + (click)="onSelect(user)" + [@flyInOut]> + <span class="badge">{{user.id}}</span> {{user.name}} + <button class="delete" + (click)="deleteUser(user); $event.stopPropagation()"> + x + </button> + </li> +</ul> +<user-detail [user]="selectedUser"></user-detail> diff --git a/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts index e477d31b..5af41169 100644 --- a/Client/app/containers/users/users.component.ts +++ b/Client/app/containers/users/users.component.ts @@ -7,8 +7,9 @@ import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @Component({ - selector: 'fetchdata', + selector: 'users', templateUrl: './users.component.html', + styleUrls: ['./users.component.css'], animations: [ // Animation example // Triggered in the ngFor with [@flyInOut] @@ -26,8 +27,8 @@ import { UserService } from '../../shared/user.service'; }) export class UsersComponent implements OnInit { - public newUserName: string; - public users: IUser[]; + private users: IUser[]; + private selectedUser: IUser; // Use "constructor"s only for dependency injection constructor(private userService: UserService) { } @@ -35,7 +36,6 @@ export class UsersComponent implements OnInit { // Here you want to handle anything with @Input()'s @Output()'s // Data retrieval / etc - this is when the Component is "ready" and wired up ngOnInit() { - this.newUserName = ''; this.userService.getUsers().subscribe(result => { console.log('Get user result: ', result); console.log('TransferHttp [GET] /api/users/allresult', result); @@ -43,6 +43,10 @@ export class UsersComponent implements OnInit { }); } + onSelect(user: IUser): void { + this.selectedUser = user; + } + deleteUser(user) { this.userService.deleteUser(user).subscribe(result => { console.log('Delete user result: ', result); @@ -55,21 +59,11 @@ export class UsersComponent implements OnInit { }); } - updateUser(user) { - this.userService.updateUser(user).subscribe(result => { - console.log('Put user result: ', result); - if (!result.ok) { - alert('There was an issue, Could not edit user'); - } - }); - } - addUser(newUserName) { this.userService.addUser(newUserName).subscribe(result => { console.log('Post user result: ', result); if (result.ok) { this.users.push(result.json()); - this.newUserName = ''; } else { alert('There was an issue, Could not edit user'); } From 5b7c2b95ccecf30d91ef973f6cfcef23b2399ed8 Mon Sep 17 00:00:00 2001 From: Owen <LiverpoolOwen@users.noreply.github.com> Date: Thu, 20 Apr 2017 14:06:54 +0100 Subject: [PATCH 015/142] Updated documentation to mention swagger (#201) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a1d190a7..1792b6eb 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Built in docker support through VS2017 - RestAPI (WebAPI) integration - SQL Database CRUD demo + - Swagger WebAPI documentation when running in development mode - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata) - **Angular 4.0.0** : From bc14e00a582c2998d85d8728a0b36200be21f8d1 Mon Sep 17 00:00:00 2001 From: Justin Maier <just.maier@gmail.com> Date: Thu, 20 Apr 2017 23:26:29 -0700 Subject: [PATCH 016/142] Ignore dist folders in IDE and clear them on MSBuild clean (#207) --- Asp2017.csproj | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Asp2017.csproj b/Asp2017.csproj index 50717f9e..8b606d53 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -17,6 +17,8 @@ <ItemGroup> <!-- Files not to show in IDE --> <None Remove="yarn.lock" /> + <Content Remove="wwwroot\dist\**" /> + <None Remove="Client\dist\**" /> <!-- Files not to publish (note that the 'dist' subfolders are re-added below) --> <Content Remove="Client\**" /> @@ -40,4 +42,11 @@ </ResolvedFileToPublish> </ItemGroup> </Target> + <Target Name="CleanDist" AfterTargets="Clean"> + <ItemGroup> + <FilesToDelete Include="Client\dist\**; wwwroot\dist\**"/> + </ItemGroup> + <Delete Files="@(FilesToDelete)" /> + <RemoveDir Directories="Client\dist; wwwroot\dist" /> + </Target> </Project> \ No newline at end of file From bd2a9da746af5a1d11838b707f0dfe6a9d87869b Mon Sep 17 00:00:00 2001 From: mikeysteele <mjwestcott@gmail.com> Date: Fri, 21 Apr 2017 23:53:18 +1000 Subject: [PATCH 017/142] another path change (#208) * another path change * this should probably be changed too --- webpack/webpack.server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webpack/webpack.server.js b/webpack/webpack.server.js index 1daffacb..e968400a 100644 --- a/webpack/webpack.server.js +++ b/webpack/webpack.server.js @@ -9,10 +9,10 @@ module.exports = { resolve: { extensions: ['.ts', '.js', '.json'], // An array of directory names to be resolved to the current directory - modules: [root('client'), root('node_modules')], + modules: [root('Client'), root('node_modules')], }, entry: { - 'main-server': root('./client/main.server.ts') + 'main-server': root('./Client/main.server.ts') }, output: { libraryTarget: 'commonjs', From 5fbda43fc168376293954dbe0dde481dbf8d56d7 Mon Sep 17 00:00:00 2001 From: M-R-Bishop <mike@recordtogether.com> Date: Wed, 26 Apr 2017 08:57:08 -0500 Subject: [PATCH 018/142] Fixed SignalR client connection for all browsers (#223) --- Client/polyfills/polyfills.ts | 26 +++++++++++++------------- package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Client/polyfills/polyfills.ts b/Client/polyfills/polyfills.ts index e2e900bd..fe3a6bd6 100644 --- a/Client/polyfills/polyfills.ts +++ b/Client/polyfills/polyfills.ts @@ -4,19 +4,19 @@ */ /** IE9, IE10 and IE11 requires all of the following polyfills. **/ -// import 'core-js/es6/symbol'; -// import 'core-js/es6/object'; -// import 'core-js/es6/function'; -// import 'core-js/es6/parse-int'; -// import 'core-js/es6/parse-float'; -// import 'core-js/es6/number'; -// import 'core-js/es6/math'; -// import 'core-js/es6/string'; -// import 'core-js/es6/date'; -// import 'core-js/es6/array'; -// import 'core-js/es6/regexp'; -// import 'core-js/es6/map'; -// import 'core-js/es6/set'; + import 'core-js/es6/symbol'; + import 'core-js/es6/object'; + import 'core-js/es6/function'; + import 'core-js/es6/parse-int'; + import 'core-js/es6/parse-float'; + import 'core-js/es6/number'; + import 'core-js/es6/math'; + import 'core-js/es6/string'; + import 'core-js/es6/date'; + import 'core-js/es6/array'; + import 'core-js/es6/regexp'; + import 'core-js/es6/map'; + import 'core-js/es6/set'; /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. diff --git a/package.json b/package.json index ca18648a..f72a0790 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "webpack": "^2.2.0", "webpack-hot-middleware": "^2.12.2", "webpack-merge": "^0.14.1", - "zone.js": "^0.8.5" + "zone.js": "^0.8.9" }, "devDependencies": { "@ngtools/webpack": "^1.3.0", From 6200416c7bbdbd5d69e9a085413d0fa58bd24190 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 26 Apr 2017 22:48:01 -0400 Subject: [PATCH 019/142] fix(users): private properties broke AoT - now public --- Client/app/containers/users/users.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts index 5af41169..c91a7087 100644 --- a/Client/app/containers/users/users.component.ts +++ b/Client/app/containers/users/users.component.ts @@ -27,8 +27,8 @@ import { UserService } from '../../shared/user.service'; }) export class UsersComponent implements OnInit { - private users: IUser[]; - private selectedUser: IUser; + users: IUser[]; + selectedUser: IUser; // Use "constructor"s only for dependency injection constructor(private userService: UserService) { } From a00b031b31eed29128056431ad7ace8f4513bd3e Mon Sep 17 00:00:00 2001 From: markoj21 <markoj21@gmail.com> Date: Thu, 4 May 2017 12:47:08 -0700 Subject: [PATCH 020/142] #235 (#236) Argumant of Type (params: BootFuncParams) #235 Froze the version of the package. Closes #235 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f72a0790..c076155c 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "@ngx-translate/http-loader": "0.0.3", "@types/node": "^7.0.12", "angular2-template-loader": "0.6.0", - "aspnet-prerendering": "^2.0.0", + "aspnet-prerendering": "2.0.3", "aspnet-webpack": "^1.0.17", "awesome-typescript-loader": "^3.0.0", "bootstrap": "^3.3.7", From 3917aead656fcdcbe3df95ba60e6ecb08bc724ae Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Fri, 5 May 2017 09:30:02 -0400 Subject: [PATCH 021/142] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1792b6eb..1d73860f 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,10 @@ export ASPNETCORE_ENVIRONMENT=Development # Upcoming Features: +- **Fix and update Webpack build / Vendor chunking and overall compilation speed.** ( important ) - Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there) - Potractor e2e testing -- Add Redux back in (maybe?) +- Add basic Redux State store (Will also hold state durijg HMR builds) - ~~Add Azure application insights module (or at least demo how to use it)~~ - ~~Add i18n support~~ - ~~DONE - Fix old README to match new project~~ From 73a3f98d3dbc729d7e88a028500dab8a410cd02f Mon Sep 17 00:00:00 2001 From: Tim Harker <tim@rekrah.com> Date: Fri, 5 May 2017 13:06:14 -0400 Subject: [PATCH 022/142] User Rest API Demo - Error Handling (#242) * User Rest API Demo - Error Handling Fixed error handling to actually show the alert, on error. * Console Log Changed from `alert` to `console.log`. --- Client/app/components/user-detail/user-detail.component.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Client/app/components/user-detail/user-detail.component.ts b/Client/app/components/user-detail/user-detail.component.ts index 1d50f1f0..9db88355 100644 --- a/Client/app/components/user-detail/user-detail.component.ts +++ b/Client/app/components/user-detail/user-detail.component.ts @@ -15,9 +15,8 @@ export class UserDetailComponent { updateUser(user) { this.userService.updateUser(user).subscribe(result => { console.log('Put user result: ', result); - if (!result.ok) { - alert('There was an issue, Could not edit user'); - } + }, error => { + console.log(`There was an issue. ${error._body}.`); }); } } From fa98c6b2b9977809ef6fa4e402ee3c3d78f7ff2b Mon Sep 17 00:00:00 2001 From: Tim Harker <tim@rekrah.com> Date: Fri, 5 May 2017 13:06:35 -0400 Subject: [PATCH 023/142] User Rest API Demo - Error Handling (#241) * User Rest API Demo - Error Handling Fixed error handling to actually show the alert, on error. * Console Log Changed from `alert` to `console.log`. --- Client/app/containers/users/users.component.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Client/app/containers/users/users.component.ts b/Client/app/containers/users/users.component.ts index c91a7087..9dd28781 100644 --- a/Client/app/containers/users/users.component.ts +++ b/Client/app/containers/users/users.component.ts @@ -53,9 +53,9 @@ export class UsersComponent implements OnInit { if (result.ok) { let position = this.users.indexOf(user); this.users.splice(position, 1); - } else { - alert('There was an issue, Could not delete user'); } + }, error => { + console.log(`There was an issue. ${error._body}.`); }); } @@ -64,9 +64,9 @@ export class UsersComponent implements OnInit { console.log('Post user result: ', result); if (result.ok) { this.users.push(result.json()); - } else { - alert('There was an issue, Could not edit user'); } + }, error => { + console.log(`There was an issue. ${error._body}.`); }); } } From e907562c6ca229a998e749a8eae4db5cc55e04b6 Mon Sep 17 00:00:00 2001 From: Anders Teern <andersk2@gmail.com> Date: Wed, 10 May 2017 15:46:57 +0200 Subject: [PATCH 024/142] Issue https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues/160 Replace Karma test runner with Jest (#248) Prevent coverage reports from being published Closes #160 --- .gitignore | 7 +++++-- Asp2017.csproj | 3 ++- Client/test/jestGlobalMocks.ts | 15 +++++++++++++++ Client/test/setupJest.ts | 2 ++ Client/tsconfig.spec.json | 19 +++++++++++++++++++ boot-tests.ts | 33 --------------------------------- karma.conf.js | 26 -------------------------- package.json | 27 ++++++++++++++++++++------- 8 files changed, 63 insertions(+), 69 deletions(-) create mode 100644 Client/test/jestGlobalMocks.ts create mode 100644 Client/test/setupJest.ts create mode 100644 Client/tsconfig.spec.json delete mode 100644 boot-tests.ts delete mode 100644 karma.conf.js diff --git a/.gitignore b/.gitignore index ac0cbb29..9812c3c4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ yarn.lock npm-debug.log.* - + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -255,4 +255,7 @@ _Pvt_Extensions .awcache # VSCode files -.vscode/chrome \ No newline at end of file +.vscode/chrome + +# Jest Code Coverage report +coverage/ diff --git a/Asp2017.csproj b/Asp2017.csproj index 8b606d53..0cb19457 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -19,6 +19,7 @@ <None Remove="yarn.lock" /> <Content Remove="wwwroot\dist\**" /> <None Remove="Client\dist\**" /> + <Content Remove="coverage\**" /> <!-- Files not to publish (note that the 'dist' subfolders are re-added below) --> <Content Remove="Client\**" /> @@ -49,4 +50,4 @@ <Delete Files="@(FilesToDelete)" /> <RemoveDir Directories="Client\dist; wwwroot\dist" /> </Target> -</Project> \ No newline at end of file +</Project> diff --git a/Client/test/jestGlobalMocks.ts b/Client/test/jestGlobalMocks.ts new file mode 100644 index 00000000..e5e90ebc --- /dev/null +++ b/Client/test/jestGlobalMocks.ts @@ -0,0 +1,15 @@ +const mock = () => { + let storage = {}; + return { + getItem: key => key in storage ? storage[key] : null, + setItem: (key, value) => storage[key] = value || '', + removeItem: key => delete storage[key], + clear: () => storage = {} + }; +}; + +Object.defineProperty(window, 'localStorage', {value: mock()}); +Object.defineProperty(window, 'sessionStorage', {value: mock()}); +Object.defineProperty(window, 'getComputedStyle', { + value: () => ['-webkit-appearance'] +}); diff --git a/Client/test/setupJest.ts b/Client/test/setupJest.ts new file mode 100644 index 00000000..1d3bd024 --- /dev/null +++ b/Client/test/setupJest.ts @@ -0,0 +1,2 @@ +import 'jest-preset-angular'; +import './jestGlobalMocks'; diff --git a/Client/tsconfig.spec.json b/Client/tsconfig.spec.json new file mode 100644 index 00000000..584cb0a4 --- /dev/null +++ b/Client/tsconfig.spec.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "module": "commonjs", + "target": "es5", + "baseUrl": "", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/boot-tests.ts b/boot-tests.ts deleted file mode 100644 index 7940a29d..00000000 --- a/boot-tests.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Load required polyfills and testing libraries -import '../polyfills/server.polyfills'; - -import 'zone.js/dist/long-stack-trace-zone'; -import 'zone.js/dist/proxy.js'; -import 'zone.js/dist/sync-test'; -import 'zone.js/dist/jasmine-patch'; -import 'zone.js/dist/async-test'; -import 'zone.js/dist/fake-async-test'; -import * as testing from '@angular/core/testing'; -import * as testingBrowser from '@angular/platform-browser-dynamic/testing'; - -// There's no typing for the `__karma__` variable. Just declare it as any -declare var __karma__: any; -declare var require: any; - -// Prevent Karma from running prematurely -__karma__.loaded = function () {}; - -// First, initialize the Angular testing environment -testing.getTestBed().initTestEnvironment( - testingBrowser.BrowserDynamicTestingModule, - testingBrowser.platformBrowserDynamicTesting() -); - -// Then we find all the tests -const context = require.context('../', true, /\.spec\.ts$/); - -// And load the modules -context.keys().map(context); - -// Finally, start Karma to run the tests -__karma__.start(); diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 65265132..00000000 --- a/karma.conf.js +++ /dev/null @@ -1,26 +0,0 @@ -// Karma configuration file, see link for more information -// https://karma-runner.github.io/0.13/config/configuration-file.html - -module.exports = function (config) { - config.set({ - basePath: '.', - frameworks: ['jasmine'], - files: [ - './wwwroot/dist/vendor.js', - './boot-tests.ts' - ], - preprocessors: { - './boot-tests.ts': ['webpack'] - }, - reporters: ['progress'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['Chrome'], - mime: { 'application/javascript': ['ts','tsx'] }, - singleRun: false, - webpack: require('./webpack.config.js')().filter(config => config.target !== 'node'), // Test against client bundle, because tests run in a browser - webpackMiddleware: { stats: 'errors-only' } - }); -}; diff --git a/package.json b/package.json index c076155c..5256ae29 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,26 @@ "name": "angular4-aspnetcore-universal", "version": "1.0.0-rc3", "scripts": { - "test": "karma start Client/test/karma.conf.js", + "test": "jest", + "test:watch": "npm run test -- --watch", + "test:ci": "npm run test -- --runInBand", + "test:coverage": "npm run test -- --coverage", "build:dev": "webpack --progress --color", "build:aot": "webpack --env.aot --env.client & webpack --env.aot --env.server" }, + "jest": { + "preset": "jest-preset-angular", + "setupTestFrameworkScriptFile": "./Client/test/setupJest.ts", + "globals": { + "__TS_CONFIG__": "Client/tsconfig.spec.json", + "__TRANSFORM_HTML__": true + }, + "coveragePathIgnorePatterns": [ + "/node_modules/", + "<rootDir>/Client/test/.*.ts" + ], + "coverageDirectory": "coverage" + }, "dependencies": { "@angular/animations": "^4.0.0", "@angular/common": "^4.0.0", @@ -60,15 +76,12 @@ "@ngtools/webpack": "^1.3.0", "@types/chai": "^3.4.34", "@types/jasmine": "^2.5.37", + "@types/jest": "^19.2.3", "chai": "^3.5.0", "codelyzer": "^3.0.0-beta.4", "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", + "jest": "^20.0.0", + "jest-preset-angular": "^2.0.1", "tslint": "^4.5.1" } } From ceaf0c850009cd78bdfa624c39fa8e6827831ced Mon Sep 17 00:00:00 2001 From: simple <maple_ye2003@hotmail.com> Date: Sat, 13 May 2017 21:00:50 -0600 Subject: [PATCH 025/142] Update temporary-aspnetcore-engine.ts (#256) --- Client/polyfills/temporary-aspnetcore-engine.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Client/polyfills/temporary-aspnetcore-engine.ts b/Client/polyfills/temporary-aspnetcore-engine.ts index d9b19b95..185b042a 100644 --- a/Client/polyfills/temporary-aspnetcore-engine.ts +++ b/Client/polyfills/temporary-aspnetcore-engine.ts @@ -126,10 +126,15 @@ export function ngAspnetCoreEngine( const LINKS = []; let TITLE = ''; - let STYLES_STRING = htmlDoc.substring( - htmlDoc.indexOf('<style ng-transition'), - htmlDoc.lastIndexOf('</style>') + 8 - ); + //let STYLES_STRING = htmlDoc.substring( + //htmlDoc.indexOf('<style ng-transition'), + //htmlDoc.lastIndexOf('</style>') + 8 + //); + let STYLES_STRING: string = htmlDoc.indexOf('<style ng-transition') > 0 + ? htmlDoc.substring( + htmlDoc.indexOf('<style ng-transition'), + htmlDoc.lastIndexOf('</style>') + 8) + : null; // STYLES_STRING = STYLES_STRING.replace(/\s/g, '').replace('<styleng-transition', '<style ng-transition'); const HEAD = AST_DOCUMENT.head; From b3fcacd62b0272051eb213912633c8e37b5208f4 Mon Sep 17 00:00:00 2001 From: simple <maple_ye2003@hotmail.com> Date: Sun, 14 May 2017 11:16:21 -0600 Subject: [PATCH 026/142] Update temporary-aspnetcore-engine.ts (#258) Sorry, my bad. --- Client/polyfills/temporary-aspnetcore-engine.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/polyfills/temporary-aspnetcore-engine.ts b/Client/polyfills/temporary-aspnetcore-engine.ts index 185b042a..218d4502 100644 --- a/Client/polyfills/temporary-aspnetcore-engine.ts +++ b/Client/polyfills/temporary-aspnetcore-engine.ts @@ -130,7 +130,7 @@ export function ngAspnetCoreEngine( //htmlDoc.indexOf('<style ng-transition'), //htmlDoc.lastIndexOf('</style>') + 8 //); - let STYLES_STRING: string = htmlDoc.indexOf('<style ng-transition') > 0 + let STYLES_STRING: string = htmlDoc.indexOf('<style ng-transition') > -1 ? htmlDoc.substring( htmlDoc.indexOf('<style ng-transition'), htmlDoc.lastIndexOf('</style>') + 8) From 98f1948f1b8bedfc48b97318835588dc50e0e3b3 Mon Sep 17 00:00:00 2001 From: "Stephen M. Redd" <redds@reddnet.net> Date: Wed, 17 May 2017 11:45:15 -0400 Subject: [PATCH 027/142] Issue #78 Updates typescript to 2.3.2, adds vs code launch configs (#228) Closes #25 - updated for 4.0 project --- .vscode/launch.json | 183 +++++++++++++++++++++++++++----------------- package.json | 2 +- 2 files changed, 115 insertions(+), 70 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4464938c..c3669663 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,73 +1,118 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "[Development] Launch Web", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart", - "launchBrowser": { - "enabled": true, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/Views" - } + "version": "0.2.0", + "compounds": [{ + "name": "[Development] Debug Server & Client", + "configurations": ["[Development] Launch Server (no browser)", "[Development] Debug TypeScript"] + }], + "configurations": [{ + + "name": "[Development] Debug TypeScript", + "type": "chrome", + "request": "launch", + "url": "http://localhost:5000/", + "webRoot": "${workspaceRoot}/wwwroot", + "sourceMapPathOverrides": { + "webpack:///./*": "${workspaceRoot}\\*" + } + }, + { + "name": "[Development] Launch Server (no browser)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": false, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" }, - { - "name": "[Production] Launch Web", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", - "args": [], - "cwd": "${workspaceRoot}", - "stopAtEntry": false, - "internalConsoleOptions": "openOnSessionStart", - "launchBrowser": { - "enabled": true, - "args": "${auto-detect-url}", - "windows": { - "command": "cmd.exe", - "args": "/C start ${auto-detect-url}" - }, - "osx": { - "command": "open" - }, - "linux": { - "command": "xdg-open" - } - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Production" - }, - "sourceFileMap": { - "/Views": "${workspaceRoot}/src/AspCoreServer/Views" - } + "osx": { + "command": "open" }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" + "linux": { + "command": "xdg-open" } - ] -} \ No newline at end of file + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/Views" + } + }, + { + "name": "[Development] Launch Web", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/Views" + } + }, + { + "name": "[Production] Launch Web", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", + "args": [], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "internalConsoleOptions": "openOnSessionStart", + "launchBrowser": { + "enabled": true, + "args": "${auto-detect-url}", + "windows": { + "command": "cmd.exe", + "args": "/C start ${auto-detect-url}" + }, + "osx": { + "command": "open" + }, + "linux": { + "command": "xdg-open" + } + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Production" + }, + "sourceFileMap": { + "/Views": "${workspaceRoot}/src/AspCoreServer/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + } + ] +} diff --git a/package.json b/package.json index 5256ae29..ea5a30f6 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "signalr": "^2.2.1", "style-loader": "^0.13.1", "to-string-loader": "^1.1.5", - "typescript": "^2.2.1", + "typescript": "^2.3.2", "url-loader": "^0.5.7", "webpack": "^2.2.0", "webpack-hot-middleware": "^2.12.2", From 6d6dba048b092aef014d66461bb1de38dc46c065 Mon Sep 17 00:00:00 2001 From: Jan Sviland <jan@sviland.org> Date: Thu, 18 May 2017 02:21:58 +0200 Subject: [PATCH 028/142] added a 404 error page (#261) --- Client/app/app.module.ts | 19 +++++++++++++++---- .../not-found/not-found.component.html | 10 ++++++++++ .../not-found/not-found.component.ts | 12 ++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 Client/app/containers/not-found/not-found.component.html create mode 100644 Client/app/containers/not-found/not-found.component.ts diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index 13f98f84..3a2fa5a9 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -17,6 +17,7 @@ import { UsersComponent } from './containers/users/users.component'; import { UserDetailComponent } from './components/user-detail/user-detail.component'; import { CounterComponent } from './containers/counter/counter.component'; import { ChatComponent } from './containers/chat/chat.component'; +import { NotFoundComponent } from './containers/not-found/not-found.component'; import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; import { LinkService } from './shared/link.service'; @@ -43,6 +44,7 @@ export function createTranslateLoader(http: Http, baseHref) { UserDetailComponent, HomeComponent, ChatComponent, + NotFoundComponent, NgxBootstrapComponent ], imports: [ @@ -132,11 +134,20 @@ export function createTranslateLoader(http: Http, baseHref) { ] } }, - + { path: 'lazy', loadChildren: './containers/+lazy/lazy.module#LazyModule'}, - - // All else fails - go home! - { path: '**', redirectTo: 'home' } + + { + path: '**', component: NotFoundComponent, + data: { + title: '404 - Not found', + meta: [{ name: 'description', content: '404 - Error' }], + links: [ + { rel: 'canonical', href: 'http://blogs.example.com/bootstrap/something' }, + { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/bootstrap-demo' } + ] + } + } ]) ], providers: [ diff --git a/Client/app/containers/not-found/not-found.component.html b/Client/app/containers/not-found/not-found.component.html new file mode 100644 index 00000000..347dcb93 --- /dev/null +++ b/Client/app/containers/not-found/not-found.component.html @@ -0,0 +1,10 @@ +<div class="wrapper"> + <header class="header header--large"> + <h1 class="title">Ahhhhhhhhhhh! This page doesn't exist</h1> + <h2 class="strapline">Not to worry. You can either head back to <a href="http://github.com/">our homepage</a>, or sit there and listen to a goat scream like + a human.</h2> + </header> + <div class="content fit-vid vid"> + <iframe src="http://www.youtube.com/embed/SIaFtAKnqBU?vq=hd720&rel=0&showinfo=0&controls=0&iv_load_policy=3&loop=1&playlist=SIaFtAKnqBU&modestbranding=1&autoplay=1" + width="560" height="315" frameborder="0" webkitAllowFullScreen allowFullScreen></iframe> + </div> diff --git a/Client/app/containers/not-found/not-found.component.ts b/Client/app/containers/not-found/not-found.component.ts new file mode 100644 index 00000000..0ebd7e90 --- /dev/null +++ b/Client/app/containers/not-found/not-found.component.ts @@ -0,0 +1,12 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'not-found', + templateUrl: './not-found.component.html' + // styleUrls: ['./not-found.component.css'] +}) +export class NotFoundComponent implements OnInit { + constructor() { } + + ngOnInit() { } +} From 77f3d0fe0a08655db19f77b4f068d9f27a18f5ad Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 24 May 2017 15:01:14 -0400 Subject: [PATCH 029/142] docs(readme): add note about npm run build:dev Adding for #279 just incase others need to do the same. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 1d73860f..94262a05 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,8 @@ VS2017 will automatically install all the neccessary npm & .NET dependencies whe Simply push F5 to start debugging ! +**Note**: If you get any errors after this such as `module not found: main.server` (or similar), open up command line and run `npm run build:dev` to make sure all the assets have been properly built by Webpack. + ### Visual Studio Code > Note: Make sure you have the C# extension & .NET Core Debugger installed. From 12e96bd796e68d0dfac5f4c986c40d4d0138fd2d Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 24 May 2017 15:04:56 -0400 Subject: [PATCH 030/142] docs(readme): add link to ng-aspnetcore-engine readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94262a05..c9117b87 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ Angular application gets serialized into a String, sent to the Browser, along wi The short-version is that we invoke that Node process, passing in our Request object & invoke the `boot-server` file, and we get back a nice object that we pass into .NETs `ViewData` object, and sprinkle through out our `Views/Shared/_Layout.cshtml` and `/Views/Home/index.cshtml` files! -A more detailed explanation can be found here: [TODO-add-link * You can read a more detailed explanation here](#) +A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/ng-aspnetcore-engine) ```csharp // Prerender / Serialize application (with Universal) From 355b99ef76c34e28c6f2ce391b77ace5f4893e80 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 25 May 2017 13:33:35 -0400 Subject: [PATCH 031/142] feat(errors): fix ssr errors to bubble up to browser closes #281 closes #277 --- Client/polyfills/temporary-aspnetcore-engine.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Client/polyfills/temporary-aspnetcore-engine.ts b/Client/polyfills/temporary-aspnetcore-engine.ts index 218d4502..a68986c2 100644 --- a/Client/polyfills/temporary-aspnetcore-engine.ts +++ b/Client/polyfills/temporary-aspnetcore-engine.ts @@ -196,17 +196,27 @@ export function ngAspnetCoreEngine( moduleRef.destroy(); }, (err) => { + // isStable subscription error (Template / code error) reject(err); }); + }, err => { + // bootstrapModuleFactory error + reject(err); }); + + }, err => { + // getFactory error + reject(err); }); } catch (ex) { + // try/catch error reject(ex); } }); + } /* ********************** Private / Internal ****************** */ From c7d2db9cff570d5507ccb01591b3e48a95aba5a2 Mon Sep 17 00:00:00 2001 From: TommyLCox <tommylcox@advsysunlimited.com> Date: Fri, 9 Jun 2017 22:55:29 -0500 Subject: [PATCH 032/142] Replaced ng2-bootstrap with ngx-bootstrap. (#299) Closes #226 --- Client/app/app.module.ts | 2 +- Client/app/containers/home/home.component.html | 2 +- package.json | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index 3a2fa5a9..7c92f103 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -4,7 +4,7 @@ import { CommonModule, APP_BASE_HREF } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; import { FormsModule } from '@angular/forms'; -import { Ng2BootstrapModule } from 'ng2-bootstrap'; +import { Ng2BootstrapModule } from 'ngx-bootstrap'; // i18n support import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; diff --git a/Client/app/containers/home/home.component.html b/Client/app/containers/home/home.component.html index 8a4eeb65..3f872f90 100644 --- a/Client/app/containers/home/home.component.html +++ b/Client/app/containers/home/home.component.html @@ -41,7 +41,7 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> </ul> </li> - <li>Bootstrap (ng2-bootstrap) : Bootstrap capable of being rendered even on the server.</li> + <li>Bootstrap (ngx-bootstrap) : Bootstrap capable of being rendered even on the server.</li> <li>Unit testing via karma & jasmine.</li> <!--<li>e2e testing via protractor.</li>--> </ul> diff --git a/package.json b/package.json index ea5a30f6..9486ba33 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,9 @@ "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", "json-loader": "^0.5.4", - "ng2-bootstrap": "^1.6.1", + "ng2-signalr": "^2.0.2", + "ngx-bootstrap": "^1.7.1", "node-sass": "^4.5.2", "preboot": "^4.5.2", "raw-loader": "^0.5.1", From 7756b809d1a646f402b05a5fd3274e8b1ef7ac35 Mon Sep 17 00:00:00 2001 From: Jan Sviland <jan@sviland.org> Date: Sun, 11 Jun 2017 21:01:45 +0200 Subject: [PATCH 033/142] added simple example of sitemap.xml (#289) --- Server/Controllers/HomeController.cs | 20 ++++++++++++++++++++ Startup.cs | 16 +++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 3feb71ce..2bc1ddaa 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -55,6 +55,26 @@ public async Task<IActionResult> Index() return View(); } + [Route("sitemap.xml")] + public async Task<IActionResult> SitemapXml() + { + String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; + + xml += "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; + xml += "<sitemap>"; + xml += "<loc>http://localhost:4251/home</loc>"; + xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; + xml += "</sitemap>"; + xml += "<sitemap>"; + xml += "<loc>http://localhost:4251/counter</loc>"; + xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; + xml += "</sitemap>"; + xml += "</sitemapindex>"; + + return Content(xml, "text/xml"); + + } + public IActionResult Error() { return View(); diff --git a/Startup.cs b/Startup.cs index 4e3c0510..d117fffd 100644 --- a/Startup.cs +++ b/Startup.cs @@ -101,14 +101,20 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF else { app.UseMvc(routes => - { - routes.MapRoute( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + routes.MapRoute( + "Sitemap", + "sitemap.xml", + new { controller = "Home", action = "SitemapXml" }); - routes.MapSpaFallbackRoute( + routes.MapSpaFallbackRoute( name: "spa-fallback", defaults: new { controller = "Home", action = "Index" }); + }); app.UseExceptionHandler("/Home/Error"); } From 04273f64e122fa5648f4d21fab76da30163eedfd Mon Sep 17 00:00:00 2001 From: Alex Klaus <aklaus@lawmaster.com.au> Date: Thu, 22 Jun 2017 11:17:49 +1000 Subject: [PATCH 034/142] Fixed error 500 on requesting swagger.json (#312) This fixes #310. Error 500 was occurring on requesting swagger.json (http://localhost:5000/swagger/v1/swagger.json) under the following conditions: - Pulled the project "as is" from GitHub - The environment is set to Development (as per this post) - Launch the project (F5) - Swagger UI gets loaded (http://localhost:5000/swagger/) See Swagger's requirements on method attributes in the comment here - https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/69#issuecomment-245347034 Closes #310 --- Server/Controllers/HomeController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 2bc1ddaa..7311cf45 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -14,6 +14,7 @@ namespace AspCoreServer.Controllers { public class HomeController : Controller { + [HttpGet] public async Task<IActionResult> Index() { var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); @@ -55,6 +56,7 @@ public async Task<IActionResult> Index() return View(); } + [HttpGet] [Route("sitemap.xml")] public async Task<IActionResult> SitemapXml() { From a138974dfcf74ed723a4066231e6b808f112ec98 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Mon, 3 Jul 2017 10:41:02 -0400 Subject: [PATCH 035/142] fix(typescript): pin typescript to 2.3.4 TS 2.4 brings on many different errors, we will wait until RxJs and Angular core have compatible fixes to those breaking changes. Officially angular is still on 2.2.2 --- package.json | 4 ++-- tsconfig.json | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 9486ba33..a63bda10 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "jquery": "^2.2.1", "json-loader": "^0.5.4", - "ng2-signalr": "^2.0.2", + "ng2-signalr": "2.0.4", "ngx-bootstrap": "^1.7.1", "node-sass": "^4.5.2", "preboot": "^4.5.2", @@ -66,7 +66,7 @@ "signalr": "^2.2.1", "style-loader": "^0.13.1", "to-string-loader": "^1.1.5", - "typescript": "^2.3.2", + "typescript": "2.3.4", "url-loader": "^0.5.7", "webpack": "^2.2.0", "webpack-hot-middleware": "^2.12.2", diff --git a/tsconfig.json b/tsconfig.json index 468cee8f..8728ffab 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ "sourceMap": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, + "noStrictGenericChecks": true, "lib": [ "es2016", "dom" From 1992852b0494b3628ea79a6265138515a2e01667 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 6 Jul 2017 11:52:08 -0400 Subject: [PATCH 036/142] docs(readme): add note on angular-application-insights usage https://github.com/MarkPieszak/angular-application-insights/blob/master/README.md#usage --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c9117b87..8f2e730b 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,14 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Microsoft Application Insights setup (for MVC & Web API routing) - Client-side Angular2 Application Insights integration - If you're using Azure simply install `npm i -S @markpieszak/ng-application-insights` as a dependencies. + - Note: Make sure only the Browser makes these calls ([usage info here](https://github.com/MarkPieszak/angular-application-insights/blob/master/README.md#usage)) - More information here: - https://github.com/MarkPieszak/angular-application-insights -```typescript - // Add the Module to your imports - ApplicationInsightsModule.forRoot({ - instrumentationKey: 'Your-Application-Insights-instrumentationKey' - }) -``` + ```typescript + // Add the Module to your imports + ApplicationInsightsModule.forRoot({ + instrumentationKey: 'Your-Application-Insights-instrumentationKey' + }) + ``` > Looking for the older 2.x branch? Go [here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/old-2.x-universal-branch) From e3dd1307d0afc1685bb6733f6981bf9d5e74af8b Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 6 Jul 2017 21:00:48 -0400 Subject: [PATCH 037/142] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..2085d375 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2017 Mark Pieszak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 53b240448b247d3e54a7c82d1b0a92e3901f45f6 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 6 Jul 2017 21:00:57 -0400 Subject: [PATCH 038/142] Delete LICENSE.md --- LICENSE.md | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 6e07e7c6..00000000 --- a/LICENSE.md +++ /dev/null @@ -1,11 +0,0 @@ -License - -The MIT License (MIT) - -Copyright (c) 2016-2017 Mark Pieszak - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file From 1db31066ce416b2001c061732e59fa12f6da351d Mon Sep 17 00:00:00 2001 From: Mark MacLin <cyberjaxx@users.noreply.github.com> Date: Fri, 7 Jul 2017 09:23:51 -0700 Subject: [PATCH 039/142] Build Fixes and Database Initialization (#324) * fix build errors and warnings * Database wasn't initialized in non development environment Fixes #331 --- Client/app/shared/route.resolver.ts | 6 +++--- Startup.cs | 4 ++-- package.json | 4 +--- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Client/app/shared/route.resolver.ts b/Client/app/shared/route.resolver.ts index 6bbef56b..9b7e02c0 100644 --- a/Client/app/shared/route.resolver.ts +++ b/Client/app/shared/route.resolver.ts @@ -1,10 +1,10 @@ -import { Injectable } from '@angular/core'; +import { Injectable } from '@angular/core'; import { Resolve } from '@angular/router'; -import { SignalR, SignalRConnection } from 'ng2-signalr'; +import { SignalR, ISignalRConnection } from 'ng2-signalr'; @Injectable() -export class ConnectionResolver implements Resolve<SignalRConnection> { +export class ConnectionResolver implements Resolve<ISignalRConnection> { constructor(private _signalR: SignalR) { } diff --git a/Startup.cs b/Startup.cs index d117fffd..54e456be 100644 --- a/Startup.cs +++ b/Startup.cs @@ -71,6 +71,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseStaticFiles(); + DbInitializer.Initialize(context); + if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); @@ -78,8 +80,6 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF HotModuleReplacement = true }); - DbInitializer.Initialize(context); - app.UseSwagger(); // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. diff --git a/package.json b/package.json index a63bda10..3d0e18c9 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,6 @@ "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", "json-loader": "^0.5.4", - "ng2-signalr": "2.0.4", "ngx-bootstrap": "^1.7.1", "node-sass": "^4.5.2", @@ -82,7 +81,6 @@ "codelyzer": "^3.0.0-beta.4", "jasmine-core": "^2.5.2", "jest": "^20.0.0", - "jest-preset-angular": "^2.0.1", - "tslint": "^4.5.1" + "jest-preset-angular": "^2.0.1" } } From a83a6a9bef283c39845467457dd50358b007c6f8 Mon Sep 17 00:00:00 2001 From: nemad <nemad@users.noreply.github.com> Date: Tue, 1 Aug 2017 01:13:07 +0200 Subject: [PATCH 040/142] #336 Fixed duplicate pages on hot module reload (#346) Closes #336 --- Client/main.browser.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Client/main.browser.ts b/Client/main.browser.ts index f1e76b9d..01723608 100644 --- a/Client/main.browser.ts +++ b/Client/main.browser.ts @@ -9,10 +9,6 @@ const rootElemTagName = 'app'; // Update this if you change your root component 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); - oldRootElem.parentNode.insertBefore(newRootElem, oldRootElem); modulePromise.then(appModule => appModule.destroy()); }); } else { From 9cfd9c5b64e4c37b3330603c75ac7afae6194e3b Mon Sep 17 00:00:00 2001 From: Isaac Levin <isaac2004@users.noreply.github.com> Date: Fri, 18 Aug 2017 10:08:16 -0400 Subject: [PATCH 041/142] Port Project to .NET CORE 2.0/EFCore 2.0/.NET Standard 2.0 (#371) Also fixed issue with Lazy Module (took + out of path) Closes #369 --- Asp2017.csproj | 18 +- Client/app/app.module.ts | 4 +- .../{+lazy => lazy}/lazy.component.ts | 0 .../containers/{+lazy => lazy}/lazy.module.ts | 0 Server/Controllers/HomeController.cs | 159 +++++++++--------- Startup.cs | 48 +++--- global.json | 3 - 7 files changed, 114 insertions(+), 118 deletions(-) rename Client/app/containers/{+lazy => lazy}/lazy.component.ts (100%) rename Client/app/containers/{+lazy => lazy}/lazy.module.ts (100%) delete mode 100644 global.json diff --git a/Asp2017.csproj b/Asp2017.csproj index 0cb19457..712d1b94 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -1,18 +1,16 @@ <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFramework>netcoreapp1.1</TargetFramework> + <TargetFramework>netcoreapp2.0</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore" Version="1.1.1" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.2" /> - <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="1.1.0" /> - <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="1.1.1" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.1" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0-rc3" /> + <!-- New Meta Package has SpaServices in It --> + <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> + <PackageReference Include="NETStandard.Library" Version="2.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /> </ItemGroup> <ItemGroup> <!-- Files not to show in IDE --> @@ -45,7 +43,7 @@ </Target> <Target Name="CleanDist" AfterTargets="Clean"> <ItemGroup> - <FilesToDelete Include="Client\dist\**; wwwroot\dist\**"/> + <FilesToDelete Include="Client\dist\**; wwwroot\dist\**" /> </ItemGroup> <Delete Files="@(FilesToDelete)" /> <RemoveDir Directories="Client\dist; wwwroot\dist" /> diff --git a/Client/app/app.module.ts b/Client/app/app.module.ts index 7c92f103..753aed35 100644 --- a/Client/app/app.module.ts +++ b/Client/app/app.module.ts @@ -1,4 +1,4 @@ -import { NgModule, Inject } from '@angular/core'; +import { NgModule, Inject } from '@angular/core'; import { RouterModule } from '@angular/router'; import { CommonModule, APP_BASE_HREF } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; @@ -135,7 +135,7 @@ export function createTranslateLoader(http: Http, baseHref) { } }, - { path: 'lazy', loadChildren: './containers/+lazy/lazy.module#LazyModule'}, + { path: 'lazy', loadChildren: './containers/lazy/lazy.module#LazyModule'}, { path: '**', component: NotFoundComponent, diff --git a/Client/app/containers/+lazy/lazy.component.ts b/Client/app/containers/lazy/lazy.component.ts similarity index 100% rename from Client/app/containers/+lazy/lazy.component.ts rename to Client/app/containers/lazy/lazy.component.ts diff --git a/Client/app/containers/+lazy/lazy.module.ts b/Client/app/containers/lazy/lazy.module.ts similarity index 100% rename from Client/app/containers/+lazy/lazy.module.ts rename to Client/app/containers/lazy/lazy.module.ts diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 7311cf45..3f19192d 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -12,32 +12,37 @@ namespace AspCoreServer.Controllers { - public class HomeController : Controller + public class HomeController : Controller + { + [HttpGet] + public async Task<IActionResult> Index() { - [HttpGet] - public async Task<IActionResult> Index() - { - var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); - var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); - - var applicationBasePath = hostEnv.ContentRootPath; - var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); - var unencodedPathAndQuery = requestFeature.RawTarget; - var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; - - // ** TransferData concept ** - // Here we can pass any Custom Data we want ! - - // By default we're passing down Cookies, Headers, Host from the Request object here - TransferData transferData = new TransferData(); - transferData.request = AbstractHttpContextRequestInfo(Request); - transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; - // Add more customData here, add it to the TransferData class - - // Prerender / Serialize application (with Universal) - var prerenderResult = await Prerenderer.RenderToString( + var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); + var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); + + var applicationBasePath = hostEnv.ContentRootPath; + var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); + var unencodedPathAndQuery = requestFeature.RawTarget; + var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; + + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + + // By default we're passing down Cookies, Headers, Host from the Request object here + TransferData transferData = new TransferData(); + transferData.request = AbstractHttpContextRequestInfo(Request); + transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; + // Add more customData here, add it to the TransferData class + + //Prerender now needs CancellationToken + System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); + System.Threading.CancellationToken cancelToken = cancelSource.Token; + + // Prerender / Serialize application (with Universal) + var prerenderResult = await Prerenderer.RenderToString( "/", nodeServices, + cancelToken, new JavaScriptModuleExport(applicationBasePath + "/Client/dist/main-server"), unencodedAbsoluteUrl, unencodedPathAndQuery, @@ -46,66 +51,66 @@ public async Task<IActionResult> Index() Request.PathBase.ToString() ); - ViewData["SpaHtml"] = prerenderResult.Html; // our <app> from Angular - ViewData["Title"] = prerenderResult.Globals["title"]; // set our <title> from Angular - ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place - ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags - ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags - ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; - - return View(); - } - - [HttpGet] - [Route("sitemap.xml")] - public async Task<IActionResult> SitemapXml() - { - String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; - - xml += "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; - xml += "<sitemap>"; - xml += "<loc>http://localhost:4251/home</loc>"; - xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; - xml += "</sitemap>"; - xml += "<sitemap>"; - xml += "<loc>http://localhost:4251/counter</loc>"; - xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; - xml += "</sitemap>"; - xml += "</sitemapindex>"; - - return Content(xml, "text/xml"); - - } - - public IActionResult Error() - { - return View(); - } - - private IRequest AbstractHttpContextRequestInfo(HttpRequest request) - { - - IRequest requestSimplified = new IRequest(); - requestSimplified.cookies = request.Cookies; - requestSimplified.headers = request.Headers; - requestSimplified.host = request.Host; - - return requestSimplified; - } + ViewData["SpaHtml"] = prerenderResult.Html; // our <app> from Angular + ViewData["Title"] = prerenderResult.Globals["title"]; // set our <title> from Angular + ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place + ViewData["Meta"] = prerenderResult.Globals["meta"]; // set our <meta> SEO tags + ViewData["Links"] = prerenderResult.Globals["links"]; // set our <link rel="canonical"> etc SEO tags + ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; + + return View(); } - public class IRequest + [HttpGet] + [Route("sitemap.xml")] + public async Task<IActionResult> SitemapXml() { - public object cookies { get; set; } - public object headers { get; set; } - public object host { get; set; } + String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; + + xml += "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; + xml += "<sitemap>"; + xml += "<loc>http://localhost:4251/home</loc>"; + xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; + xml += "</sitemap>"; + xml += "<sitemap>"; + xml += "<loc>http://localhost:4251/counter</loc>"; + xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; + xml += "</sitemap>"; + xml += "</sitemapindex>"; + + return Content(xml, "text/xml"); + + } + + public IActionResult Error() + { + return View(); } - public class TransferData + private IRequest AbstractHttpContextRequestInfo(HttpRequest request) { - public dynamic request { get; set; } - // Your data here ? - public object thisCameFromDotNET { get; set; } + IRequest requestSimplified = new IRequest(); + requestSimplified.cookies = request.Cookies; + requestSimplified.headers = request.Headers; + requestSimplified.host = request.Host; + + return requestSimplified; } + } + + public class IRequest + { + public object cookies { get; set; } + public object headers { get; set; } + public object host { get; set; } + } + + public class TransferData + { + public dynamic request { get; set; } + + // Your data here ? + public object thisCameFromDotNET { get; set; } + } } diff --git a/Startup.cs b/Startup.cs index 54e456be..00fc9291 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,17 +1,12 @@ -using System; +using System; using System.IO; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Antiforgery; - using Microsoft.AspNetCore.SpaServices.Webpack; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; - -using Microsoft.AspNetCore.NodeServices; using AspCoreServer.Data; using Swashbuckle.AspNetCore.Swagger; @@ -69,26 +64,27 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); - app.UseStaticFiles(); + app.UseStaticFiles(); - DbInitializer.Initialize(context); + DbInitializer.Initialize(context); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); - app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { + app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions + { HotModuleReplacement = true }); - app.UseSwagger(); - - // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. app.UseSwaggerUI(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1"); }); - app.MapWhen(x => !x.Request.Path.Value.StartsWith("/swagger"), builder => + // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint. + + + app.MapWhen(x => !x.Request.Path.Value.StartsWith("/swagger", StringComparison.OrdinalIgnoreCase), builder => { builder.UseMvc(routes => { @@ -101,19 +97,19 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF else { app.UseMvc(routes => - { - routes.MapRoute( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); - - routes.MapRoute( - "Sitemap", - "sitemap.xml", - new { controller = "Home", action = "SitemapXml" }); - - routes.MapSpaFallbackRoute( - name: "spa-fallback", - defaults: new { controller = "Home", action = "Index" }); + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + routes.MapRoute( + "Sitemap", + "sitemap.xml", + new { controller = "Home", action = "SitemapXml" }); + + routes.MapSpaFallbackRoute( + name: "spa-fallback", + defaults: new { controller = "Home", action = "Index" }); }); app.UseExceptionHandler("/Home/Error"); diff --git a/global.json b/global.json deleted file mode 100644 index 5c5ead2f..00000000 --- a/global.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "sdk": { "version": "1.0.0" } -} From cae82e8e3065ec67d5146820a85acc273fa8034c Mon Sep 17 00:00:00 2001 From: Isaac Levin <isaac2004@users.noreply.github.com> Date: Fri, 18 Aug 2017 10:36:19 -0400 Subject: [PATCH 042/142] Update Readme file to show Core 2.0 Support (#374) * Port Project to .NET CORE 2.0/EFCore 2.0/.NET Standard 2.0 Also fixed issue with Lazy Module (took + out of path) * update readme for Core 2.0 --- README.md | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8f2e730b..84e58f91 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ -# ASP.NET Core & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! <p align="center"> - <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core Angular 2+ Starter" title="ASP.NET Core Angular 2+ Starter"> + <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.0 Angular 2+ Starter" title="ASP.NET Core 2.0 Angular 2+ Starter"> </p> -### Harness the power of Angular 2+, ASP.NET Core, now with SEO ! +### Harness the power of Angular 2+, ASP.NET Core 2.0, now with SEO ! Angular SEO in action: @@ -15,7 +15,7 @@ Angular SEO in action: ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Angular Universal](https://github.com/angular/universal) and is meant to be an advanced starter -for both ASP.NET Core using Angular 4.0+, not only for the client-side, but to be rendered on the server for instant +for both ASP.NET Core 2.0 using Angular 4.0+, not only for the client-side, but to be rendered on the server for instant application paints (Note: If you don't need Universal (SSR) [read here](#faq) on how to disable it). This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). @@ -40,7 +40,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual > These are just some of the features found in this starter! -- ASP.NET 1.0 - VS2017 support now! +- ASP.NET 2.0 - VS2017 15.3 support now! - Azure delpoyment straight from VS2017 - Built in docker support through VS2017 - RestAPI (WebAPI) integration @@ -68,9 +68,8 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Typescript 2 - Codelyzer (for Real-time static code analysis) - VSCode & Atom provide real-time analysis out of the box. - - **NOTE**: Does not fully work with Visual Studio yet. (Even with VS2017 and .NET core 1.0) -- **ASP.NET Core 1.1** +- **ASP.NET Core 2.0** - Integration with NodeJS to provide pre-rendering, as well as any other Node module asset you want to use. @@ -100,7 +99,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual ### Visual Studio 2017 -Make sure you have .NET Core 1.0+ installed and/or VS2017. +Make sure you have .NET Core 2.0 installed and/or VS2017 15.3. VS2017 will automatically install all the neccessary npm & .NET dependencies when you open the project. Simply push F5 to start debugging ! @@ -134,12 +133,6 @@ export ASPNETCORE_ENVIRONMENT=Development - Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there) - Potractor e2e testing - Add basic Redux State store (Will also hold state durijg HMR builds) -- ~~Add Azure application insights module (or at least demo how to use it)~~ -- ~~Add i18n support~~ -- ~~DONE - Fix old README to match new project~~ -- ~~Add AoT compilation~~ -- ~~Add Bootstrap with SCSS~~ -- ~~Add REST API CRUD Demo~~ ---- From 8b5d2e08e3727de4f9802bd5b91b3b37af5d5bae Mon Sep 17 00:00:00 2001 From: Bill <OceansideBill@users.noreply.github.com> Date: Fri, 18 Aug 2017 07:37:49 -0700 Subject: [PATCH 043/142] Pull Request: 1.Changes to the top level scss file to support multiple display widths. 2.Changes to the navmenu component to make the 'hamburger' menu visible on mobile displays. (#372) --- Client/app/app.component.scss | 45 ++++++++++++++-- .../components/navmenu/navmenu.component.css | 51 ++++++++++++++++++- .../components/navmenu/navmenu.component.html | 20 ++++---- .../components/navmenu/navmenu.component.ts | 12 ++++- 4 files changed, 112 insertions(+), 16 deletions(-) diff --git a/Client/app/app.component.scss b/Client/app/app.component.scss index 9ba6e4bf..481063dc 100644 --- a/Client/app/app.component.scss +++ b/Client/app/app.component.scss @@ -14,13 +14,50 @@ $icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/'; Note: This Component has ViewEncapsulation.None so the styles will bleed out */ +@media (max-width: 767px) { + body { + background: #f1f1f1; + line-height: 18px; + padding-top: 30px; + } + + h1 { + border-bottom: 3px #4189C7 solid; + font-size: 24px; + } + + h2 { + font-size: 20px; + } + + h1, h2, h3 { + padding: 3px 0; + } +} + +@media (min-width: 768px) { + body { + background: #f1f1f1; + line-height: 18px; + padding-top: 0px; + } + + h1 { + border-bottom: 5px #4189C7 solid; + font-size: 36px; + } + + h2 { + font-size: 30px; + } + + h1, h2, h3 { + padding: 10px 0; + } +} -body { background: #f1f1f1; line-height: 18px; } ul { padding: 10px 25px; } ul li { padding: 5px 0; } -h1 { border-bottom: 5px #4189C7 solid; } -h1, h2, h3 { padding: 10px 0; } - blockquote { margin: 25px 10px; padding: 10px 35px 10px 10px; border-left: 10px #158a15 solid; background: #edffed; } blockquote a, blockquote a:hover { color: #068006; } diff --git a/Client/app/components/navmenu/navmenu.component.css b/Client/app/components/navmenu/navmenu.component.css index e15c6128..8d86aa03 100644 --- a/Client/app/components/navmenu/navmenu.component.css +++ b/Client/app/components/navmenu/navmenu.component.css @@ -1,4 +1,4 @@ -li .glyphicon { +li .glyphicon { margin-right: 10px; } @@ -19,10 +19,51 @@ li.link-active a:focus { z-index: 1; } +.icon-bar { + background-color: #4189C7; +} + + +@media (max-width: 767px) { + /* Apply for small displays */ + .main-nav { + width: 100%; + } + + .navbar-brand { + font-size: 14px; + background-color: #f1f1f1; + } + .navbar-toggle { + padding: 0px 5px; + margin-top: 0px; + height: 26px; + } + + .navbar-link { + margin-top: 4px; + margin-left: 45px; + position: fixed; + } + + .navbar-collapse { + background-color: white; + } + + .navbar a { + /* If a menu item's text is too long, truncate it */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 5px; + } +} + @media (min-width: 768px) { /* On small screens, convert the nav menu to a vertical sidebar */ .main-nav { height: 100%; + max-width: 330px; width: calc(25% - 20px); } .navbar { @@ -30,6 +71,13 @@ li.link-active a:focus { border-width: 0px; height: 100%; } + .navbar-brand{ + width: 100%; + } + .navbar-link { + display: block; + width: 100% + } .navbar-header { float: none; } @@ -51,7 +99,6 @@ li.link-active a:focus { } .navbar a { /* If a menu item's text is too long, truncate it */ - width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/Client/app/components/navmenu/navmenu.component.html b/Client/app/components/navmenu/navmenu.component.html index 277923a1..b92d8671 100644 --- a/Client/app/components/navmenu/navmenu.component.html +++ b/Client/app/components/navmenu/navmenu.component.html @@ -1,16 +1,18 @@ -<div class='main-nav'> +<div class='main-nav'> <div class='navbar'> <div class='navbar-header'> - <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'> - <span class='sr-only'>Toggle navigation</span> - <span class='icon-bar'></span> - <span class='icon-bar'></span> - <span class='icon-bar'></span> - </button> - <a class='navbar-brand' [routerLink]="['/home']">Angular 4 Universal & ASP.NET Core </a> + <div class='navbar-brand'> + <button type='button' class='navbar-toggle' (click)="collapseNavbar()"> + <span class='sr-only'>Toggle navigation</span> + <span class='icon-bar'></span> + <span class='icon-bar'></span> + <span class='icon-bar'></span> + </button> + <a [routerLink]="['/home']" class='navbar-link'>Angular 4 Universal & ASP.NET Core</a> + </div> </div> <div class='clearfix'></div> - <div class='navbar-collapse collapse'> + <div class='navbar-collapse {{collapse}}'> <ul class='nav navbar-nav'> <li [routerLinkActive]="['link-active']"> <a [routerLink]="['/home']"> diff --git a/Client/app/components/navmenu/navmenu.component.ts b/Client/app/components/navmenu/navmenu.component.ts index 7a1691cb..3287431d 100644 --- a/Client/app/components/navmenu/navmenu.component.ts +++ b/Client/app/components/navmenu/navmenu.component.ts @@ -1,9 +1,19 @@ -import { Component } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'nav-menu', templateUrl: './navmenu.component.html', styleUrls: ['./navmenu.component.css'] }) + export class NavMenuComponent { + collapse: string = "collapse"; + + collapseNavbar(): void { + if (this.collapse.length > 1) { + this.collapse = ""; + } else { + this.collapse = "collapse"; + } + } } From 309b61681e3d0d3e7388273e20ee6615a97a2a59 Mon Sep 17 00:00:00 2001 From: markoj21 <markoj21@gmail.com> Date: Fri, 18 Aug 2017 07:38:06 -0700 Subject: [PATCH 044/142] Update transfer-http.ts (#321) Fixed the missing body parameters, which caused issues with the options. --- Client/modules/transfer-http/transfer-http.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Client/modules/transfer-http/transfer-http.ts b/Client/modules/transfer-http/transfer-http.ts index e8667b58..3f9b5d84 100644 --- a/Client/modules/transfer-http/transfer-http.ts +++ b/Client/modules/transfer-http/transfer-http.ts @@ -37,7 +37,7 @@ export class TransferHttp { * Performs a request with `post` http method. */ post(url: string, body: any, options?: RequestOptionsArgs): Observable<any> { - return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { + return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { return this.http.post(url, body, options); }); } @@ -46,7 +46,7 @@ export class TransferHttp { */ put(url: string, body: any, options?: RequestOptionsArgs): Observable<any> { - return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { + return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { return this.http.put(url, body, options); }); } @@ -62,7 +62,7 @@ export class TransferHttp { * Performs a request with `patch` http method. */ patch(url: string, body: any, options?: RequestOptionsArgs): Observable<any> { - return this.getPostData(url, body, options, (url: string, options: RequestOptionsArgs) => { + return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { return this.http.patch(url, body.options); }); } From fe9ab9c151c79a8ff919ff2f71253c243e2ffb97 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Fri, 18 Aug 2017 10:42:21 -0400 Subject: [PATCH 045/142] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 84e58f91..183ab0b0 100644 --- a/README.md +++ b/README.md @@ -408,6 +408,7 @@ To support IE9 through IE11 open the `polyfills.ts` file in the `polyfills` fold Many thanks go out to Steve Sanderson ([@SteveSandersonMS](https://github.com/SteveSandersonMS)) from Microsoft and his amazing work on JavaScriptServices and integrating the world of Node with ASP.NET Core. Also thank you to the many Contributors ! +- [@Isaac2004](https://github.com/Isaac2004) - [@AbrarJahin](https://github.com/AbrarJahin) - [@LiverpoolOwen](https://github.com/LiverpoolOwen) - [@hakonamatata](https://github.com/hakonamatata) From 55684364d0bfcfa9bee99a98deb6c1829bf446bd Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 24 Aug 2017 10:15:06 -0400 Subject: [PATCH 046/142] WIP - Re-sync with JavaScriptServices (#376) * WIP - Re-sync with JavaScriptServices Main goal is to re-align a bit more with JavaScriptServices to ensure people coming from there, wanting to add additional features, have an easier time syncing with this repo. Adds back vendor builds, Adds back fast HMR (but waiting for https://github.com/aspnet/JavaScriptServices/issues/1204) AoT faster Cleans up multiple tsconfigs etc etc... * fix gitignore * fix HMR, VS builds, misc updates * add package-lock to gitignore closes #307 closes #318 closes #296 closes #294 closes #271 closes #267 closes #230 closes #161 --- .gitignore | 6 +- .vscode/launch.json | 6 +- Asp2017.csproj | 67 ++++++---- Client/main.server.aot.ts | 41 ------ Client/tsconfig.browser.json | 9 -- Client/tsconfig.server.aot.json | 8 -- Client/tsconfig.server.json | 9 -- {Client => ClientApp}/app/app.component.html | 2 +- {Client => ClientApp}/app/app.component.scss | 0 {Client => ClientApp}/app/app.component.ts | 0 .../app/app.module.browser.ts | 7 +- .../app/app.module.server.ts | 6 +- {Client => ClientApp}/app/app.module.ts | 15 ++- .../components/navmenu/navmenu.component.css | 0 .../components/navmenu/navmenu.component.html | 0 .../components/navmenu/navmenu.component.ts | 0 .../user-detail/user-detail.component.html | 0 .../user-detail/user-detail.component.ts | 0 .../app/containers/chat/chat.component.html | 0 .../app/containers/chat/chat.component.scss | 0 .../app/containers/chat/chat.component.ts | 2 +- .../containers/counter/counter.component.html | 0 .../counter/counter.component.spec.ts | 0 .../containers/counter/counter.component.ts | 0 .../app/containers/home/home.component.html | 15 +-- .../app/containers/home/home.component.ts | 6 +- .../app/containers/lazy/lazy.component.ts | 0 .../app/containers/lazy/lazy.module.ts | 0 .../ngx-bootstrap.component.html | 0 .../ngx-bootstrap.component.ts | 0 .../not-found/not-found.component.html | 0 .../not-found/not-found.component.ts | 0 .../app/containers/users/users.component.css | 0 .../app/containers/users/users.component.html | 0 .../app/containers/users/users.component.ts | 0 {Client => ClientApp}/app/models/User.ts | 0 .../app/shared/constants/baseurl.constants.ts | 0 .../app/shared/constants/request.ts | 0 .../app/shared/link.service.ts | 0 .../app/shared/route.resolver.ts | 0 .../app/shared/user.service.ts | 0 .../boot.browser.ts | 4 +- .../boot.server.ts | 4 +- .../transfer-http/transfer-http.module.ts | 0 .../modules/transfer-http/transfer-http.ts | 0 .../browser-transfer-state.module.ts | 0 .../server-transfer-state.module.ts | 0 .../transfer-state/server-transfer-state.ts | 0 .../modules/transfer-state/transfer-state.ts | 0 .../polyfills/browser.polyfills.ts | 0 {Client => ClientApp}/polyfills/polyfills.ts | 0 {Client => ClientApp}/polyfills/rx-imports.ts | 0 .../polyfills/server.polyfills.ts | 0 .../polyfills/temporary-aspnetcore-engine.ts | 0 {Client => ClientApp}/test/jestGlobalMocks.ts | 0 {Client => ClientApp}/test/setupJest.ts | 0 {Client => ClientApp}/tsconfig.spec.json | 0 {Client => ClientApp}/typings.d.ts | 0 README.md | 3 +- Server/Controllers/HomeController.cs | 2 +- Startup.cs | 3 +- Views/Home/Index.cshtml | 3 +- Views/Shared/_Layout.cshtml | 43 +++--- package.json | 43 +++--- tsconfig.json | 7 +- webpack.additions.js | 18 +++ webpack.config.js | 125 ++++++++++++------ webpack.config.vendor.js | 97 ++++++++++++++ 68 files changed, 341 insertions(+), 210 deletions(-) delete mode 100644 Client/main.server.aot.ts delete mode 100644 Client/tsconfig.browser.json delete mode 100644 Client/tsconfig.server.aot.json delete mode 100644 Client/tsconfig.server.json rename {Client => ClientApp}/app/app.component.html (94%) rename {Client => ClientApp}/app/app.component.scss (100%) rename {Client => ClientApp}/app/app.component.ts (100%) rename Client/app/browser-app.module.ts => ClientApp/app/app.module.browser.ts (94%) rename Client/app/server-app.module.ts => ClientApp/app/app.module.server.ts (90%) rename {Client => ClientApp}/app/app.module.ts (96%) rename {Client => ClientApp}/app/components/navmenu/navmenu.component.css (100%) rename {Client => ClientApp}/app/components/navmenu/navmenu.component.html (100%) rename {Client => ClientApp}/app/components/navmenu/navmenu.component.ts (100%) rename {Client => ClientApp}/app/components/user-detail/user-detail.component.html (100%) rename {Client => ClientApp}/app/components/user-detail/user-detail.component.ts (100%) rename {Client => ClientApp}/app/containers/chat/chat.component.html (100%) rename Client/app/containers/chat/chat.component.css => ClientApp/app/containers/chat/chat.component.scss (100%) rename {Client => ClientApp}/app/containers/chat/chat.component.ts (97%) rename {Client => ClientApp}/app/containers/counter/counter.component.html (100%) rename {Client => ClientApp}/app/containers/counter/counter.component.spec.ts (100%) rename {Client => ClientApp}/app/containers/counter/counter.component.ts (100%) rename {Client => ClientApp}/app/containers/home/home.component.html (90%) rename {Client => ClientApp}/app/containers/home/home.component.ts (78%) rename {Client => ClientApp}/app/containers/lazy/lazy.component.ts (100%) rename {Client => ClientApp}/app/containers/lazy/lazy.module.ts (100%) rename {Client => ClientApp}/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html (100%) rename {Client => ClientApp}/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts (100%) rename {Client => ClientApp}/app/containers/not-found/not-found.component.html (100%) rename {Client => ClientApp}/app/containers/not-found/not-found.component.ts (100%) rename {Client => ClientApp}/app/containers/users/users.component.css (100%) rename {Client => ClientApp}/app/containers/users/users.component.html (100%) rename {Client => ClientApp}/app/containers/users/users.component.ts (100%) rename {Client => ClientApp}/app/models/User.ts (100%) rename {Client => ClientApp}/app/shared/constants/baseurl.constants.ts (100%) rename {Client => ClientApp}/app/shared/constants/request.ts (100%) rename {Client => ClientApp}/app/shared/link.service.ts (100%) rename {Client => ClientApp}/app/shared/route.resolver.ts (100%) rename {Client => ClientApp}/app/shared/user.service.ts (100%) rename Client/main.browser.ts => ClientApp/boot.browser.ts (77%) rename Client/main.server.ts => ClientApp/boot.server.ts (94%) rename {Client => ClientApp}/modules/transfer-http/transfer-http.module.ts (100%) rename {Client => ClientApp}/modules/transfer-http/transfer-http.ts (100%) rename {Client => ClientApp}/modules/transfer-state/browser-transfer-state.module.ts (100%) rename {Client => ClientApp}/modules/transfer-state/server-transfer-state.module.ts (100%) rename {Client => ClientApp}/modules/transfer-state/server-transfer-state.ts (100%) rename {Client => ClientApp}/modules/transfer-state/transfer-state.ts (100%) rename {Client => ClientApp}/polyfills/browser.polyfills.ts (100%) rename {Client => ClientApp}/polyfills/polyfills.ts (100%) rename {Client => ClientApp}/polyfills/rx-imports.ts (100%) rename {Client => ClientApp}/polyfills/server.polyfills.ts (100%) rename {Client => ClientApp}/polyfills/temporary-aspnetcore-engine.ts (100%) rename {Client => ClientApp}/test/jestGlobalMocks.ts (100%) rename {Client => ClientApp}/test/setupJest.ts (100%) rename {Client => ClientApp}/tsconfig.spec.json (100%) rename {Client => ClientApp}/typings.d.ts (100%) create mode 100644 webpack.additions.js create mode 100644 webpack.config.vendor.js diff --git a/.gitignore b/.gitignore index 9812c3c4..2f41fd03 100644 --- a/.gitignore +++ b/.gitignore @@ -35,7 +35,7 @@ Obj/ .vs/ /wwwroot/dist/ -/Client/dist/ +/ClientApp/dist/ # MSTest test Results [Tt]est[Rr]esult*/ @@ -187,6 +187,7 @@ BundleArtifacts/ !*.[Cc]ache/ # Others +*.db ClientBin/ ~$* *~ @@ -259,3 +260,6 @@ _Pvt_Extensions # Jest Code Coverage report coverage/ + +.DS_Store +package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json index c3669663..08d4edf9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, @@ -51,7 +51,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, @@ -82,7 +82,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp1.1/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, diff --git a/Asp2017.csproj b/Asp2017.csproj index 712d1b94..e373e9ae 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -1,51 +1,68 @@ <Project ToolsVersion="15.0" Sdk="Microsoft.NET.Sdk.Web"> + <PropertyGroup> - <TargetFramework>netcoreapp2.0</TargetFramework> + <TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">netcoreapp2.0</TargetFramework> + <TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">TargetFrameworkOverride</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> + <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> <IsPackable>false</IsPackable> </PropertyGroup> - <ItemGroup> - <!-- New Meta Package has SpaServices in It --> + + <ItemGroup Condition="'$(TargetFrameworkOverride)' == ''"> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> + </ItemGroup> + <ItemGroup Condition="'$(TargetFrameworkOverride)' != ''"> + <PackageReference Include="Microsoft.AspNetCore" Version="2.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="2.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.0.0" /> + </ItemGroup> + <ItemGroup> <PackageReference Include="NETStandard.Library" Version="2.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /> </ItemGroup> - <ItemGroup> - <!-- Files not to show in IDE --> - <None Remove="yarn.lock" /> - <Content Remove="wwwroot\dist\**" /> - <None Remove="Client\dist\**" /> - <Content Remove="coverage\**" /> - <!-- Files not to publish (note that the 'dist' subfolders are re-added below) --> - <Content Remove="Client\**" /> + <ItemGroup> + <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" /> </ItemGroup> + <ItemGroup> - <Content Include="Client\tsconfig.browser.json" /> - <Content Include="Client\tsconfig.server.aot.json" /> - <Content Include="Client\tsconfig.server.json" /> + <!-- Files not to publish (note that the 'dist' subfolders are re-added below) --> + <Content Remove="ClientApp\**" /> </ItemGroup> - <Target Name="RunWebpack" AfterTargets="ComputeFilesToPublish"> + + <!--/-:cnd:noEmit --> + <Target Name="DebugRunWebpack" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('wwwroot\dist') "> + <!-- Ensure Node.js is installed --> + <Exec Command="node --version" ContinueOnError="true"> + <Output TaskParameter="ExitCode" PropertyName="ErrorCode" /> + </Exec> + <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." /> + + <!-- In development, the dist files won't exist on the first run or when cloning to + a different machine, so rebuild them if not already present. --> + <Message Importance="high" Text="Performing first-run Webpack build..." /> + <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js" /> + <Exec Command="node node_modules/webpack/bin/webpack.js" /> + </Target> + <!--/+:cnd:noEmit --> + + <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> <Exec Command="npm install" /> - <Exec Command="node node_modules/webpack/bin/webpack.js --env.aot --env.client" /> - <Exec Command="node node_modules/webpack/bin/webpack.js --env.aot --env.server" /> + <Exec Command="node node_modules/webpack/bin/webpack.js --config webpack.config.vendor.js --env.prod" /> + <Exec Command="node node_modules/webpack/bin/webpack.js --env.prod" /> + <!-- Include the newly-built files in the publish output --> <ItemGroup> - <DistFiles Include="wwwroot\dist\**; Client\dist\**" /> + <DistFiles Include="wwwroot\dist\**; ClientApp\dist\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </ResolvedFileToPublish> </ItemGroup> </Target> - <Target Name="CleanDist" AfterTargets="Clean"> - <ItemGroup> - <FilesToDelete Include="Client\dist\**; wwwroot\dist\**" /> - </ItemGroup> - <Delete Files="@(FilesToDelete)" /> - <RemoveDir Directories="Client\dist; wwwroot\dist" /> - </Target> + </Project> diff --git a/Client/main.server.aot.ts b/Client/main.server.aot.ts deleted file mode 100644 index eb4f0699..00000000 --- a/Client/main.server.aot.ts +++ /dev/null @@ -1,41 +0,0 @@ -import 'zone.js/dist/zone-node'; -import './polyfills/server.polyfills'; -import { enableProdMode } from '@angular/core'; -import { INITIAL_CONFIG } from '@angular/platform-server'; -import { APP_BASE_HREF } from '@angular/common'; -import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; - -import { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; -// Grab the (Node) server-specific NgModule -import { ServerAppModuleNgFactory } from './ngfactory/app/server-app.module.ngfactory'; -// Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; - -enableProdMode(); - -export default createServerRenderer(params => { - - // Platform-server provider configuration - const setupOptions: IEngineOptions = { - appSelector: '<app></app>', - ngModule: ServerAppModuleNgFactory, - request: params, - providers: [ - // Optional - Any other Server providers you want to pass (remember you'll have to provide them for the Browser as well) - ] - }; - - return ngAspnetCoreEngine(setupOptions).then(response => { - // Apply your transferData to response.globals - response.globals.transferData = createTransferScript({ - someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object' - }); - - return ({ - html: response.html, - globals: response.globals - }); - }); -}); - -/* -------- THIS FILE IS TEMPORARY and will be gone when @ngtools/webpack can handle dual files (w server) ---------- */ diff --git a/Client/tsconfig.browser.json b/Client/tsconfig.browser.json deleted file mode 100644 index d04b9e60..00000000 --- a/Client/tsconfig.browser.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "angularCompilerOptions": { - "entryModule": "./app/browser-app.module#BrowserAppModule" - }, - "exclude": [ - "./main.server.aot.ts" - ] -} diff --git a/Client/tsconfig.server.aot.json b/Client/tsconfig.server.aot.json deleted file mode 100644 index 2407460a..00000000 --- a/Client/tsconfig.server.aot.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.server.json", - "angularCompilerOptions": { - "genDir": "ngfactory", - "entryModule": "./app/server-app.module#ServerAppModule" - }, - "exclude": [] -} diff --git a/Client/tsconfig.server.json b/Client/tsconfig.server.json deleted file mode 100644 index 6fecd055..00000000 --- a/Client/tsconfig.server.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig.json", - "angularCompilerOptions": { - "entryModule": "./app/server-app.module#ServerAppModule" - }, - "exclude": [ - "./main.server.aot.ts" - ] -} diff --git a/Client/app/app.component.html b/ClientApp/app/app.component.html similarity index 94% rename from Client/app/app.component.html rename to ClientApp/app/app.component.html index 12a962a5..a3e3bf9b 100644 --- a/Client/app/app.component.html +++ b/ClientApp/app/app.component.html @@ -3,4 +3,4 @@ </div> <div class="col-sm-9 body-content"> <router-outlet></router-outlet> -</div> \ No newline at end of file +</div> diff --git a/Client/app/app.component.scss b/ClientApp/app/app.component.scss similarity index 100% rename from Client/app/app.component.scss rename to ClientApp/app/app.component.scss diff --git a/Client/app/app.component.ts b/ClientApp/app/app.component.ts similarity index 100% rename from Client/app/app.component.ts rename to ClientApp/app/app.component.ts diff --git a/Client/app/browser-app.module.ts b/ClientApp/app/app.module.browser.ts similarity index 94% rename from Client/app/browser-app.module.ts rename to ClientApp/app/app.module.browser.ts index 0150d22c..0af291d4 100644 --- a/Client/app/browser-app.module.ts +++ b/ClientApp/app/app.module.browser.ts @@ -6,7 +6,7 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { SignalRModule, SignalRConfiguration } from 'ng2-signalr'; import { ORIGIN_URL } from './shared/constants/baseurl.constants'; -import { AppModule } from './app.module'; +import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; import { REQUEST } from './shared/constants/request'; import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; @@ -41,7 +41,7 @@ export function getRequest() { BrowserTransferStateModule, // Our Common AppModule - AppModule, + AppModuleShared, SignalRModule.forRoot(createConfig) ], @@ -58,5 +58,4 @@ export function getRequest() { } ] }) -export class BrowserAppModule { -} +export class AppModule { } diff --git a/Client/app/server-app.module.ts b/ClientApp/app/app.module.server.ts similarity index 90% rename from Client/app/server-app.module.ts rename to ClientApp/app/app.module.server.ts index ac91990a..9ccbfb52 100644 --- a/Client/app/server-app.module.ts +++ b/ClientApp/app/app.module.server.ts @@ -3,7 +3,7 @@ import { ServerModule } from '@angular/platform-server'; import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { AppModule } from './app.module'; +import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module'; import { TransferState } from '../modules/transfer-state/transfer-state'; @@ -20,10 +20,10 @@ import { TransferState } from '../modules/transfer-state/transfer-state'; ServerTransferStateModule, // Our Common AppModule - AppModule + AppModuleShared ] }) -export class ServerAppModule { +export class AppModule { constructor(private transferState: TransferState) { } diff --git a/Client/app/app.module.ts b/ClientApp/app/app.module.ts similarity index 96% rename from Client/app/app.module.ts rename to ClientApp/app/app.module.ts index 753aed35..57498bb8 100644 --- a/Client/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -1,5 +1,5 @@ import { NgModule, Inject } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterModule, PreloadAllModules } from '@angular/router'; import { CommonModule, APP_BASE_HREF } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; import { FormsModule } from '@angular/forms'; @@ -134,9 +134,9 @@ export function createTranslateLoader(http: Http, baseHref) { ] } }, - + { path: 'lazy', loadChildren: './containers/lazy/lazy.module#LazyModule'}, - + { path: '**', component: NotFoundComponent, data: { @@ -148,7 +148,12 @@ export function createTranslateLoader(http: Http, baseHref) { ] } } - ]) + ], { + // Router options + useHash: false, + preloadingStrategy: PreloadAllModules, + initialNavigation: 'enabled' + }) ], providers: [ LinkService, @@ -157,5 +162,5 @@ export function createTranslateLoader(http: Http, baseHref) { TranslateModule ] }) -export class AppModule { +export class AppModuleShared { } diff --git a/Client/app/components/navmenu/navmenu.component.css b/ClientApp/app/components/navmenu/navmenu.component.css similarity index 100% rename from Client/app/components/navmenu/navmenu.component.css rename to ClientApp/app/components/navmenu/navmenu.component.css diff --git a/Client/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html similarity index 100% rename from Client/app/components/navmenu/navmenu.component.html rename to ClientApp/app/components/navmenu/navmenu.component.html diff --git a/Client/app/components/navmenu/navmenu.component.ts b/ClientApp/app/components/navmenu/navmenu.component.ts similarity index 100% rename from Client/app/components/navmenu/navmenu.component.ts rename to ClientApp/app/components/navmenu/navmenu.component.ts diff --git a/Client/app/components/user-detail/user-detail.component.html b/ClientApp/app/components/user-detail/user-detail.component.html similarity index 100% rename from Client/app/components/user-detail/user-detail.component.html rename to ClientApp/app/components/user-detail/user-detail.component.html diff --git a/Client/app/components/user-detail/user-detail.component.ts b/ClientApp/app/components/user-detail/user-detail.component.ts similarity index 100% rename from Client/app/components/user-detail/user-detail.component.ts rename to ClientApp/app/components/user-detail/user-detail.component.ts diff --git a/Client/app/containers/chat/chat.component.html b/ClientApp/app/containers/chat/chat.component.html similarity index 100% rename from Client/app/containers/chat/chat.component.html rename to ClientApp/app/containers/chat/chat.component.html diff --git a/Client/app/containers/chat/chat.component.css b/ClientApp/app/containers/chat/chat.component.scss similarity index 100% rename from Client/app/containers/chat/chat.component.css rename to ClientApp/app/containers/chat/chat.component.scss diff --git a/Client/app/containers/chat/chat.component.ts b/ClientApp/app/containers/chat/chat.component.ts similarity index 97% rename from Client/app/containers/chat/chat.component.ts rename to ClientApp/app/containers/chat/chat.component.ts index ca9dc8ea..8bbd9ed9 100644 --- a/Client/app/containers/chat/chat.component.ts +++ b/ClientApp/app/containers/chat/chat.component.ts @@ -11,7 +11,7 @@ export class ChatMessage { @Component({ selector: 'chat', templateUrl: './chat.component.html', - styleUrls: ['./chat.component.css'] + styleUrls: ['./chat.component.scss'] }) export class ChatComponent implements OnInit { diff --git a/Client/app/containers/counter/counter.component.html b/ClientApp/app/containers/counter/counter.component.html similarity index 100% rename from Client/app/containers/counter/counter.component.html rename to ClientApp/app/containers/counter/counter.component.html diff --git a/Client/app/containers/counter/counter.component.spec.ts b/ClientApp/app/containers/counter/counter.component.spec.ts similarity index 100% rename from Client/app/containers/counter/counter.component.spec.ts rename to ClientApp/app/containers/counter/counter.component.spec.ts diff --git a/Client/app/containers/counter/counter.component.ts b/ClientApp/app/containers/counter/counter.component.ts similarity index 100% rename from Client/app/containers/counter/counter.component.ts rename to ClientApp/app/containers/counter/counter.component.ts diff --git a/Client/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html similarity index 90% rename from Client/app/containers/home/home.component.html rename to ClientApp/app/containers/home/home.component.html index 3f872f90..06d801e7 100644 --- a/Client/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -1,23 +1,20 @@ -<h1> - {{ title }}</h1> +<h1>{{ title }}</h1> <blockquote> <strong>Enjoy the latest features from .NET Core & Angular 4.0!</strong> <br> For more info check the repo here: <a href="https://github.com/MarkPieszak/aspnetcore-angular2-universal">AspNetCore-Angular2-Universal repo</a> - <br><br> </blockquote> - <div class="row"> <div class="col-lg-6"> <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <ul> - <li>ASP.NET Core 1.1 :: ( Visual Studio 2017 )</li> + <li>ASP.NET Core 2.0 :: ( Visual Studio 2017 )</li> <li> Angular 4.* front-end UI framework <ul> - <li>Angular **platform-server** (Universal moved into Core here) - server-side rendering for SEO, deep-linking, and + <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.</li> <!--<li>HMR State Management - Don't lose your applications state during HMR!</li>--> <li>AoT (Ahead-of-time) production compilation for even faster Prod builds.</li> @@ -33,7 +30,7 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> </ul>--> </li> <li> - Webpack 2 + Webpack <ul> <!--<li>TS2 aware path support</li>--> <li>Hot Module Reloading/Replacement for an amazing development experience.</li> @@ -65,11 +62,11 @@ <h2>{{ 'HOME_ISSUES_TITLE' | translate }}</h2> <h2> {{ 'SWITCH_LANGUAGE' | translate }}</h2> <button class="btn btn-default" (click)="setLanguage('en')"> - <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} + <span class="flag-icon flag-icon-us"></span> {{ 'ENGLISH' | translate }} </button> <button class="btn btn-default" (click)="setLanguage('no')"> - <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} + <span class="flag-icon flag-icon-no"></span> {{ 'NORWEGIAN' | translate }} </button> </div> diff --git a/Client/app/containers/home/home.component.ts b/ClientApp/app/containers/home/home.component.ts similarity index 78% rename from Client/app/containers/home/home.component.ts rename to ClientApp/app/containers/home/home.component.ts index ed7e6f0a..3065c0b1 100644 --- a/Client/app/containers/home/home.component.ts +++ b/ClientApp/app/containers/home/home.component.ts @@ -8,10 +8,12 @@ import { TranslateService } from '@ngx-translate/core'; }) export class HomeComponent implements OnInit { - title: string = 'Angular 4.0 Universal & ASP.NET Core advanced starter-kit'; + title: string = 'Angular 4.0 Universal & ASP.NET Core 2.0 advanced starter-kit'; // Use "constructor"s only for dependency injection - constructor(public translate: TranslateService) { } + constructor( + public translate: TranslateService + ) { } // Here you want to handle anything with @Input()'s @Output()'s // Data retrieval / etc - this is when the Component is "ready" and wired up diff --git a/Client/app/containers/lazy/lazy.component.ts b/ClientApp/app/containers/lazy/lazy.component.ts similarity index 100% rename from Client/app/containers/lazy/lazy.component.ts rename to ClientApp/app/containers/lazy/lazy.component.ts diff --git a/Client/app/containers/lazy/lazy.module.ts b/ClientApp/app/containers/lazy/lazy.module.ts similarity index 100% rename from Client/app/containers/lazy/lazy.module.ts rename to ClientApp/app/containers/lazy/lazy.module.ts diff --git a/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html similarity index 100% rename from Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html rename to ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.html diff --git a/Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts b/ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts similarity index 100% rename from Client/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts rename to ClientApp/app/containers/ngx-bootstrap-demo/ngx-bootstrap.component.ts diff --git a/Client/app/containers/not-found/not-found.component.html b/ClientApp/app/containers/not-found/not-found.component.html similarity index 100% rename from Client/app/containers/not-found/not-found.component.html rename to ClientApp/app/containers/not-found/not-found.component.html diff --git a/Client/app/containers/not-found/not-found.component.ts b/ClientApp/app/containers/not-found/not-found.component.ts similarity index 100% rename from Client/app/containers/not-found/not-found.component.ts rename to ClientApp/app/containers/not-found/not-found.component.ts diff --git a/Client/app/containers/users/users.component.css b/ClientApp/app/containers/users/users.component.css similarity index 100% rename from Client/app/containers/users/users.component.css rename to ClientApp/app/containers/users/users.component.css diff --git a/Client/app/containers/users/users.component.html b/ClientApp/app/containers/users/users.component.html similarity index 100% rename from Client/app/containers/users/users.component.html rename to ClientApp/app/containers/users/users.component.html diff --git a/Client/app/containers/users/users.component.ts b/ClientApp/app/containers/users/users.component.ts similarity index 100% rename from Client/app/containers/users/users.component.ts rename to ClientApp/app/containers/users/users.component.ts diff --git a/Client/app/models/User.ts b/ClientApp/app/models/User.ts similarity index 100% rename from Client/app/models/User.ts rename to ClientApp/app/models/User.ts diff --git a/Client/app/shared/constants/baseurl.constants.ts b/ClientApp/app/shared/constants/baseurl.constants.ts similarity index 100% rename from Client/app/shared/constants/baseurl.constants.ts rename to ClientApp/app/shared/constants/baseurl.constants.ts diff --git a/Client/app/shared/constants/request.ts b/ClientApp/app/shared/constants/request.ts similarity index 100% rename from Client/app/shared/constants/request.ts rename to ClientApp/app/shared/constants/request.ts diff --git a/Client/app/shared/link.service.ts b/ClientApp/app/shared/link.service.ts similarity index 100% rename from Client/app/shared/link.service.ts rename to ClientApp/app/shared/link.service.ts diff --git a/Client/app/shared/route.resolver.ts b/ClientApp/app/shared/route.resolver.ts similarity index 100% rename from Client/app/shared/route.resolver.ts rename to ClientApp/app/shared/route.resolver.ts diff --git a/Client/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts similarity index 100% rename from Client/app/shared/user.service.ts rename to ClientApp/app/shared/user.service.ts diff --git a/Client/main.browser.ts b/ClientApp/boot.browser.ts similarity index 77% rename from Client/main.browser.ts rename to ClientApp/boot.browser.ts index 01723608..a7830543 100644 --- a/Client/main.browser.ts +++ b/ClientApp/boot.browser.ts @@ -1,7 +1,7 @@ import './polyfills/browser.polyfills'; import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { BrowserAppModule } from './app/browser-app.module'; +import { AppModule } from './app/app.module.browser'; const rootElemTagName = 'app'; // Update this if you change your root component selector @@ -15,4 +15,4 @@ if (module['hot']) { enableProdMode(); } -const modulePromise = platformBrowserDynamic().bootstrapModule(BrowserAppModule); +const modulePromise = platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/Client/main.server.ts b/ClientApp/boot.server.ts similarity index 94% rename from Client/main.server.ts rename to ClientApp/boot.server.ts index e20e4f9f..d93a4f56 100644 --- a/Client/main.server.ts +++ b/ClientApp/boot.server.ts @@ -7,7 +7,7 @@ import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; import { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; // Grab the (Node) server-specific NgModule -import { ServerAppModule } from './app/server-app.module'; +import { AppModule } from './app/app.module.server'; // Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; @@ -18,7 +18,7 @@ export default createServerRenderer((params: BootFuncParams) => { // Platform-server provider configuration const setupOptions: IEngineOptions = { appSelector: '<app></app>', - ngModule: ServerAppModule, + ngModule: AppModule, request: params, providers: [ // Optional - Any other Server providers you want to pass (remember you'll have to provide them for the Browser as well) diff --git a/Client/modules/transfer-http/transfer-http.module.ts b/ClientApp/modules/transfer-http/transfer-http.module.ts similarity index 100% rename from Client/modules/transfer-http/transfer-http.module.ts rename to ClientApp/modules/transfer-http/transfer-http.module.ts diff --git a/Client/modules/transfer-http/transfer-http.ts b/ClientApp/modules/transfer-http/transfer-http.ts similarity index 100% rename from Client/modules/transfer-http/transfer-http.ts rename to ClientApp/modules/transfer-http/transfer-http.ts diff --git a/Client/modules/transfer-state/browser-transfer-state.module.ts b/ClientApp/modules/transfer-state/browser-transfer-state.module.ts similarity index 100% rename from Client/modules/transfer-state/browser-transfer-state.module.ts rename to ClientApp/modules/transfer-state/browser-transfer-state.module.ts diff --git a/Client/modules/transfer-state/server-transfer-state.module.ts b/ClientApp/modules/transfer-state/server-transfer-state.module.ts similarity index 100% rename from Client/modules/transfer-state/server-transfer-state.module.ts rename to ClientApp/modules/transfer-state/server-transfer-state.module.ts diff --git a/Client/modules/transfer-state/server-transfer-state.ts b/ClientApp/modules/transfer-state/server-transfer-state.ts similarity index 100% rename from Client/modules/transfer-state/server-transfer-state.ts rename to ClientApp/modules/transfer-state/server-transfer-state.ts diff --git a/Client/modules/transfer-state/transfer-state.ts b/ClientApp/modules/transfer-state/transfer-state.ts similarity index 100% rename from Client/modules/transfer-state/transfer-state.ts rename to ClientApp/modules/transfer-state/transfer-state.ts diff --git a/Client/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts similarity index 100% rename from Client/polyfills/browser.polyfills.ts rename to ClientApp/polyfills/browser.polyfills.ts diff --git a/Client/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts similarity index 100% rename from Client/polyfills/polyfills.ts rename to ClientApp/polyfills/polyfills.ts diff --git a/Client/polyfills/rx-imports.ts b/ClientApp/polyfills/rx-imports.ts similarity index 100% rename from Client/polyfills/rx-imports.ts rename to ClientApp/polyfills/rx-imports.ts diff --git a/Client/polyfills/server.polyfills.ts b/ClientApp/polyfills/server.polyfills.ts similarity index 100% rename from Client/polyfills/server.polyfills.ts rename to ClientApp/polyfills/server.polyfills.ts diff --git a/Client/polyfills/temporary-aspnetcore-engine.ts b/ClientApp/polyfills/temporary-aspnetcore-engine.ts similarity index 100% rename from Client/polyfills/temporary-aspnetcore-engine.ts rename to ClientApp/polyfills/temporary-aspnetcore-engine.ts diff --git a/Client/test/jestGlobalMocks.ts b/ClientApp/test/jestGlobalMocks.ts similarity index 100% rename from Client/test/jestGlobalMocks.ts rename to ClientApp/test/jestGlobalMocks.ts diff --git a/Client/test/setupJest.ts b/ClientApp/test/setupJest.ts similarity index 100% rename from Client/test/setupJest.ts rename to ClientApp/test/setupJest.ts diff --git a/Client/tsconfig.spec.json b/ClientApp/tsconfig.spec.json similarity index 100% rename from Client/tsconfig.spec.json rename to ClientApp/tsconfig.spec.json diff --git a/Client/typings.d.ts b/ClientApp/typings.d.ts similarity index 100% rename from Client/typings.d.ts rename to ClientApp/typings.d.ts diff --git a/README.md b/README.md index 183ab0b0..040dc0e0 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ npm install && npm run build:dev && dotnet restore # or yarn install ``` -If you're running the project from command line with `dotnet run` make sure you set your environment variables to Development (otherwise things like HMR won't work). +If you're running the project from command line with `dotnet run` make sure you set your environment variables to Development (otherwise things like HMR might not work). ```bash # on Windows: @@ -129,7 +129,6 @@ export ASPNETCORE_ENVIRONMENT=Development # Upcoming Features: -- **Fix and update Webpack build / Vendor chunking and overall compilation speed.** ( important ) - Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there) - Potractor e2e testing - Add basic Redux State store (Will also hold state durijg HMR builds) diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 3f19192d..87b41ff2 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -43,7 +43,7 @@ public async Task<IActionResult> Index() "/", nodeServices, cancelToken, - new JavaScriptModuleExport(applicationBasePath + "/Client/dist/main-server"), + new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), unencodedAbsoluteUrl, unencodedPathAndQuery, transferData, // Our simplified Request object & any other CustommData you want to send! diff --git a/Startup.cs b/Startup.cs index 00fc9291..7736f9e9 100644 --- a/Startup.cs +++ b/Startup.cs @@ -73,7 +73,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseDeveloperExceptionPage(); app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { - HotModuleReplacement = true + HotModuleReplacement = true, + HotModuleReplacementEndpoint = "/dist/__webpack_hmr" }); app.UseSwagger(); app.UseSwaggerUI(c => diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index 418bb1fd..b0796562 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,6 +1,7 @@ @Html.Raw(ViewData["SpaHtml"]) +<script src="http://github.com/~/dist/vendor.js" asp-append-version="true"></script> @section scripts { <!-- Our webpack bundle --> - <script src="http://github.com/~/dist/main-browser.js" asp-append-version="true"></script> + <script src="http://github.com/~/dist/main-client.js" asp-append-version="true"></script> } diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 8d8fb071..509c52db 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,32 +1,33 @@ <!DOCTYPE html> <html> - <head> - <base href="http://github.com/" /> - <title>@ViewData["Title"] + + + @ViewData["Title"] - - - @Html.Raw(ViewData["Meta"]) - @Html.Raw(ViewData["Links"]) + + + @Html.Raw(ViewData["Meta"]) + @Html.Raw(ViewData["Links"]) - + + - @Html.Raw(ViewData["Styles"]) + @Html.Raw(ViewData["Styles"]) - - - @RenderBody() + + + @RenderBody() - - + + - + - - @Html.Raw(ViewData["TransferData"]) + + @Html.Raw(ViewData["TransferData"]) - @RenderSection("scripts", required: false) - + @RenderSection("scripts", required: false) + diff --git a/package.json b/package.json index 3d0e18c9..f3b00472 100644 --- a/package.json +++ b/package.json @@ -6,40 +6,44 @@ "test:watch": "npm run test -- --watch", "test:ci": "npm run test -- --runInBand", "test:coverage": "npm run test -- --coverage", - "build:dev": "webpack --progress --color", - "build:aot": "webpack --env.aot --env.client & webpack --env.aot --env.server" + "build:dev": "npm run build:vendor && npm run build:webpack", + "build:webpack": "webpack --progress --color", + "build:prod": "npm run build:vendor -- --env.prod && npm run build:dev -- --env.prod", + "build:vendor": "webpack --config webpack.config.vendor.js --progress --color" }, "jest": { "preset": "jest-preset-angular", - "setupTestFrameworkScriptFile": "./Client/test/setupJest.ts", + "setupTestFrameworkScriptFile": "./ClientApp/test/setupJest.ts", "globals": { - "__TS_CONFIG__": "Client/tsconfig.spec.json", + "__TS_CONFIG__": "ClientApp/tsconfig.spec.json", "__TRANSFORM_HTML__": true }, "coveragePathIgnorePatterns": [ "/node_modules/", - "/Client/test/.*.ts" + "/ClientApp/test/.*.ts" ], "coverageDirectory": "coverage" }, "dependencies": { - "@angular/animations": "^4.0.0", - "@angular/common": "^4.0.0", - "@angular/compiler": "^4.0.0", - "@angular/compiler-cli": "^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", + "@angular/animations": "^4.3.0", + "@angular/common": "^4.3.0", + "@angular/compiler": "^4.3.0", + "@angular/compiler-cli": "^4.3.0", + "@angular/core": "^4.3.0", + "@angular/forms": "^4.3.0", + "@angular/http": "^4.3.0", + "@angular/platform-browser": "^4.3.0", + "@angular/platform-browser-dynamic": "^4.3.0", + "@angular/platform-server": "^4.3.0", + "@angular/router": "^4.3.0", + "@nguniversal/aspnetcore-engine": "^1.0.0-beta.2", "@ngx-translate/core": "^6.0.1", "@ngx-translate/http-loader": "0.0.3", "@types/node": "^7.0.12", + "angular2-router-loader": "^0.3.4", "angular2-template-loader": "0.6.0", "aspnet-prerendering": "2.0.3", - "aspnet-webpack": "^1.0.17", + "aspnet-webpack": "^2.0.1", "awesome-typescript-loader": "^3.0.0", "bootstrap": "^3.3.7", "bootstrap-sass": "^3.3.7", @@ -55,7 +59,7 @@ "jquery": "^2.2.1", "json-loader": "^0.5.4", "ng2-signalr": "2.0.4", - "ngx-bootstrap": "^1.7.1", + "ngx-bootstrap": "^2.0.0-beta.2", "node-sass": "^4.5.2", "preboot": "^4.5.2", "raw-loader": "^0.5.1", @@ -78,7 +82,8 @@ "@types/jasmine": "^2.5.37", "@types/jest": "^19.2.3", "chai": "^3.5.0", - "codelyzer": "^3.0.0-beta.4", + "codelyzer": "^3.0.0", + "tslint": "^5.0.0", "jasmine-core": "^2.5.2", "jest": "^20.0.0", "jest-preset-angular": "^2.0.1" diff --git a/tsconfig.json b/tsconfig.json index 8728ffab..0e68aae4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,10 +4,11 @@ "module": "es2015", "target": "es5", "noImplicitAny": false, - "sourceMap": false, + "sourceMap": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "noStrictGenericChecks": true, + "skipDefaultLibCheck": true, + "skipLibCheck": true, "lib": [ "es2016", "dom" @@ -15,6 +16,6 @@ "types": [ "node" ] }, "include": [ - "Client" + "ClientApp" ] } diff --git a/webpack.additions.js b/webpack.additions.js new file mode 100644 index 00000000..62bf4bb3 --- /dev/null +++ b/webpack.additions.js @@ -0,0 +1,18 @@ +/* [ Webpack Additions ] + * + * This file contains ADD-ONS we are adding on-top of the traditional JavaScriptServices repo + * We do this so that those already using JavaScriptServices can easily figure out how to combine this repo into it. + */ + +// Shared rules[] we need to add +const sharedModuleRules = [ + // sass + { test: /\.scss$/, loaders: ['to-string-loader', 'css-loader', 'sass-loader'] }, + // font-awesome + { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000' } +]; + + +module.exports = { + sharedModuleRules +}; diff --git a/webpack.config.js b/webpack.config.js index 76980b9c..06435301 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,46 +1,97 @@ -const path = require('path'); -const webpackMerge = require('webpack-merge'); -const commonPartial = require('./webpack/webpack.common'); -const clientPartial = require('./webpack/webpack.client'); -const serverPartial = require('./webpack/webpack.server'); -const prodPartial = require('./webpack/webpack.prod'); -const { getAotPlugin } = require('./webpack/webpack.aot'); +/* + * Webpack (JavaScriptServices) with a few changes & updates + * - This is to keep us inline with JSServices, and help those using that template to add things from this one + * + * Things updated or changed: + * module -> rules [] + * .ts$ test : Added 'angular2-router-loader' for lazy-loading in development + * added ...sharedModuleRules (for scss & font-awesome loaders) + */ -module.exports = function (options, webpackOptions) { - options = options || {}; - webpackOptions = webpackOptions || {}; +const path = require('path'); +const webpack = require('webpack'); +const merge = require('webpack-merge'); +const AotPlugin = require('@ngtools/webpack').AotPlugin; +const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; - if (options.aot) { - console.log(`Running build for ${options.client ? 'client' : 'server'} with AoT Compilation`) - } +const { sharedModuleRules } = require('./webpack.additions'); - const serverConfig = webpackMerge({}, commonPartial, serverPartial, { - entry: options.aot ? { 'main-server' : './Client/main.server.aot.ts' } : serverPartial.entry, // Temporary +module.exports = (env) => { + // Configuration in common to both client-side and server-side bundles + const isDevBuild = !(env && env.prod); + const sharedConfig = { + stats: { modules: false }, + context: __dirname, + resolve: { extensions: [ '.js', '.ts' ] }, + output: { + filename: '[name].js', + publicPath: 'dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix + }, + module: { + rules: [ + { test: /\.ts$/, use: isDevBuild ? ['awesome-typescript-loader?silent=true', 'angular2-template-loader', 'angular2-router-loader'] : '@ngtools/webpack' }, + { test: /\.html$/, use: 'html-loader?minimize=false' }, + { test: /\.css$/, use: [ 'to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] }, + { test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }, + ...sharedModuleRules + ] + }, + plugins: [new CheckerPlugin()] + }; + + // Configuration for client-side bundle suitable for running in browsers + const clientBundleOutputDir = './wwwroot/dist'; + const clientBundleConfig = merge(sharedConfig, { + entry: { 'main-client': './ClientApp/boot.browser.ts' }, + output: { path: path.join(__dirname, clientBundleOutputDir) }, plugins: [ - getAotPlugin('server', !!options.aot) - ] + new webpack.DllReferencePlugin({ + context: __dirname, + manifest: require('./wwwroot/dist/vendor-manifest.json') + }) + ].concat(isDevBuild ? [ + // Plugins that apply in development builds only + new webpack.SourceMapDevToolPlugin({ + filename: '[file].map', // Remove this line if you prefer inline source maps + moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk + }) + ] : [ + // Plugins that apply in production builds only + new webpack.optimize.UglifyJsPlugin(), + new AotPlugin({ + tsConfigPath: './tsconfig.json', + entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), + exclude: ['./**/*.server.ts'] + }) + ]) }); - let clientConfig = webpackMerge({}, commonPartial, clientPartial, { + // Configuration for server-side (prerendering) bundle suitable for running in Node + const serverBundleConfig = merge(sharedConfig, { + resolve: { mainFields: ['main'] }, + entry: { 'main-server': './ClientApp/boot.server.ts' }, plugins: [ - getAotPlugin('client', !!options.aot) - ] + new webpack.DllReferencePlugin({ + context: __dirname, + manifest: require('./ClientApp/dist/vendor-manifest.json'), + sourceType: 'commonjs2', + name: './vendor' + }) + ].concat(isDevBuild ? [] : [ + // Plugins that apply in production builds only + new AotPlugin({ + tsConfigPath: './tsconfig.json', + entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'), + exclude: ['./**/*.browser.ts'] + }) + ]), + output: { + libraryTarget: 'commonjs', + path: path.join(__dirname, './ClientApp/dist') + }, + target: 'node', + devtool: 'inline-source-map' }); - if (webpackOptions.prod) { - clientConfig = webpackMerge({}, clientConfig, prodPartial); - } - - const configs = []; - if (!options.aot) { - configs.push(clientConfig, serverConfig); - - } else if (options.client) { - configs.push(clientConfig); - - } else if (options.server) { - configs.push(serverConfig); - } - - return configs; -} + return [clientBundleConfig, serverBundleConfig]; +}; diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js new file mode 100644 index 00000000..7ae8a6d2 --- /dev/null +++ b/webpack.config.vendor.js @@ -0,0 +1,97 @@ +const path = require('path'); +const webpack = require('webpack'); +const ExtractTextPlugin = require('extract-text-webpack-plugin'); +const merge = require('webpack-merge'); +const treeShakableModules = [ + '@angular/animations', + '@angular/common', + '@angular/compiler', + '@angular/core', + '@angular/forms', + '@angular/http', + '@angular/platform-browser', + '@angular/platform-browser-dynamic', + '@angular/router', + 'ngx-bootstrap', + 'zone.js', +]; +const nonTreeShakableModules = [ + 'bootstrap', + 'bootstrap/dist/css/bootstrap.css', + 'core-js', + // 'es6-promise', + // 'es6-shim', + 'event-source-polyfill', + // 'jquery', +]; +const allModules = treeShakableModules.concat(nonTreeShakableModules); + +module.exports = (env) => { + console.log(`env = ${JSON.stringify(env)}`) + const extractCSS = new ExtractTextPlugin('vendor.css'); + const isDevBuild = !(env && env.prod); + const sharedConfig = { + stats: { modules: false }, + resolve: { extensions: [ '.js' ] }, + module: { + rules: [ + { test: /\.(png|woff|woff2|eot|ttf|svg)(\?|$)/, use: 'url-loader?limit=100000' } + ] + }, + output: { + publicPath: 'dist/', + filename: '[name].js', + library: '[name]_[hash]' + }, + 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 + ] + }; + + const clientBundleConfig = merge(sharedConfig, { + entry: { + // To keep development builds fast, include all vendor dependencies in the vendor bundle. + // But for production builds, leave the tree-shakable ones out so the AOT compiler can produce a smaller bundle. + vendor: isDevBuild ? allModules : nonTreeShakableModules + }, + output: { path: path.join(__dirname, 'wwwroot', 'dist') }, + module: { + rules: [ + { test: /\.css(\?|$)/, use: extractCSS.extract({ use: isDevBuild ? 'css-loader' : 'css-loader?minimize' }) } + ] + }, + plugins: [ + extractCSS, + new webpack.DllPlugin({ + path: path.join(__dirname, 'wwwroot', 'dist', '[name]-manifest.json'), + name: '[name]_[hash]' + }) + ].concat(isDevBuild ? [] : [ + new webpack.optimize.UglifyJsPlugin() + ]) + }); + + const serverBundleConfig = merge(sharedConfig, { + target: 'node', + resolve: { mainFields: ['main'] }, + entry: { vendor: allModules.concat(['aspnet-prerendering']) }, + output: { + path: path.join(__dirname, 'ClientApp', 'dist'), + libraryTarget: 'commonjs2', + }, + module: { + rules: [ { test: /\.css(\?|$)/, use: ['to-string-loader', isDevBuild ? 'css-loader' : 'css-loader?minimize' ] } ] + }, + plugins: [ + new webpack.DllPlugin({ + path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'), + name: '[name]_[hash]' + }) + ] + }); + + return [clientBundleConfig, serverBundleConfig]; +} From c490c6916fabf27e65409a92f863198ef630718a Mon Sep 17 00:00:00 2001 From: Isaac Levin Date: Thu, 24 Aug 2017 10:16:18 -0400 Subject: [PATCH 047/142] update to readme to clean up some things (#384) --- README.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 040dc0e0..c900ba32 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,22 @@ -# ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)!

- ASP.NET Core 2.0 Angular 2+ Starter + ASP.NET Core 2.0 Angular 4+ Starter

-### Harness the power of Angular 2+, ASP.NET Core 2.0, now with SEO ! +### Harness the power of Angular 4+, ASP.NET Core 2.0, now with SEO ! Angular SEO in action:

- ASP.NET Core Angular2 SEO + ASP.NET Core Angular4 SEO

### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net -This repository is maintained by [Angular Universal](https://github.com/angular/universal) and is meant to be an advanced starter +This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter for both ASP.NET Core 2.0 using Angular 4.0+, not only for the client-side, but to be rendered on the server for instant -application paints (Note: If you don't need Universal (SSR) [read here](#faq) on how to disable it). +application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). @@ -29,7 +29,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual * [Deployment](#deployment) * [Upcoming Features](#upcoming-features) * [Application Structure](#application-structure) -* [Universal Gotchas](#universal-gotchas) +* [Gotchas](#gotchas) * [FAQ](#faq---also-check-out-the-faq-issues-label) * [Special Thanks](#special-thanks) * [License](#license) @@ -46,10 +46,10 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - RestAPI (WebAPI) integration - SQL Database CRUD demo - Swagger WebAPI documentation when running in development mode - - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata) + - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) - **Angular 4.0.0** : - - Featuring Server-side rendering (Platform-Server (basically Angular Universal, but moved into Angular Core) + - Featuring Server-side rendering (Platform-Server) - Faster initial paints, SEO (Search-engine optimization w Title/Meta/Link tags), social media link-previews, etc - i18n internationalization support (via/ ngx-translate) - Baked in best-practices (follows Angular style guide) @@ -95,7 +95,9 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual # Getting Started? -**Make sure you have at least Node 6.x or higher (w/ npm 3+) installed!** +- **Make sure you have at least Node 6.x or higher (w/ npm 3+) installed!** +- **This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md)** + ### Visual Studio 2017 @@ -261,7 +263,7 @@ The short-version is that we invoke that Node process, passing in our Request ob A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/ng-aspnetcore-engine) ```csharp -// Prerender / Serialize application (with Universal) +// Prerender / Serialize application var prerenderResult = await Prerenderer.RenderToString( /* all of our parameters / options / boot-server file / customData object goes here */ ); @@ -283,7 +285,7 @@ Take a look at the `_Layout.cshtml` file for example, notice how we let .NET han - @ViewData["Title"] - AspNET.Core Angular 4.0.0 (+) Universal starter + @ViewData["Title"] - AspNET.Core Angular 4.0.0 (+) starter @@ -320,9 +322,10 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct ---- -# Universal "Gotchas" +# "Gotchas" +- This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md) -> When building Universal components in Angular 2 there are a few things to keep in mind. +> When building components in Angular 4 there are a few things to keep in mind. - **`window`**, **`document`**, **`navigator`**, and other browser types - _do not exist on the server_ - so using them, or any library that uses them (jQuery for example) will not work. You do have some options, if you truly need some of this functionality: - If you need to use them, consider limiting them to only your client and wrapping them situationally. You can use the Object injected using the PLATFORM_ID token to check whether the current platform is browser or server. @@ -364,7 +367,7 @@ constructor(element: ElementRef, renderer: Renderer) { # FAQ - Also check out the [FAQ Issues label](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq) -### How can I disable Universal / SSR (Server-side rendering)? +### How can I disable SSR (Server-side rendering)? Simply comment out the logic within HomeController, and replace `@Html.Raw(ViewData["SpaHtml"])` with just your applications root AppComponent tag ("app" in our case): ``. @@ -373,16 +376,16 @@ AppComponent tag ("app" in our case): ``. ### How do I have code run only in the Browser? -Check the [Universal Gotchas](#universal-gotchas) on how to use `isPlatformBrowser()`. +Check the [Gotchas](#gotchas) on how to use `isPlatformBrowser()`. ### How do I Material2 with this repo? -You'll either want to remove SSR for now, or wait as support should be coming to handle Universal/platform-server rendering. +You'll either want to remove SSR for now, or wait as support should be coming to handle platform-server rendering. -### How can I use jQuery and/or some jQuery plugins with Angular Universal? +### How can I use jQuery and/or some jQuery plugins with this repo? > Note: If at all possible, try to avoid using jQuery or libraries dependent on it, as there are -better, more abstract ways of dealing with the DOM in Angular (2+) such as using the Renderer, etc. +better, more abstract ways of dealing with the DOM in Angular (4+) such as using the Renderer, etc. Yes, of course but there are a few things you need to setup before doing this. First, make sure jQuery is included in webpack vendor file, and that you have a webpack Plugin setup for it. `new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })` From 8c73209f2f82778b7c8c09ca9f41d9ec8280cfda Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 24 Aug 2017 10:23:03 -0400 Subject: [PATCH 048/142] docs(readme): update to reflect new naming --- README.md | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c900ba32..7b53a279 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - **Webpack build system (Webpack 2)** - HMR : Hot Module Reloading/Replacement - - Production builds + - Production builds w/ AoT Compilation - **Testing frameworks** - Unit testing with Karma/Jasmine @@ -106,7 +106,7 @@ VS2017 will automatically install all the neccessary npm & .NET dependencies whe Simply push F5 to start debugging ! -**Note**: If you get any errors after this such as `module not found: main.server` (or similar), open up command line and run `npm run build:dev` to make sure all the assets have been properly built by Webpack. +**Note**: If you get any errors after this such as `module not found: boot.server` (or similar), open up command line and run `npm run build:dev` to make sure all the assets have been properly built by Webpack. ### Visual Studio Code @@ -177,14 +177,14 @@ Here we have the *usual suspects* found at the root level. - `protractor` - config files (e2e testing) - `tslint` - TypeScript code linting rules -### /Client/ - Everything Angular +### /ClientApp/ - Everything Angular > Let's take a look at how this is structured so we can make some sense of it all! -With Angular Universal, we need to split our applicatoin logic **per platform** so [if we look inside this folder](./Client), +With Angular Universal, we need to split our applicatoin logic **per platform** so [if we look inside this folder](./ClientApp), you'll see the 2 root files, that branch the entire logic for browser & server respectively. -- [**Main.Browser.ts**](./Client/main.browser.ts) - +- [**Boot.Browser.ts**](./ClientApp/main.browser.ts) - This file starts up the entire Angular application for the Client/browser platform. Here we setup a few things, client Angular bootstrapping. @@ -192,24 +192,24 @@ Here we setup a few things, client Angular bootstrapping. You'll barely need to touch this file, but something to note, this is the file where you would import libraries that you **only** want being used in the Browser. (Just know that you'd have to provide a mock implementation for the Server when doing that). -- [**Main-Server.ts**](./Client/main.server.ts) - +- [**Boot.Server.ts**](./ClientApp/boot.server.ts) - This file is where Angular _platform-server_ *serializes* the Angular application itself on the .NET server within a very quick Node process, and renders it a string. This is what causes that initial fast paint of the entire application to the Browser, and helps us get all our _SEO_ goodness :sparkles: --- -Notice the folder structure here in `./Client/` : +Notice the folder structure here in `./ClientApp/` : ```diff -+ /Client/ ++ /ClientApp/ + /app/ App NgModule - our Root NgModule (you'll insert Components/etc here most often) AppComponent / App Routes / global css styles * Notice that we have 2 dividing NgModules: - browser-app.module & server-app.module + app.module.browser & app.module.server You'll almost always be using the common app.module, but these 2 are used to split up platform logic for situations where you need to use Dependency Injection / etc, between platforms. @@ -226,21 +226,21 @@ Note: You could use whatever folder conventions you'd like, I prefer to split up ``` When adding new features/components/etc to your application you'll be commonly adding things to the Root **NgModule** (located -in `/Client/app/app.module.ts`), but why are there **two** other NgModules in this folder? +in `/ClientApp/app/app.module.ts`), but why are there **two** other NgModules in this folder? This is because we want to split our logic **per Platform**, but notice they both share the Common NgModule named `app.module.ts`. When adding most things to your application, this is the only place you'll have to add in your new Component / Directive / Pipe / etc. You'll only occassional need to manually -add in the Platform specific things to either `browser-app.module || server-app.module`. +add in the Platform specific things to either `app.module.browser || app.module.server`. To illustrate this point with an example, you can see how we're using Dependency Injection to inject a `StorageService` that is different for the Browser & Server. ```typescript -// For the Browser (browser-app.module) +// For the Browser (app.module.browser) { provide: StorageService, useClass: BrowserStorage } -// For the Server (server-app.module) +// For the Server (app.module.server) { provide: StorageService, useClass: ServerStorage } ``` @@ -258,14 +258,14 @@ Angular application gets serialized into a String, sent to the Browser, along wi --- -The short-version is that we invoke that Node process, passing in our Request object & invoke the `boot-server` file, and we get back a nice object that we pass into .NETs `ViewData` object, and sprinkle through out our `Views/Shared/_Layout.cshtml` and `/Views/Home/index.cshtml` files! +The short-version is that we invoke that Node process, passing in our Request object & invoke the `boot.server` file, and we get back a nice object that we pass into .NETs `ViewData` object, and sprinkle through out our `Views/Shared/_Layout.cshtml` and `/Views/Home/index.cshtml` files! A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/ng-aspnetcore-engine) ```csharp // Prerender / Serialize application var prerenderResult = await Prerenderer.RenderToString( - /* all of our parameters / options / boot-server file / customData object goes here */ + /* all of our parameters / options / boot.server file / customData object goes here */ ); ViewData["SpaHtml"] = prerenderResult.Html; @@ -323,6 +323,7 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct ---- # "Gotchas" + - This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md) > When building components in Angular 4 there are a few things to keep in mind. @@ -372,7 +373,7 @@ constructor(element: ElementRef, renderer: Renderer) { Simply comment out the logic within HomeController, and replace `@Html.Raw(ViewData["SpaHtml"])` with just your applications root AppComponent tag ("app" in our case): ``. -> You could also remove any `isPlatformBrowser/etc` logic, and delete the boot-server, browser-app.module & server-app.module files, just make sure your `boot-client` file points to `app.module`. +> You could also remove any `isPlatformBrowser/etc` logic, and delete the boot.server, app.module.browser & app.module.server files, just make sure your `boot.browser` file points to `app.module`. ### How do I have code run only in the Browser? @@ -433,6 +434,8 @@ Nothing's ever perfect, but please let me know by creating an issue (make sure t Copyright (c) 2016-2017 [Mark Pieszak](https://github.com/MarkPieszak) +### Follow me online: + Twitter: [@MarkPieszak](http://twitter.com/MarkPieszak) | Medium: [@MarkPieszak](https://medium.com/@MarkPieszak) ---- From 76108b67a9f664bf09927a1d8b137ce2d86d1c4c Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 24 Aug 2017 10:24:08 -0400 Subject: [PATCH 049/142] docs(readme): naming fix --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b53a279..b42ca3b6 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ Here we have the *usual suspects* found at the root level. With Angular Universal, we need to split our applicatoin logic **per platform** so [if we look inside this folder](./ClientApp), you'll see the 2 root files, that branch the entire logic for browser & server respectively. -- [**Boot.Browser.ts**](./ClientApp/main.browser.ts) - +- [**Boot.Browser.ts**](./ClientApp/boot.browser.ts) - This file starts up the entire Angular application for the Client/browser platform. Here we setup a few things, client Angular bootstrapping. @@ -391,7 +391,7 @@ better, more abstract ways of dealing with the DOM in Angular (4+) such as using Yes, of course but there are a few things you need to setup before doing this. First, make sure jQuery is included in webpack vendor file, and that you have a webpack Plugin setup for it. `new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })` -Now, make sure any "plugins" etc that you have, are only included in your `main.browser.ts` file. (ie: `import 'slick-carousel';`) +Now, make sure any "plugins" etc that you have, are only included in your `boot.browser.ts` file. (ie: `import 'slick-carousel';`) In a Component you want to use jQuery, make sure to import it near the top like so: ```typescript From 265e9a791fbe1b9376d80a49d9a1a2894d620313 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 24 Aug 2017 16:09:33 -0400 Subject: [PATCH 050/142] fix(csproj): update csproj file to open in vs2017 --- Asp2017.csproj | 54 ++++++++++++++++---------------------------------- 1 file changed, 17 insertions(+), 37 deletions(-) diff --git a/Asp2017.csproj b/Asp2017.csproj index e373e9ae..db3c39eb 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -1,55 +1,29 @@ - - + - netcoreapp2.0 - TargetFrameworkOverride + netcoreapp2.0 true Latest false - - - - - - - - - - + + - - - + + + + + - - - - - - - - - - - - - - - - - - + @@ -64,5 +38,11 @@ - + + + + + + + From 94605d604a4fa0b054da4819017764ce5db1e82c Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 24 Aug 2017 16:27:47 -0400 Subject: [PATCH 051/142] feat(angular-cli): add basic angular-cli support for generators ## Minimal) Angular-CLI integration - This is to be used mainly for Generating Components/Services/etc. - Usage examples: - `ng g c components/example-component` - `ng g s shared/some-service` --- .angular-cli.json | 17 +++++++++++++++++ README.md | 9 +++++++-- package.json | 5 +++-- 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 .angular-cli.json diff --git a/.angular-cli.json b/.angular-cli.json new file mode 100644 index 00000000..3382816f --- /dev/null +++ b/.angular-cli.json @@ -0,0 +1,17 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "project": { + "name": "AspnetCore-Angular-Universal" + }, + "apps": [ + { + "root": "ClientApp" + } + ], + "defaults": { + "styleExt": "scss", + "component": { + "spec": false + } + } +} diff --git a/README.md b/README.md index b42ca3b6..dac4e4d3 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,12 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) - **Angular 4.0.0** : - - Featuring Server-side rendering (Platform-Server) + - (Minimal) Angular-CLI integration + - This is to be used mainly for Generating Components/Services/etc. + - Usage examples: + - `ng g c components/example-component` + - `ng g s shared/some-service` + - Featuring Server-side rendering (Platform-Server, aka: "Universal") - Faster initial paints, SEO (Search-engine optimization w Title/Meta/Link tags), social media link-previews, etc - i18n internationalization support (via/ ngx-translate) - Baked in best-practices (follows Angular style guide) @@ -62,7 +67,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Production builds w/ AoT Compilation - **Testing frameworks** - - Unit testing with Karma/Jasmine + - Unit testing with Jest (Going back to Karma soon) - **Productivity** - Typescript 2 diff --git a/package.json b/package.json index f3b00472..49da51e0 100644 --- a/package.json +++ b/package.json @@ -77,15 +77,16 @@ "zone.js": "^0.8.9" }, "devDependencies": { + "@angular/cli": "^1.3.2", "@ngtools/webpack": "^1.3.0", "@types/chai": "^3.4.34", "@types/jasmine": "^2.5.37", "@types/jest": "^19.2.3", "chai": "^3.5.0", "codelyzer": "^3.0.0", - "tslint": "^5.0.0", "jasmine-core": "^2.5.2", "jest": "^20.0.0", - "jest-preset-angular": "^2.0.1" + "jest-preset-angular": "^2.0.1", + "tslint": "^5.0.0" } } From 469a7a705b8aa8d2f8487c458fb4d625288ffade Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 25 Aug 2017 10:34:44 -0400 Subject: [PATCH 052/142] fix(signal-r): updated signalr chatroom --- .../components/navmenu/navmenu.component.html | 10 +-- .../app/containers/chat/chat.component.html | 80 ++++++++++--------- .../app/containers/chat/chat.component.scss | 4 +- .../app/containers/chat/chat.component.ts | 26 +++--- Views/Shared/_Layout.cshtml | 6 +- 5 files changed, 68 insertions(+), 58 deletions(-) diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index b92d8671..a4cffbdc 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -39,11 +39,11 @@ Lazy-loaded demo - +
  • + + Chat + +
  • diff --git a/ClientApp/app/containers/chat/chat.component.html b/ClientApp/app/containers/chat/chat.component.html index e29d109b..d44cc419 100644 --- a/ClientApp/app/containers/chat/chat.component.html +++ b/ClientApp/app/containers/chat/chat.component.html @@ -1,45 +1,51 @@ -

    SignalR chat example

    +

    WebSockets (SignalR) Chatroom Example

    -
    -
    -
    -
    - Chat -
    -
    -
      +
      + Data is stored in Azure here - so even in localhost you might be chatting with other people! +
      -
    • - +
      +
      +
      +
      + Angular Chat +
      +
      +
        -
        -
        - {{message.user}} - x mins ago -
        -

        - {{message.content}} -

        -
        - +
      • + + User Avatar + -
      +
      +
      + {{message.user}} + x mins ago +
      +

      + {{ message.content }} +

      - -
      +
    • + +
    +
    +
    +
    -
    {{chatMessages | json}}
    \ No newline at end of file +
    {{ chatMessages | json }}
    diff --git a/ClientApp/app/containers/chat/chat.component.scss b/ClientApp/app/containers/chat/chat.component.scss index d6fefaad..8e085732 100644 --- a/ClientApp/app/containers/chat/chat.component.scss +++ b/ClientApp/app/containers/chat/chat.component.scss @@ -33,7 +33,7 @@ .panel-body { overflow-y: scroll; - height: 600px; + height: 500px; } ::-webkit-scrollbar-track { @@ -49,4 +49,4 @@ ::-webkit-scrollbar-thumb { -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); background-color: #555; -} \ No newline at end of file +} diff --git a/ClientApp/app/containers/chat/chat.component.ts b/ClientApp/app/containers/chat/chat.component.ts index 8bbd9ed9..78cbdb47 100644 --- a/ClientApp/app/containers/chat/chat.component.ts +++ b/ClientApp/app/containers/chat/chat.component.ts @@ -5,7 +5,7 @@ import { SignalR, BroadcastEventListener, SignalRConnection } from 'ng2-signalr' import { Subscription } from 'rxjs/Subscription'; export class ChatMessage { - constructor(public user: string, public content: string) { } + constructor(public content: string, public user: string) { } } @Component({ @@ -21,23 +21,25 @@ export class ChatComponent implements OnInit { private _subscription: Subscription; constructor(route: ActivatedRoute) { - this._connection = route.snapshot.data['connection']; + this._connection = route.snapshot.data['connection']; } ngOnInit() { - const onMessageSent$ = new BroadcastEventListener('OnMessageSent'); - this._connection.listen(onMessageSent$); - this._subscription = onMessageSent$.subscribe((chatMessage: ChatMessage) => { - this.chatMessages.push(chatMessage); - console.log('chat messages', this.chatMessages); - }); + const onMessageSent$ = new BroadcastEventListener('OnMessageSent'); + this._connection.listen(onMessageSent$); + this._subscription = onMessageSent$.subscribe((chatMessage: ChatMessage) => { + this.chatMessages.push(chatMessage); + console.log('chat messages', this.chatMessages); + }); } // send chat message to server - sendMessage(user, message) { - console.log('send message', user, message); - this._connection.invoke('Chat', new ChatMessage(user, message)) - .catch((err: any) => console.log('Failed to invoke', err)); + sendMessage(user, messageInput) { + console.log('send message', user, messageInput.value); + this._connection.invoke('Chat', new ChatMessage(messageInput.value, user)) + .catch((err: any) => console.log('Failed to invoke', err)); + + messageInput.value = ''; } } diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 509c52db..7fb24bdb 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -18,16 +18,18 @@ @RenderBody() - + - + @Html.Raw(ViewData["TransferData"]) @RenderSection("scripts", required: false) + From 593622f3cbc7cf47047e9109b172334984d6a2d5 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 25 Aug 2017 10:39:22 -0400 Subject: [PATCH 053/142] fix(signal-r): update resolve --- ClientApp/app/shared/route.resolver.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/ClientApp/app/shared/route.resolver.ts b/ClientApp/app/shared/route.resolver.ts index 9b7e02c0..43b0078c 100644 --- a/ClientApp/app/shared/route.resolver.ts +++ b/ClientApp/app/shared/route.resolver.ts @@ -1,15 +1,21 @@ import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; +import { Resolve, Router } from '@angular/router'; import { SignalR, ISignalRConnection } from 'ng2-signalr'; @Injectable() export class ConnectionResolver implements Resolve { - constructor(private _signalR: SignalR) { } + constructor( + private _signalR: SignalR, + private _router: Router + ) { } - resolve() { - console.log('ConnectionResolver. Resolving...'); - return this._signalR.connect(); + resolve(): Promise { + return this._signalR.connect().then((item) => { + return item; + }).catch(() => { + return this._router.navigate(['/']); + }); } -} +} From 34c67fbf0631a625f8d5a2388f2525cedc1f538e Mon Sep 17 00:00:00 2001 From: David Gnanasekaran Date: Sat, 26 Aug 2017 17:27:03 +0530 Subject: [PATCH 054/142] Modified .csproject to emit the wwwroot/dist folder, if the folder is not created yet (#390) --- Asp2017.csproj | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Asp2017.csproj b/Asp2017.csproj index db3c39eb..1bebcdf2 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -23,6 +23,20 @@ + + + + + + + + + + + + + From 85558da2b2a0573629b705548b3dad8e0938df94 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Wed, 30 Aug 2017 12:04:36 -0400 Subject: [PATCH 055/142] chore(webpack): remove old deprecated webpack folder --- webpack/helpers.js | 9 -------- webpack/webpack.aot.js | 48 --------------------------------------- webpack/webpack.client.js | 20 ---------------- webpack/webpack.common.js | 25 -------------------- webpack/webpack.prod.js | 4 ---- webpack/webpack.server.js | 22 ------------------ 6 files changed, 128 deletions(-) delete mode 100644 webpack/helpers.js delete mode 100644 webpack/webpack.aot.js delete mode 100644 webpack/webpack.client.js delete mode 100644 webpack/webpack.common.js delete mode 100644 webpack/webpack.prod.js delete mode 100644 webpack/webpack.server.js diff --git a/webpack/helpers.js b/webpack/helpers.js deleted file mode 100644 index 2b49ccc0..00000000 --- a/webpack/helpers.js +++ /dev/null @@ -1,9 +0,0 @@ -const { resolve } = require('path'); - -function root(path) { - return resolve(__dirname, '..', path); -} - -module.exports = { - root: root -}; diff --git a/webpack/webpack.aot.js b/webpack/webpack.aot.js deleted file mode 100644 index 5af7ca50..00000000 --- a/webpack/webpack.aot.js +++ /dev/null @@ -1,48 +0,0 @@ -const { root } = require('./helpers'); -const { AotPlugin } = require('@ngtools/webpack'); - -const tsconfigs = { - client: root('./Client/tsconfig.browser.json'), - server: root('./Client/tsconfig.server.json') -}; - -const aotTsconfigs = { - client: root('./Client/tsconfig.browser.json'), - server: root('./Client/tsconfig.server.aot.json') -}; - -/** - * Generates a AotPlugin for @ngtools/webpack - * - * @param {string} platform Should either be client or server - * @param {boolean} aot Enables/Disables AoT Compilation - * @returns - */ -function getAotPlugin(platform, aot) { - - var aotPlugin = new AotPlugin({ - tsConfigPath: aot ? aotTsconfigs[platform] : tsconfigs[platform], - skipCodeGeneration: !aot - }); - - // TEMPORARY fix for Windows 10 - will be gone when fixed - aotPlugin._compilerHost._resolve = function (path_to_resolve) { - path_1 = require("path"); - path_to_resolve = aotPlugin._compilerHost._normalizePath(path_to_resolve); - if (path_to_resolve[0] == '.') { - return aotPlugin._compilerHost._normalizePath(path_1.join(aotPlugin._compilerHost.getCurrentDirectory(), path_to_resolve)); - } - else if (path_to_resolve[0] == '/' || path_to_resolve.match(/^\w:\//)) { - return path_to_resolve; - } - else { - return aotPlugin._compilerHost._normalizePath(path_1.join(aotPlugin._compilerHost._basePath, path_to_resolve)); - } - }; - - return aotPlugin; -} - -module.exports = { - getAotPlugin: getAotPlugin -}; diff --git a/webpack/webpack.client.js b/webpack/webpack.client.js deleted file mode 100644 index 1c50c612..00000000 --- a/webpack/webpack.client.js +++ /dev/null @@ -1,20 +0,0 @@ -const { AotPlugin } = require('@ngtools/webpack'); - -const { root } = require('./helpers'); -const clientBundleOutputDir = root('./wwwroot/dist'); - -/** - * This is a client config which should be merged on top of common config - */ -module.exports = { - entry: { - 'main-browser': root('./Client/main.browser.ts') - }, - output: { - path: root('./wwwroot/dist'), - }, - target: 'web', - plugins: [ - - ] -}; diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js deleted file mode 100644 index 254ecf86..00000000 --- a/webpack/webpack.common.js +++ /dev/null @@ -1,25 +0,0 @@ -const { root } = require('./helpers'); - -/** - * This is a common webpack config which is the base for all builds - */ -module.exports = { - devtool: 'source-map', - resolve: { - extensions: ['.ts', '.js'] - }, - output: { - filename: '[name].js', - publicPath: '/dist/' // Webpack dev middleware, if enabled, handles requests for this URL prefix - }, - module: { - rules: [ - { test: /\.ts$/, loader: '@ngtools/webpack' }, - { test: /\.css$/, loader: ['to-string-loader', 'css-loader'] }, - { test: /\.html$/, loader: 'html-loader' }, - { test: /\.scss$/, loaders: ['to-string-loader', 'css-loader', 'sass-loader'] }, - { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url-loader?limit=10000' } - ] - }, - plugins: [] -}; diff --git a/webpack/webpack.prod.js b/webpack/webpack.prod.js deleted file mode 100644 index 658f1cd7..00000000 --- a/webpack/webpack.prod.js +++ /dev/null @@ -1,4 +0,0 @@ -/** - * This is a prod config to be merged with the Client config - */ -module.exports = {}; diff --git a/webpack/webpack.server.js b/webpack/webpack.server.js deleted file mode 100644 index e968400a..00000000 --- a/webpack/webpack.server.js +++ /dev/null @@ -1,22 +0,0 @@ -const { root } = require('./helpers'); -const { AotPlugin } = require('@ngtools/webpack'); - -/** - * This is a server config which should be merged on top of common config - */ -module.exports = { - devtool: 'inline-source-map', - resolve: { - extensions: ['.ts', '.js', '.json'], - // An array of directory names to be resolved to the current directory - modules: [root('Client'), root('node_modules')], - }, - entry: { - 'main-server': root('./Client/main.server.ts') - }, - output: { - libraryTarget: 'commonjs', - path: root('./Client/dist') - }, - target: 'node' -}; From 234f246320f691f1a1956cc0b3cef17c5c7ce2ca Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 1 Sep 2017 18:32:23 -0400 Subject: [PATCH 056/142] fix(build): fix prod flag --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49da51e0..66b937c3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test:coverage": "npm run test -- --coverage", "build:dev": "npm run build:vendor && npm run build:webpack", "build:webpack": "webpack --progress --color", - "build:prod": "npm run build:vendor -- --env.prod && npm run build:dev -- --env.prod", + "build:prod": "npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", "build:vendor": "webpack --config webpack.config.vendor.js --progress --color" }, "jest": { From af574fc67955ea8a1eb487e6e8bc9035c2a303f9 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sat, 2 Sep 2017 12:41:09 -0400 Subject: [PATCH 057/142] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dac4e4d3..073623ee 100644 --- a/README.md +++ b/README.md @@ -445,7 +445,7 @@ Twitter: [@MarkPieszak](http://twitter.com/MarkPieszak) | Medium: [@MarkPieszak] ---- -# Looking for Angular Consulting / Training / support? +# Looking for Angular & ASP.NET Consulting / Training / support? [Contact me](mpieszak84@gmail.com), and let's talk about your projects needs! From 359623e4c623f1be2f696658f2693e723bda91d9 Mon Sep 17 00:00:00 2001 From: Bill Sheldon <30598595+BillSheldon-HunterIndustries@users.noreply.github.com> Date: Sun, 3 Sep 2017 15:53:19 -0700 Subject: [PATCH 058/142] Minor updates related to the menu when in a mobile size display. When a menu item is selected this update causes the menu to automatically collapse. (#397) --- .../components/navmenu/navmenu.component.html | 18 +++++++++--------- .../components/navmenu/navmenu.component.ts | 6 +++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index a4cffbdc..57b9c351 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -14,35 +14,35 @@
    diff --git a/ClientApp/app/components/navmenu/navmenu.component.ts b/ClientApp/app/components/navmenu/navmenu.component.ts index 3287431d..ac36f036 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.ts +++ b/ClientApp/app/components/navmenu/navmenu.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component } from '@angular/core'; @Component({ selector: 'nav-menu', @@ -16,4 +16,8 @@ export class NavMenuComponent { this.collapse = "collapse"; } } + + collapseMenu() { + this.collapse = "collapse" + } } From 9e2583d18d68effa39a6816a0682788778556dab Mon Sep 17 00:00:00 2001 From: David Gnanasekaran Date: Mon, 4 Sep 2017 20:19:13 +0530 Subject: [PATCH 059/142] (Unit testing) using Karma test runner (#399) * Modified .csproject to emit the wwwroot/dist folder, if the folder is not created yet * Remove Jest and added Karma test runner for executing unit test cases --- Asp2017.csproj | 2 +- ClientApp/test/boot-tests.js | 30 +++++++ ClientApp/test/jestGlobalMocks.ts | 15 ---- ClientApp/test/karma.conf.js | 46 +++++++++++ ClientApp/test/setupJest.ts | 2 - ClientApp/test/webpack.config.test.js | 112 ++++++++++++++++++++++++++ package.json | 30 +++---- 7 files changed, 201 insertions(+), 36 deletions(-) create mode 100644 ClientApp/test/boot-tests.js delete mode 100644 ClientApp/test/jestGlobalMocks.ts create mode 100644 ClientApp/test/karma.conf.js delete mode 100644 ClientApp/test/setupJest.ts create mode 100644 ClientApp/test/webpack.config.test.js diff --git a/Asp2017.csproj b/Asp2017.csproj index 1bebcdf2..d944c48d 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -54,7 +54,7 @@
    - + diff --git a/ClientApp/test/boot-tests.js b/ClientApp/test/boot-tests.js new file mode 100644 index 00000000..eba839d2 --- /dev/null +++ b/ClientApp/test/boot-tests.js @@ -0,0 +1,30 @@ +Error.stackTraceLimit = Infinity; +// Load required polyfills and testing libraries +require('reflect-metadata'); +require('zone.js'); +require('zone.js/dist/long-stack-trace-zone'); +require('zone.js/dist/proxy.js'); +require('zone.js/dist/sync-test'); +require('zone.js/dist/jasmine-patch'); +require('zone.js/dist/async-test'); +require('zone.js/dist/fake-async-test'); +const testing = require('@angular/core/testing'); +const testingBrowser = require('@angular/platform-browser-dynamic/testing'); + +// Prevent Karma from running prematurely +__karma__.loaded = function () {}; + +// First, initialize the Angular testing environment +testing.getTestBed().initTestEnvironment( + testingBrowser.BrowserDynamicTestingModule, + testingBrowser.platformBrowserDynamicTesting() +); + +// Then we find all the tests +const context = require.context('../', true, /\.spec\.ts$/); + +// And load the modules +context.keys().map(context); + +// Finally, start Karma to run the tests +__karma__.start(); diff --git a/ClientApp/test/jestGlobalMocks.ts b/ClientApp/test/jestGlobalMocks.ts deleted file mode 100644 index e5e90ebc..00000000 --- a/ClientApp/test/jestGlobalMocks.ts +++ /dev/null @@ -1,15 +0,0 @@ -const mock = () => { - let storage = {}; - return { - getItem: key => key in storage ? storage[key] : null, - setItem: (key, value) => storage[key] = value || '', - removeItem: key => delete storage[key], - clear: () => storage = {} - }; -}; - -Object.defineProperty(window, 'localStorage', {value: mock()}); -Object.defineProperty(window, 'sessionStorage', {value: mock()}); -Object.defineProperty(window, 'getComputedStyle', { - value: () => ['-webkit-appearance'] -}); diff --git a/ClientApp/test/karma.conf.js b/ClientApp/test/karma.conf.js new file mode 100644 index 00000000..0ae99871 --- /dev/null +++ b/ClientApp/test/karma.conf.js @@ -0,0 +1,46 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/0.13/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '.', + frameworks: ['jasmine'], + exclude: [], + files: [ + './boot-tests.js' + ], + preprocessors: { + './boot-tests.js': ['coverage', 'webpack', 'sourcemap'] + }, + client: { + captureConsole: false + }, + coverageReporter: { + type: 'in-memory' + }, + remapCoverageReporter: { + 'text-summary': null, + json: './coverage/coverage.json', + html: './coverage/html' + }, + reporters: ['mocha', 'coverage', 'remap-coverage'], + port: 9876, + colors: true, + logLevel: config.LOG_WARN, + autoWatch: false, + browsers: ['Chrome'], + mime: { + 'application/javascript': ['ts', 'tsx'] + }, + singleRun: true, + webpack: require('./webpack.config.test.js')({ + env: 'test' + }), + webpackMiddleware: { + noInfo: true, + stats: { + chunks: false + } + } + }); +}; diff --git a/ClientApp/test/setupJest.ts b/ClientApp/test/setupJest.ts deleted file mode 100644 index 1d3bd024..00000000 --- a/ClientApp/test/setupJest.ts +++ /dev/null @@ -1,2 +0,0 @@ -import 'jest-preset-angular'; -import './jestGlobalMocks'; diff --git a/ClientApp/test/webpack.config.test.js b/ClientApp/test/webpack.config.test.js new file mode 100644 index 00000000..6818b46c --- /dev/null +++ b/ClientApp/test/webpack.config.test.js @@ -0,0 +1,112 @@ +const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin'); +const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); + +var path = require('path'); +var rootPath = path.join.bind(path, path.resolve(__dirname, '../../')); + +module.exports = function (options) { + return { + devtool: 'inline-source-map', + resolve: { + extensions: ['.ts', '.js'], + modules: [rootPath('ClientApp'), 'node_modules'] + }, + module: { + rules: [{ + enforce: 'pre', + test: /\.js$/, + loader: 'source-map-loader', + exclude: [ + rootPath('node_modules/rxjs'), + rootPath('node_modules/@angular') + ] + }, + { + test: /\.ts$/, + use: [{ + loader: 'awesome-typescript-loader', + query: { + sourceMap: false, + inlineSourceMap: true, + compilerOptions: { + removeComments: true + } + }, + }, + 'angular2-template-loader' + ], + exclude: [/\.e2e\.ts$/] + }, + { + test: /\.css$/, + loader: ['to-string-loader', 'css-loader'] + }, + { + test: /\.scss$/, + loader: ['raw-loader', 'sass-loader'] + }, + { + test: /\.html$/, + loader: 'raw-loader' + }, + { + enforce: 'post', + test: /\.(js|ts)$/, + loader: 'istanbul-instrumenter-loader', + options: { + esModules: true + }, + include: rootPath('ClientApp'), + exclude: [ + /ClientApp\\test/, + /\.(e2e|spec)\.ts$/, + /node_modules/ + ] + } + + ] + }, + plugins: [ + new ContextReplacementPlugin( + /** + * The (\\|\/) piece accounts for path separators in *nix and Windows + */ + /angular(\\|\/)core(\\|\/)@angular/, + rootPath('ClientApp'), // location of your src + { + /** + * your Angular Async Route paths relative to this root directory + */ + } + ), + new LoaderOptionsPlugin({ + debug: false, + options: { + /** + * legacy options go here + */ + } + }), + + ], + performance: { + hints: false + }, + + /** + * Include polyfills or mocks for various node stuff + * Description: Node configuration + * + * See: https://webpack.github.io/docs/configuration.html#node + */ + node: { + global: true, + process: false, + crypto: 'empty', + module: false, + clearImmediate: false, + setImmediate: false + } + + }; +} diff --git a/package.json b/package.json index 66b937c3..6072e16c 100644 --- a/package.json +++ b/package.json @@ -2,8 +2,8 @@ "name": "angular4-aspnetcore-universal", "version": "1.0.0-rc3", "scripts": { - "test": "jest", - "test:watch": "npm run test -- --watch", + "test": "karma start ClientApp/test/karma.conf.js", + "test:watch": "npm run test -- --auto-watch --no-single-run", "test:ci": "npm run test -- --runInBand", "test:coverage": "npm run test -- --coverage", "build:dev": "npm run build:vendor && npm run build:webpack", @@ -11,19 +11,6 @@ "build:prod": "npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", "build:vendor": "webpack --config webpack.config.vendor.js --progress --color" }, - "jest": { - "preset": "jest-preset-angular", - "setupTestFrameworkScriptFile": "./ClientApp/test/setupJest.ts", - "globals": { - "__TS_CONFIG__": "ClientApp/tsconfig.spec.json", - "__TRANSFORM_HTML__": true - }, - "coveragePathIgnorePatterns": [ - "/node_modules/", - "/ClientApp/test/.*.ts" - ], - "coverageDirectory": "coverage" - }, "dependencies": { "@angular/animations": "^4.3.0", "@angular/common": "^4.3.0", @@ -81,12 +68,19 @@ "@ngtools/webpack": "^1.3.0", "@types/chai": "^3.4.34", "@types/jasmine": "^2.5.37", - "@types/jest": "^19.2.3", "chai": "^3.5.0", "codelyzer": "^3.0.0", + "istanbul-instrumenter-loader": "^3.0.0", "jasmine-core": "^2.5.2", - "jest": "^20.0.0", - "jest-preset-angular": "^2.0.1", + "karma": "^1.7.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^1.1.1", + "karma-jasmine": "^1.1.0", + "karma-mocha-reporter": "^2.2.4", + "karma-remap-coverage": "^0.1.4", + "karma-sourcemap-loader": "^0.3.7", + "karma-webpack": "^2.0.3", "tslint": "^5.0.0" } } From 2f11294427ef5da2d6929798c8628e25296aca75 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 5 Sep 2017 19:02:50 -0400 Subject: [PATCH 060/142] feat(preboot): add preboot functionality & fix aspnetcore-engine (#401) Closes #363 --- ClientApp/app/app.module.browser.ts | 3 +++ ClientApp/app/app.module.server.ts | 3 +++ ClientApp/polyfills/temporary-aspnetcore-engine.ts | 8 ++++++++ Server/Controllers/HomeController.cs | 1 + Views/Shared/_Layout.cshtml | 1 + package.json | 2 +- 6 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index 0af291d4..70918765 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -11,6 +11,8 @@ import { AppComponent } from './app.component'; import { REQUEST } from './shared/constants/request'; import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; +import { BrowserPrebootModule } from 'preboot/browser'; + export function createConfig(): SignalRConfiguration { const signalRConfig = new SignalRConfiguration(); @@ -37,6 +39,7 @@ export function getRequest() { BrowserModule.withServerTransition({ appId: 'my-app-id' // make sure this matches with your Server NgModule }), + BrowserPrebootModule.replayEvents(), BrowserAnimationsModule, BrowserTransferStateModule, diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts index 9ccbfb52..9e22b71e 100644 --- a/ClientApp/app/app.module.server.ts +++ b/ClientApp/app/app.module.server.ts @@ -8,6 +8,8 @@ import { AppComponent } from './app.component'; import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module'; import { TransferState } from '../modules/transfer-state/transfer-state'; +import { ServerPrebootModule } from 'preboot/server'; + @NgModule({ bootstrap: [AppComponent], imports: [ @@ -15,6 +17,7 @@ import { TransferState } from '../modules/transfer-state/transfer-state'; appId: 'my-app-id' // make sure this matches with your Browser NgModule }), ServerModule, + ServerPrebootModule.recordEvents({ appRoot: 'app' }), NoopAnimationsModule, ServerTransferStateModule, diff --git a/ClientApp/polyfills/temporary-aspnetcore-engine.ts b/ClientApp/polyfills/temporary-aspnetcore-engine.ts index a68986c2..5a2ce9df 100644 --- a/ClientApp/polyfills/temporary-aspnetcore-engine.ts +++ b/ClientApp/polyfills/temporary-aspnetcore-engine.ts @@ -122,6 +122,7 @@ export function ngAspnetCoreEngine( // Strip out Styles / Meta-tags / Title const STYLES = []; + const SCRIPTS = []; const META = []; const LINKS = []; let TITLE = ''; @@ -148,6 +149,12 @@ export function ngAspnetCoreEngine( TITLE = element.children[0].data; } + if (element.name === 'script') { + SCRIPTS.push( + `` + ); + } + // Broken after 4.0 (worked in rc) // if (element.name === 'style') { // let styleTag = '') + 8 - //); - let STYLES_STRING: string = htmlDoc.indexOf('') + 8) : null; - // STYLES_STRING = STYLES_STRING.replace(/\s/g, '').replace(', NgModuleFactory<{}>>(); function getFactory( moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler ): Promise> { + return new Promise>((resolve, reject) => { // If module has been compiled AoT if (moduleOrFactory instanceof NgModuleFactory) { - console.log('Already AoT?'); resolve(moduleOrFactory); return; } else { @@ -244,7 +237,6 @@ function getFactory( // If module factory is cached if (moduleFactory) { - console.log('\n\n\n WE FOUND ONE!! USE IT!!\n\n\n'); resolve(moduleFactory); return; } @@ -252,7 +244,6 @@ function getFactory( // Compile the module and cache it compiler.compileModuleAsync(moduleOrFactory) .then((factory) => { - console.log('\n\n\n\n MAP THIS THING!!!!\n\n\n '); factoryCacheMap.set(moduleOrFactory, factory); resolve(factory); }, (err => { From 8a02df7d09f3934464684472fa49c31cf8ca209f Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 21 Sep 2017 17:40:08 -0400 Subject: [PATCH 066/142] bug(ngx-bootstrap): pin to beta.3 for now fixes MouseEvent issue from latest beta release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b35ac7ab..03efe97c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "jquery": "^2.2.1", "json-loader": "^0.5.4", "ng2-signalr": "2.0.4", - "ngx-bootstrap": "^2.0.0-beta.2", + "ngx-bootstrap": "2.0.0-beta.3", "node-sass": "^4.5.2", "preboot": "^5.0.0", "raw-loader": "^0.5.1", From 3848e4357d24a04d39d3931d0a36d383c07cc005 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 22 Sep 2017 17:01:57 -0400 Subject: [PATCH 067/142] docs(readme): renderer2 fix --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f24dfc64..9e30c7a4 100644 --- a/README.md +++ b/README.md @@ -337,7 +337,7 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct - **`window`**, **`document`**, **`navigator`**, and other browser types - _do not exist on the server_ - so using them, or any library that uses them (jQuery for example) will not work. You do have some options, if you truly need some of this functionality: - If you need to use them, consider limiting them to only your client and wrapping them situationally. You can use the Object injected using the PLATFORM_ID token to check whether the current platform is browser or server. - ``` + ```typescript import { PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; @@ -358,9 +358,9 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct - Try to *limit or* **avoid** using **`setTimeout`**. It will slow down the server-side rendering process. Make sure to remove them [`ngOnDestroy`](https://angular.io/docs/ts/latest/api/core/index/OnDestroy-class.html) in Components. - Also for RxJs timeouts, make sure to _cancel_ their stream on success, for they can slow down rendering as well. - **Don't manipulate the nativeElement directly**. Use the _Renderer2_. We do this to ensure that in any environment we're able to change our view. -``` -constructor(element: ElementRef, renderer: Renderer) { - renderer.setElementStyle(element.nativeElement, 'font-size', 'x-large'); +```typescript +constructor(element: ElementRef, renderer: Renderer2) { + this.renderer.setStyle(element.nativeElement, 'font-size', 'x-large'); } ``` - The application runs XHR requests on the server & once again on the Client-side (when the application bootstraps) From e627f3ac9bcadf2a7021587d343bdfa1fea9fc3a Mon Sep 17 00:00:00 2001 From: David Gnanasekaran Date: Fri, 29 Sep 2017 23:38:36 +0530 Subject: [PATCH 068/142] fix (AoT - Lazy load) : lazy modules chunks creation on Server bundle (#410) Closes #422 Closes #413 * Modified .csproject to emit the wwwroot/dist folder, if the folder is not created yet * Remove Jest and added Karma test runner for executing unit test cases * Added PhantomJS for running test in CI environments. Removed deprecated tslint rules and amended existing rules. * adding tslint config to angular-cli for the ng lint command (#407) * fix (AoT - Lazy load) : commented code that prevented lazy modules chunks from getting created on PlatformServer * packages updated to fix 1) Zone already loaded error, 2) issue with not-found component template load issue * Update README.md * Update README.md * chore(engine): remove comments * Enable uglifyjs on server bundle to reduce bundle size * Mangling and compress options disabled for Server bundle to fix preboot script issue. Removed duplicated Bootstrap css from vendor bundle --- .../not-found/not-found.component.ts | 1 - ClientApp/boot.server.ts | 2 +- Views/Shared/_Layout.cshtml | 2 +- package.json | 46 ++++++++++--------- webpack.config.js | 10 +++- webpack.config.vendor.js | 8 ++-- 6 files changed, 39 insertions(+), 30 deletions(-) diff --git a/ClientApp/app/containers/not-found/not-found.component.ts b/ClientApp/app/containers/not-found/not-found.component.ts index 0ebd7e90..e39faa05 100644 --- a/ClientApp/app/containers/not-found/not-found.component.ts +++ b/ClientApp/app/containers/not-found/not-found.component.ts @@ -3,7 +3,6 @@ import { Component, OnInit } from '@angular/core'; @Component({ selector: 'not-found', templateUrl: './not-found.component.html' - // styleUrls: ['./not-found.component.css'] }) export class NotFoundComponent implements OnInit { constructor() { } diff --git a/ClientApp/boot.server.ts b/ClientApp/boot.server.ts index d93a4f56..86d65086 100644 --- a/ClientApp/boot.server.ts +++ b/ClientApp/boot.server.ts @@ -13,7 +13,7 @@ import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './poly enableProdMode(); -export default createServerRenderer((params: BootFuncParams) => { +export default createServerRenderer((params) => { // Platform-server provider configuration const setupOptions: IEngineOptions = { diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 3f6d8aff..49ecd9b0 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -10,7 +10,7 @@ @Html.Raw(ViewData["Links"]) - + @Html.Raw(ViewData["Styles"]) diff --git a/package.json b/package.json index 03efe97c..f7d886f7 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,9 @@ "test:coverage": "npm run test -- --coverage", "build:dev": "npm run build:vendor && npm run build:webpack", "build:webpack": "webpack --progress --color", - "build:prod": "npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", - "build:vendor": "webpack --config webpack.config.vendor.js --progress --color" + "build:prod": "npm run clean && npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", + "build:vendor": "webpack --config webpack.config.vendor.js --progress --color", + "clean": "rimraf wwwroot/dist clientapp/dist" }, "dependencies": { "@angular/animations": "^4.3.0", @@ -29,21 +30,21 @@ "@ngx-translate/core": "^6.0.1", "@ngx-translate/http-loader": "0.0.3", "@types/node": "^7.0.12", - "angular2-router-loader": "^0.3.4", - "angular2-template-loader": "0.6.0", - "aspnet-prerendering": "2.0.3", + "angular2-router-loader": "^0.3.5", + "angular2-template-loader": "^0.6.2", + "aspnet-prerendering": "^3.0.1", "aspnet-webpack": "^2.0.1", "awesome-typescript-loader": "^3.0.0", "bootstrap": "^3.3.7", "bootstrap-sass": "^3.3.7", - "core-js": "^2.4.1", + "core-js": "^2.5.1", "css": "^2.2.1", - "css-loader": "^0.25.0", - "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", + "css-loader": "^0.28.7", + "event-source-polyfill": "^0.0.9", + "expose-loader": "^0.7.3", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^0.11.2", + "html-loader": "^0.5.1", "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", "json-loader": "^0.5.4", @@ -52,18 +53,18 @@ "node-sass": "^4.5.2", "preboot": "^5.0.0", "raw-loader": "^0.5.1", - "rimraf": "^2.6.1", - "rxjs": "^5.0.1", - "sass-loader": "^6.0.3", + "rimraf": "^2.6.2", + "rxjs": "^5.4.3", + "sass-loader": "^6.0.6", "signalr": "^2.2.1", - "style-loader": "^0.13.1", + "style-loader": "^0.18.2", "to-string-loader": "^1.1.5", - "typescript": "2.3.4", + "typescript": "2.5.2", "url-loader": "^0.5.7", - "webpack": "^2.2.0", - "webpack-hot-middleware": "^2.12.2", - "webpack-merge": "^0.14.1", - "zone.js": "^0.8.9" + "webpack": "^3.6.0", + "webpack-hot-middleware": "^2.19.1", + "webpack-merge": "^4.1.0", + "zone.js": "^0.8.17" }, "devDependencies": { "@angular/cli": "^1.3.2", @@ -84,6 +85,7 @@ "karma-remap-coverage": "^0.1.4", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.3", - "tslint": "^5.0.0" + "tslint": "^5.7.0", + "webpack-bundle-analyzer": "^2.9.0" } } diff --git a/webpack.config.js b/webpack.config.js index 06435301..02ff0a01 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,7 @@ const webpack = require('webpack'); const merge = require('webpack-merge'); const AotPlugin = require('@ngtools/webpack').AotPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const { sharedModuleRules } = require('./webpack.additions'); @@ -56,6 +57,7 @@ module.exports = (env) => { moduleFilenameTemplate: path.relative(clientBundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk }) ] : [ + // new BundleAnalyzerPlugin(), // Plugins that apply in production builds only new webpack.optimize.UglifyJsPlugin(), new AotPlugin({ @@ -68,7 +70,7 @@ module.exports = (env) => { // Configuration for server-side (prerendering) bundle suitable for running in Node const serverBundleConfig = merge(sharedConfig, { - resolve: { mainFields: ['main'] }, + // resolve: { mainFields: ['main'] }, entry: { 'main-server': './ClientApp/boot.server.ts' }, plugins: [ new webpack.DllReferencePlugin({ @@ -78,6 +80,10 @@ module.exports = (env) => { name: './vendor' }) ].concat(isDevBuild ? [] : [ + new webpack.optimize.UglifyJsPlugin({ + compress: false, + mangle: false + }), // Plugins that apply in production builds only new AotPlugin({ tsConfigPath: './tsconfig.json', @@ -90,7 +96,7 @@ module.exports = (env) => { path: path.join(__dirname, './ClientApp/dist') }, target: 'node', - devtool: 'inline-source-map' + devtool: isDevBuild ? 'inline-source-map': false }); return [clientBundleConfig, serverBundleConfig]; diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js index 7ae8a6d2..3e07b78b 100644 --- a/webpack.config.vendor.js +++ b/webpack.config.vendor.js @@ -16,8 +16,8 @@ const treeShakableModules = [ 'zone.js', ]; const nonTreeShakableModules = [ - 'bootstrap', - 'bootstrap/dist/css/bootstrap.css', + // 'bootstrap', + // 'bootstrap/dist/css/bootstrap.css', 'core-js', // 'es6-promise', // 'es6-shim', @@ -90,7 +90,9 @@ module.exports = (env) => { path: path.join(__dirname, 'ClientApp', 'dist', '[name]-manifest.json'), name: '[name]_[hash]' }) - ] + ].concat(isDevBuild ? [] : [ + new webpack.optimize.UglifyJsPlugin() + ]) }); return [clientBundleConfig, serverBundleConfig]; From b288ff49a35acaa116c34f458ff0819608507af0 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sat, 30 Sep 2017 19:47:46 -0400 Subject: [PATCH 069/142] fix(moment): temp fix adding moment to repo --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index f7d886f7..d6f32bce 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", "json-loader": "^0.5.4", + "moment": "2.18.1" "ng2-signalr": "2.0.4", "ngx-bootstrap": "2.0.0-beta.3", "node-sass": "^4.5.2", From ec03396a97aa6ff6f583678bca03057b369864a1 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 1 Oct 2017 14:00:44 -0400 Subject: [PATCH 070/142] fix(moment): add missing coma --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6f32bce..aaa5af12 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "isomorphic-fetch": "^2.2.1", "jquery": "^2.2.1", "json-loader": "^0.5.4", - "moment": "2.18.1" + "moment": "2.18.1", "ng2-signalr": "2.0.4", "ngx-bootstrap": "2.0.0-beta.3", "node-sass": "^4.5.2", From 9562cf946e39ee73ff78590a0a72bfca1b1c6273 Mon Sep 17 00:00:00 2001 From: Isaac Levin Date: Tue, 3 Oct 2017 15:40:26 -0400 Subject: [PATCH 071/142] Remove SignalR references, added to howto branch instead --- ClientApp/app/app.module.browser.ts | 16 +----- ClientApp/app/app.module.ts | 13 ----- .../app/containers/chat/chat.component.html | 51 ------------------ .../app/containers/chat/chat.component.scss | 52 ------------------- .../app/containers/chat/chat.component.ts | 45 ---------------- ClientApp/app/shared/route.resolver.ts | 21 -------- Views/Shared/_Layout.cshtml | 10 +--- package.json | 2 - 8 files changed, 2 insertions(+), 208 deletions(-) delete mode 100644 ClientApp/app/containers/chat/chat.component.html delete mode 100644 ClientApp/app/containers/chat/chat.component.scss delete mode 100644 ClientApp/app/containers/chat/chat.component.ts delete mode 100644 ClientApp/app/shared/route.resolver.ts diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index 70918765..ac318d44 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -3,8 +3,6 @@ import { BrowserModule } from '@angular/platform-browser'; import { APP_BASE_HREF } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { SignalRModule, SignalRConfiguration } from 'ng2-signalr'; - import { ORIGIN_URL } from './shared/constants/baseurl.constants'; import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; @@ -13,17 +11,6 @@ import { BrowserTransferStateModule } from '../modules/transfer-state/browser-tr import { BrowserPrebootModule } from 'preboot/browser'; -export function createConfig(): SignalRConfiguration { - const signalRConfig = new SignalRConfiguration(); - - signalRConfig.hubName = 'Ng2SignalRHub'; - signalRConfig.qs = { user: 'donald' }; - signalRConfig.url = 'http://ng2-signalr-backend.azurewebsites.net/'; - signalRConfig.logging = true; - - return signalRConfig; -} - export function getOriginUrl() { return window.location.origin; } @@ -44,9 +31,8 @@ export function getRequest() { BrowserTransferStateModule, // Our Common AppModule - AppModuleShared, + AppModuleShared - SignalRModule.forRoot(createConfig) ], providers: [ { diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index 57498bb8..61297b35 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -110,19 +110,6 @@ export function createTranslateLoader(http: Http, baseHref) { ] } }, - { - path: 'chat', component: ChatComponent, - // Wait until the resolve is finished before loading the Route - resolve: { connection: ConnectionResolver }, - data: { - title: 'SignalR chat example', - meta: [{ name: 'description', content: 'This is an Chat page Description!' }], - links: [ - { rel: 'canonical', href: 'http://blogs.example.com/chat/something' }, - { rel: 'alternate', hreflang: 'es', href: 'http://es.example.com/chat' } - ] - } - }, { path: 'ngx-bootstrap', component: NgxBootstrapComponent, data: { diff --git a/ClientApp/app/containers/chat/chat.component.html b/ClientApp/app/containers/chat/chat.component.html deleted file mode 100644 index d44cc419..00000000 --- a/ClientApp/app/containers/chat/chat.component.html +++ /dev/null @@ -1,51 +0,0 @@ -

    WebSockets (SignalR) Chatroom Example

    - -
    - Data is stored in Azure here - so even in localhost you might be chatting with other people! -
    - -
    -
    -
    -
    - Angular Chat -
    -
    -
      - -
    • - - User Avatar - - -
      -
      - {{message.user}} - x mins ago -
      -

      - {{ message.content }} -

      -
      -
    • - -
    -
    - -
    -
    -
    - -
    {{ chatMessages | json }}
    diff --git a/ClientApp/app/containers/chat/chat.component.scss b/ClientApp/app/containers/chat/chat.component.scss deleted file mode 100644 index 8e085732..00000000 --- a/ClientApp/app/containers/chat/chat.component.scss +++ /dev/null @@ -1,52 +0,0 @@ -.chat { - list-style: none; - margin: 0; - padding: 0; -} - -.chat li { - margin-bottom: 10px; - padding-bottom: 5px; - border-bottom: 1px dotted #B3A9A9; -} - - -/* -.chat li.left .chat-body { - margin-left: 60px; -} -*/ - -.chat li.right .chat-body { - margin-right: 60px; -} - -.chat li .chat-body p { - margin: 0; - color: #777777; -} - -.panel .slidedown .glyphicon, -.chat .glyphicon { - margin-right: 5px; -} - -.panel-body { - overflow-y: scroll; - height: 500px; -} - -::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - background-color: #F5F5F5; -} - -::-webkit-scrollbar { - width: 12px; - background-color: #F5F5F5; -} - -::-webkit-scrollbar-thumb { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); - background-color: #555; -} diff --git a/ClientApp/app/containers/chat/chat.component.ts b/ClientApp/app/containers/chat/chat.component.ts deleted file mode 100644 index 78cbdb47..00000000 --- a/ClientApp/app/containers/chat/chat.component.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Component, OnInit, Inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { SignalR, BroadcastEventListener, SignalRConnection } from 'ng2-signalr'; -import { Subscription } from 'rxjs/Subscription'; - -export class ChatMessage { - constructor(public content: string, public user: string) { } -} - -@Component({ - selector: 'chat', - templateUrl: './chat.component.html', - styleUrls: ['./chat.component.scss'] -}) -export class ChatComponent implements OnInit { - - public chatMessages: ChatMessage[] = []; - - private _connection: SignalRConnection; - private _subscription: Subscription; - - constructor(route: ActivatedRoute) { - this._connection = route.snapshot.data['connection']; - } - - ngOnInit() { - const onMessageSent$ = new BroadcastEventListener('OnMessageSent'); - this._connection.listen(onMessageSent$); - this._subscription = onMessageSent$.subscribe((chatMessage: ChatMessage) => { - this.chatMessages.push(chatMessage); - console.log('chat messages', this.chatMessages); - }); - } - - // send chat message to server - sendMessage(user, messageInput) { - console.log('send message', user, messageInput.value); - this._connection.invoke('Chat', new ChatMessage(messageInput.value, user)) - .catch((err: any) => console.log('Failed to invoke', err)); - - messageInput.value = ''; - } - -} diff --git a/ClientApp/app/shared/route.resolver.ts b/ClientApp/app/shared/route.resolver.ts deleted file mode 100644 index 43b0078c..00000000 --- a/ClientApp/app/shared/route.resolver.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Injectable } from '@angular/core'; -import { Resolve, Router } from '@angular/router'; - -import { SignalR, ISignalRConnection } from 'ng2-signalr'; - -@Injectable() -export class ConnectionResolver implements Resolve { - - constructor( - private _signalR: SignalR, - private _router: Router - ) { } - - resolve(): Promise { - return this._signalR.connect().then((item) => { - return item; - }).catch(() => { - return this._router.navigate(['/']); - }); - } -} diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 49ecd9b0..993b5133 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,4 +1,4 @@ - + @@ -18,14 +18,6 @@ @RenderBody() - - - - - @Html.Raw(ViewData["TransferData"]) @Html.Raw(ViewData["Scripts"]) diff --git a/package.json b/package.json index aaa5af12..9ebbf620 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "jquery": "^2.2.1", "json-loader": "^0.5.4", "moment": "2.18.1", - "ng2-signalr": "2.0.4", "ngx-bootstrap": "2.0.0-beta.3", "node-sass": "^4.5.2", "preboot": "^5.0.0", @@ -57,7 +56,6 @@ "rimraf": "^2.6.2", "rxjs": "^5.4.3", "sass-loader": "^6.0.6", - "signalr": "^2.2.1", "style-loader": "^0.18.2", "to-string-loader": "^1.1.5", "typescript": "2.5.2", From 63792d113e91b6f55780f2df41e1dc6be2adb462 Mon Sep 17 00:00:00 2001 From: Isaac Levin Date: Tue, 3 Oct 2017 15:54:34 -0400 Subject: [PATCH 072/142] refactor HomeController --- Server/Controllers/HomeController.cs | 62 ++----------------------- Server/Helpers/HttpRequestExtensions.cs | 23 +++++++++ Server/Helpers/Prerender.cs | 54 +++++++++++++++++++++ Server/Models/IRequest.cs | 14 ++++++ Server/Models/TransferData.cs | 15 ++++++ 5 files changed, 109 insertions(+), 59 deletions(-) create mode 100644 Server/Helpers/HttpRequestExtensions.cs create mode 100644 Server/Helpers/Prerender.cs create mode 100644 Server/Models/IRequest.cs create mode 100644 Server/Models/TransferData.cs diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 18899d50..71798dd5 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -1,3 +1,4 @@ +using Asp2017.Server.Helpers; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Http; using System.Diagnostics; using System; +using Asp2017.Server.Models; namespace AspCoreServer.Controllers { @@ -17,39 +19,7 @@ public class HomeController : Controller [HttpGet] public async Task Index() { - var nodeServices = Request.HttpContext.RequestServices.GetRequiredService(); - var hostEnv = Request.HttpContext.RequestServices.GetRequiredService(); - - var applicationBasePath = hostEnv.ContentRootPath; - var requestFeature = Request.HttpContext.Features.Get(); - var unencodedPathAndQuery = requestFeature.RawTarget; - var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; - - // ** TransferData concept ** - // Here we can pass any Custom Data we want ! - - // By default we're passing down Cookies, Headers, Host from the Request object here - TransferData transferData = new TransferData(); - transferData.request = AbstractHttpContextRequestInfo(Request); - transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; - // Add more customData here, add it to the TransferData class - - //Prerender now needs CancellationToken - System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); - System.Threading.CancellationToken cancelToken = cancelSource.Token; - - // Prerender / Serialize application (with Universal) - var prerenderResult = await Prerenderer.RenderToString( - "/", - nodeServices, - cancelToken, - new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), - unencodedAbsoluteUrl, - unencodedPathAndQuery, - transferData, // Our simplified Request object & any other CustommData you want to send! - 30000, - Request.PathBase.ToString() - ); + var prerenderResult = await Prerender.BuildPrerender(Request); ViewData["SpaHtml"] = prerenderResult.Html; // our from Angular ViewData["Title"] = prerenderResult.Globals["title"]; // set our from Angular @@ -87,31 +57,5 @@ public IActionResult Error() { return View(); } - - private IRequest AbstractHttpContextRequestInfo(HttpRequest request) - { - - IRequest requestSimplified = new IRequest(); - requestSimplified.cookies = request.Cookies; - requestSimplified.headers = request.Headers; - requestSimplified.host = request.Host; - - return requestSimplified; - } - } - - public class IRequest - { - public object cookies { get; set; } - public object headers { get; set; } - public object host { get; set; } - } - - public class TransferData - { - public dynamic request { get; set; } - - // Your data here ? - public object thisCameFromDotNET { get; set; } } } diff --git a/Server/Helpers/HttpRequestExtensions.cs b/Server/Helpers/HttpRequestExtensions.cs new file mode 100644 index 00000000..81663602 --- /dev/null +++ b/Server/Helpers/HttpRequestExtensions.cs @@ -0,0 +1,23 @@ +using Asp2017.Server.Models; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Asp2017.Server.Helpers +{ + public static class HttpRequestExtensions + { + public static IRequest AbstractRequestInfo(this HttpRequest request) + { + + IRequest requestSimplified = new IRequest(); + requestSimplified.cookies = request.Cookies; + requestSimplified.headers = request.Headers; + requestSimplified.host = request.Host; + + return requestSimplified; + } + } +} diff --git a/Server/Helpers/Prerender.cs b/Server/Helpers/Prerender.cs new file mode 100644 index 00000000..13124d3c --- /dev/null +++ b/Server/Helpers/Prerender.cs @@ -0,0 +1,54 @@ +using Asp2017.Server.Models; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.NodeServices; +using Microsoft.AspNetCore.SpaServices.Prerendering; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Asp2017.Server.Helpers +{ + public static class Prerender + { + public static async Task<RenderToStringResult> BuildPrerender(HttpRequest Request) + { + var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); + var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); + + var applicationBasePath = hostEnv.ContentRootPath; + var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); + var unencodedPathAndQuery = requestFeature.RawTarget; + var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; + + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + + // By default we're passing down Cookies, Headers, Host from the Request object here + TransferData transferData = new TransferData(); + transferData.request = Request.AbstractRequestInfo(); + transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; + // Add more customData here, add it to the TransferData class + + //Prerender now needs CancellationToken + System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); + System.Threading.CancellationToken cancelToken = cancelSource.Token; + + // Prerender / Serialize application (with Universal) + return await Prerenderer.RenderToString( + "/", + nodeServices, + cancelToken, + new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), + unencodedAbsoluteUrl, + unencodedPathAndQuery, + transferData, // Our simplified Request object & any other CustommData you want to send! + 30000, + Request.PathBase.ToString() + ); + } + } +} diff --git a/Server/Models/IRequest.cs b/Server/Models/IRequest.cs new file mode 100644 index 00000000..407cf571 --- /dev/null +++ b/Server/Models/IRequest.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Asp2017.Server.Models +{ + public class IRequest + { + public object cookies { get; set; } + public object headers { get; set; } + public object host { get; set; } + } +} diff --git a/Server/Models/TransferData.cs b/Server/Models/TransferData.cs new file mode 100644 index 00000000..a89ab38d --- /dev/null +++ b/Server/Models/TransferData.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Asp2017.Server.Models +{ + public class TransferData + { + public dynamic request { get; set; } + + // Your data here ? + public object thisCameFromDotNET { get; set; } + } +} From e2d0e6e13dc1f531fc2ae3553115c9ac2339be23 Mon Sep 17 00:00:00 2001 From: Isaac Levin <isaac.r.levin@gmail.com> Date: Tue, 3 Oct 2017 16:29:20 -0400 Subject: [PATCH 073/142] make prerender an extension --- Server/Controllers/HomeController.cs | 2 +- Server/Helpers/HttpRequestExtensions.cs | 42 +++++++++++++++++++ Server/Helpers/Prerender.cs | 54 ------------------------- 3 files changed, 43 insertions(+), 55 deletions(-) delete mode 100644 Server/Helpers/Prerender.cs diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 71798dd5..8a5a77f6 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -19,7 +19,7 @@ public class HomeController : Controller [HttpGet] public async Task<IActionResult> Index() { - var prerenderResult = await Prerender.BuildPrerender(Request); + var prerenderResult = await Request.BuildPrerender(); ViewData["SpaHtml"] = prerenderResult.Html; // our <app> from Angular ViewData["Title"] = prerenderResult.Globals["title"]; // set our <title> from Angular diff --git a/Server/Helpers/HttpRequestExtensions.cs b/Server/Helpers/HttpRequestExtensions.cs index 81663602..12648b60 100644 --- a/Server/Helpers/HttpRequestExtensions.cs +++ b/Server/Helpers/HttpRequestExtensions.cs @@ -1,5 +1,10 @@ using Asp2017.Server.Models; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.NodeServices; +using Microsoft.AspNetCore.SpaServices.Prerendering; +using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; @@ -19,5 +24,42 @@ public static IRequest AbstractRequestInfo(this HttpRequest request) return requestSimplified; } + + public static async Task<RenderToStringResult> BuildPrerender(this HttpRequest Request) + { + var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); + var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); + + var applicationBasePath = hostEnv.ContentRootPath; + var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); + var unencodedPathAndQuery = requestFeature.RawTarget; + var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; + + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + + // By default we're passing down Cookies, Headers, Host from the Request object here + TransferData transferData = new TransferData(); + transferData.request = Request.AbstractRequestInfo(); + transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; + // Add more customData here, add it to the TransferData class + + //Prerender now needs CancellationToken + System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); + System.Threading.CancellationToken cancelToken = cancelSource.Token; + + // Prerender / Serialize application (with Universal) + return await Prerenderer.RenderToString( + "/", + nodeServices, + cancelToken, + new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), + unencodedAbsoluteUrl, + unencodedPathAndQuery, + transferData, // Our simplified Request object & any other CustommData you want to send! + 30000, + Request.PathBase.ToString() + ); + } } } diff --git a/Server/Helpers/Prerender.cs b/Server/Helpers/Prerender.cs deleted file mode 100644 index 13124d3c..00000000 --- a/Server/Helpers/Prerender.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Asp2017.Server.Models; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.NodeServices; -using Microsoft.AspNetCore.SpaServices.Prerendering; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Asp2017.Server.Helpers -{ - public static class Prerender - { - public static async Task<RenderToStringResult> BuildPrerender(HttpRequest Request) - { - var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>(); - var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>(); - - var applicationBasePath = hostEnv.ContentRootPath; - var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>(); - var unencodedPathAndQuery = requestFeature.RawTarget; - var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; - - // ** TransferData concept ** - // Here we can pass any Custom Data we want ! - - // By default we're passing down Cookies, Headers, Host from the Request object here - TransferData transferData = new TransferData(); - transferData.request = Request.AbstractRequestInfo(); - transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)"; - // Add more customData here, add it to the TransferData class - - //Prerender now needs CancellationToken - System.Threading.CancellationTokenSource cancelSource = new System.Threading.CancellationTokenSource(); - System.Threading.CancellationToken cancelToken = cancelSource.Token; - - // Prerender / Serialize application (with Universal) - return await Prerenderer.RenderToString( - "/", - nodeServices, - cancelToken, - new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), - unencodedAbsoluteUrl, - unencodedPathAndQuery, - transferData, // Our simplified Request object & any other CustommData you want to send! - 30000, - Request.PathBase.ToString() - ); - } - } -} From c0dfc73f2d7179039248638e2dd4f36961f399cc Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 4 Oct 2017 16:02:02 -0400 Subject: [PATCH 074/142] fix(bug): fix 404 and build issues --- ClientApp/app/app.module.ts | 8 ++++---- ClientApp/app/components/navmenu/navmenu.component.html | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index 61297b35..1c492f1d 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -16,13 +16,13 @@ import { HomeComponent } from './containers/home/home.component'; import { UsersComponent } from './containers/users/users.component'; import { UserDetailComponent } from './components/user-detail/user-detail.component'; import { CounterComponent } from './containers/counter/counter.component'; -import { ChatComponent } from './containers/chat/chat.component'; +// import { ChatComponent } from './containers/chat/chat.component'; import { NotFoundComponent } from './containers/not-found/not-found.component'; import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; import { LinkService } from './shared/link.service'; import { UserService } from './shared/user.service'; -import { ConnectionResolver } from './shared/route.resolver'; +// import { ConnectionResolver } from './shared/route.resolver'; import { ORIGIN_URL } from './shared/constants/baseurl.constants'; import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module'; @@ -43,7 +43,7 @@ export function createTranslateLoader(http: Http, baseHref) { UsersComponent, UserDetailComponent, HomeComponent, - ChatComponent, + // ChatComponent, NotFoundComponent, NgxBootstrapComponent ], @@ -145,7 +145,7 @@ export function createTranslateLoader(http: Http, baseHref) { providers: [ LinkService, UserService, - ConnectionResolver, + // ConnectionResolver, TranslateModule ] }) diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index 57b9c351..bd6eadda 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -39,11 +39,11 @@ <span class='glyphicon glyphicon-star-empty'></span> Lazy-loaded demo </a> </li> - <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> + <!-- <li [routerLinkActive]="['link-active']" (click)="collapseMenu()"> <a [routerLink]="['/chat']"> <span class='glyphicon glyphicon-comment'></span> Chat </a> - </li> + </li> --> </ul> </div> </div> From ba9f260384ed107256dd4f6222adb7a1486968bc Mon Sep 17 00:00:00 2001 From: jamescoffman23 <15820584+jamescoffman23@users.noreply.github.com> Date: Sat, 7 Oct 2017 11:30:18 -0500 Subject: [PATCH 075/142] Update _Layout.cshtml (#433) --- Views/Shared/_Layout.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Views/Shared/_Layout.cshtml b/Views/Shared/_Layout.cshtml index 993b5133..bcf5be51 100644 --- a/Views/Shared/_Layout.cshtml +++ b/Views/Shared/_Layout.cshtml @@ -1,7 +1,7 @@ <!DOCTYPE html> <html> <head> - <base href="http://github.com/" /> + <base href="http://github.com/@(Url.Content("~/"))" /> <title>@ViewData["Title"] From 73da5694435cf014218e57194aad0c718dffd168 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 10 Oct 2017 19:13:31 -0400 Subject: [PATCH 076/142] docs(demo): update to 5.0 branch in readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9e30c7a4..d7ac5277 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! +> [(upcoming) Angular 5.0 demo Branch Here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/angular-5.0-updates) +

    ASP.NET Core 2.0 Angular 4+ Starter

    From f31ca8a8d054d11806df13e79d48282ea03f1687 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sat, 28 Oct 2017 22:16:40 -0400 Subject: [PATCH 077/142] Update README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7ac5277..60f75520 100644 --- a/README.md +++ b/README.md @@ -444,9 +444,11 @@ Copyright (c) 2016-2017 [Mark Pieszak](https://github.com/MarkPieszak) ---- -# Looking for Angular & ASP.NET Consulting / Training / support? +# DevHelp.Online - Angular & ASP.NET - Consulting | Training | Development -Contact me at , and let's talk about your projects needs! +Check out **[www.DevHelp.Online](http://DevHelp.Online)** for more info! + +Contact us at , and let's talk about your projects needs. ---- From 5e32bf18abda567225b1afdc7c4646a27ee137e9 Mon Sep 17 00:00:00 2001 From: Romoku Date: Tue, 31 Oct 2017 06:40:07 +0200 Subject: [PATCH 078/142] Update Asp2017.csproj (#451) The 'Client' folder does not exist, so the 'ClientApp' folder should be removed instead. --- Asp2017.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Asp2017.csproj b/Asp2017.csproj index d944c48d..46bd2ffa 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -57,6 +57,6 @@ - +
    From d2ab9e0bbb63847b6c194ab47436476d26c4ea2b Mon Sep 17 00:00:00 2001 From: electricessence Date: Sat, 16 Dec 2017 08:19:47 -0800 Subject: [PATCH 079/142] Allow for dotnet watch run command. (#524) --- Asp2017.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Asp2017.csproj b/Asp2017.csproj index 46bd2ffa..efb619c0 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -13,6 +13,9 @@ + + + From d01d2f979e961c2f78d047261f970931eadfc7f3 Mon Sep 17 00:00:00 2001 From: Adam Pine Date: Thu, 21 Dec 2017 17:14:20 -0500 Subject: [PATCH 080/142] Update Read.me to reflect the ability to use Angular Material --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 60f75520..fa942dbf 100644 --- a/README.md +++ b/README.md @@ -389,7 +389,8 @@ Check the [Gotchas](#gotchas) on how to use `isPlatformBrowser()`. ### How do I Material2 with this repo? -You'll either want to remove SSR for now, or wait as support should be coming to handle platform-server rendering. +~~You'll either want to remove SSR for now, or wait as support should be coming to handle platform-server rendering.~~ +This is now possible, with the recently updated Angular Material changes. We do not have a tutorial available for this yet. ### How can I use jQuery and/or some jQuery plugins with this repo? From 26a59053074bc7aa73fde644379058958fa63924 Mon Sep 17 00:00:00 2001 From: Adam Pine Date: Fri, 22 Dec 2017 10:37:09 -0500 Subject: [PATCH 081/142] Fix 5.0 link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa942dbf..10e2f21c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! -> [(upcoming) Angular 5.0 demo Branch Here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/angular-5.0-updates) +> [(upcoming) Angular 5.0 demo Branch Here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/angular-5.0-WIP)

    ASP.NET Core 2.0 Angular 4+ Starter From 7eafaf86aa92476c47f34a3d5f407a325b9857d7 Mon Sep 17 00:00:00 2001 From: Adam Pine Date: Fri, 22 Dec 2017 11:24:14 -0500 Subject: [PATCH 082/142] Make links to FAQ more visible Added link to the HOW-TO label --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 10e2f21c..e5578667 100644 --- a/README.md +++ b/README.md @@ -374,7 +374,7 @@ constructor(element: ElementRef, renderer: Renderer2) { ---- -# FAQ - Also check out the [FAQ Issues label](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq) +# FAQ - Also check out the [!FAQ Issues label!](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?utf8=%E2%9C%93&q=is%3Aissue%20label%3Afaq) and the [!HOW-TO Issues Label!](https://github.com/MarkPieszak/aspnetcore-angular2-universal/issues?q=is%3Aissue+label%3A%22HOW+TO+-+Guide%22) ### How can I disable SSR (Server-side rendering)? From 40bf6c19d233b713fde87330ee50660ed35fd668 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 23 Jan 2018 15:02:42 -0500 Subject: [PATCH 083/142] Feature(Angular 5.0): Upgrade to working copy of 5.0 (#538) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works both in Development & Production. 🎁 (Apologies on the delay) Now dotnet publish won't fail, and you can successfully build Production builds. In the future, boot.server.PRODUCTION.ts probably won't be needed. We'll be working on upgrading this to use the CLI version that Steve Sanderson has been working on the past few months - this will clean-up a lot of the configuration so that it's all handled by the CLI itself. (Aka: no more webpack files for us to deal with here) ✨ This should close out many issues: #535 #482 (others as well, can't find them at the moment) --- ClientApp/app/app.component.html | 2 +- ClientApp/app/app.component.ts | 11 +- ClientApp/app/app.module.browser.ts | 10 +- ClientApp/app/app.module.server.ts | 25 +- ClientApp/app/app.module.ts | 28 +- .../components/navmenu/navmenu.component.html | 2 +- .../components/navmenu/navmenu.component.ts | 10 +- .../user-detail/user-detail.component.ts | 2 +- .../containers/counter/counter.component.ts | 2 +- .../app/containers/home/home.component.html | 12 +- .../app/containers/home/home.component.ts | 2 +- .../app/containers/lazy/lazy.component.ts | 2 +- .../not-found/not-found.component.ts | 2 +- .../app/containers/users/users.component.html | 2 +- .../app/containers/users/users.component.ts | 21 +- .../app/shared/constants/baseurl.constants.ts | 3 - ClientApp/app/shared/constants/request.ts | 3 - ClientApp/app/shared/user.service.ts | 53 ++-- ClientApp/boot.browser.ts | 2 - ClientApp/boot.server.PRODUCTION.ts | 38 +++ ClientApp/boot.server.ts | 18 +- .../transfer-http/transfer-http.module.ts | 10 - .../modules/transfer-http/transfer-http.ts | 152 ----------- .../browser-transfer-state.module.ts | 20 -- .../server-transfer-state.module.ts | 12 - .../transfer-state/server-transfer-state.ts | 36 --- .../modules/transfer-state/transfer-state.ts | 40 --- .../polyfills/temporary-aspnetcore-engine.ts | 254 ------------------ ClientApp/tsconfig.app.json | 3 +- README.md | 39 +-- Server/Controllers/HomeController.cs | 2 +- Startup.cs | 4 +- Views/Home/Index.cshtml | 6 +- package.json | 40 +-- tslint.json | 3 +- webpack.config.js | 63 +++-- webpack.config.vendor.js | 2 +- 37 files changed, 232 insertions(+), 704 deletions(-) delete mode 100644 ClientApp/app/shared/constants/baseurl.constants.ts delete mode 100644 ClientApp/app/shared/constants/request.ts create mode 100644 ClientApp/boot.server.PRODUCTION.ts delete mode 100644 ClientApp/modules/transfer-http/transfer-http.module.ts delete mode 100644 ClientApp/modules/transfer-http/transfer-http.ts delete mode 100644 ClientApp/modules/transfer-state/browser-transfer-state.module.ts delete mode 100644 ClientApp/modules/transfer-state/server-transfer-state.module.ts delete mode 100644 ClientApp/modules/transfer-state/server-transfer-state.ts delete mode 100644 ClientApp/modules/transfer-state/transfer-state.ts delete mode 100644 ClientApp/polyfills/temporary-aspnetcore-engine.ts diff --git a/ClientApp/app/app.component.html b/ClientApp/app/app.component.html index a3e3bf9b..0345c682 100644 --- a/ClientApp/app/app.component.html +++ b/ClientApp/app/app.component.html @@ -1,5 +1,5 @@

    - +
    diff --git a/ClientApp/app/app.component.ts b/ClientApp/app/app.component.ts index 02b91787..7a6a75ed 100644 --- a/ClientApp/app/app.component.ts +++ b/ClientApp/app/app.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, OnDestroy, Inject, ViewEncapsulation, RendererFactory2, PLATFORM_ID } from '@angular/core'; +import { Component, OnInit, OnDestroy, Inject, ViewEncapsulation, RendererFactory2, PLATFORM_ID, Injector } from '@angular/core'; import { Router, NavigationEnd, ActivatedRoute, PRIMARY_OUTLET } from '@angular/router'; import { Meta, Title, DOCUMENT, MetaDefinition } from '@angular/platform-browser'; import { Subscription } from 'rxjs/Subscription'; @@ -7,10 +7,10 @@ import { LinkService } from './shared/link.service'; // i18n support import { TranslateService } from '@ngx-translate/core'; -import { REQUEST } from './shared/constants/request'; +import { REQUEST } from '@nguniversal/aspnetcore-engine'; @Component({ - selector: 'app', + selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], encapsulation: ViewEncapsulation.None @@ -23,6 +23,7 @@ export class AppComponent implements OnInit, OnDestroy { private defaultPageTitle: string = 'My App'; private routerSub$: Subscription; + private request; constructor( private router: Router, @@ -31,7 +32,7 @@ export class AppComponent implements OnInit, OnDestroy { private meta: Meta, private linkService: LinkService, public translate: TranslateService, - @Inject(REQUEST) private request + private injector: Injector ) { // this language will be used as a fallback when a translation isn't found in the current language translate.setDefaultLang('en'); @@ -39,6 +40,8 @@ export class AppComponent implements OnInit, OnDestroy { // the lang to use, if the lang isn't available, it will use the current loader to get them translate.use('en'); + this.request = this.injector.get(REQUEST); + console.log(`What's our REQUEST Object look like?`); console.log(`The Request object only really exists on the Server, but on the Browser we can at least see Cookies`); console.log(this.request); diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index ac318d44..56d1c20c 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -3,12 +3,10 @@ import { BrowserModule } from '@angular/platform-browser'; import { APP_BASE_HREF } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ORIGIN_URL } from './shared/constants/baseurl.constants'; +import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine'; import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; -import { REQUEST } from './shared/constants/request'; -import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; - +import { BrowserTransferStateModule } from '@angular/platform-browser'; import { BrowserPrebootModule } from 'preboot/browser'; export function getOriginUrl() { @@ -23,12 +21,8 @@ export function getRequest() { @NgModule({ bootstrap: [AppComponent], imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Server NgModule - }), BrowserPrebootModule.replayEvents(), BrowserAnimationsModule, - BrowserTransferStateModule, // Our Common AppModule AppModuleShared diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts index 9e22b71e..25711851 100644 --- a/ClientApp/app/app.module.server.ts +++ b/ClientApp/app/app.module.server.ts @@ -5,33 +5,28 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; -import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module'; -import { TransferState } from '../modules/transfer-state/transfer-state'; +import { ServerTransferStateModule } from '@angular/platform-server'; import { ServerPrebootModule } from 'preboot/server'; @NgModule({ bootstrap: [AppComponent], imports: [ - BrowserModule.withServerTransition({ - appId: 'my-app-id' // make sure this matches with your Browser NgModule - }), + // Our Common AppModule + AppModuleShared, + ServerModule, - ServerPrebootModule.recordEvents({ appRoot: 'app' }), + ServerPrebootModule.recordEvents({ appRoot: 'app-root' }), NoopAnimationsModule, - ServerTransferStateModule, - - // Our Common AppModule - AppModuleShared + // HttpTransferCacheModule still needs fixes for 5.0 + // Leave this commented out for now, as it breaks Server-renders + // Looking into fixes for this! - @MarkPieszak + // ServerTransferStateModule // <-- broken for the time-being with ASP.NET ] }) export class AppModule { - constructor(private transferState: TransferState) { } + constructor() { } - // Gotcha (needs to be an arrow function) - ngOnBootstrap = () => { - this.transferState.inject(); - } } diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index 1c492f1d..d31250c4 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -2,7 +2,10 @@ import { NgModule, Inject } from '@angular/core'; import { RouterModule, PreloadAllModules } from '@angular/router'; import { CommonModule, APP_BASE_HREF } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; +import { HttpClientModule, HttpClient } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; +import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; +import { TransferHttpCacheModule } from '@nguniversal/common'; import { Ng2BootstrapModule } from 'ngx-bootstrap'; @@ -16,17 +19,14 @@ import { HomeComponent } from './containers/home/home.component'; import { UsersComponent } from './containers/users/users.component'; import { UserDetailComponent } from './components/user-detail/user-detail.component'; import { CounterComponent } from './containers/counter/counter.component'; -// import { ChatComponent } from './containers/chat/chat.component'; import { NotFoundComponent } from './containers/not-found/not-found.component'; import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-bootstrap.component'; import { LinkService } from './shared/link.service'; import { UserService } from './shared/user.service'; -// import { ConnectionResolver } from './shared/route.resolver'; -import { ORIGIN_URL } from './shared/constants/baseurl.constants'; -import { TransferHttpModule } from '../modules/transfer-http/transfer-http.module'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine'; -export function createTranslateLoader(http: Http, baseHref) { +export function createTranslateLoader(http: HttpClient, baseHref) { // Temporary Azure hack if (baseHref === null && typeof window !== 'undefined') { baseHref = window.location.origin; @@ -43,24 +43,28 @@ export function createTranslateLoader(http: Http, baseHref) { UsersComponent, UserDetailComponent, HomeComponent, - // ChatComponent, NotFoundComponent, NgxBootstrapComponent ], imports: [ CommonModule, - HttpModule, + BrowserModule.withServerTransition({ + appId: 'my-app-id' // make sure this matches with your Server NgModule + }), + HttpClientModule, + TransferHttpCacheModule, + BrowserTransferStateModule, + + FormsModule, Ng2BootstrapModule.forRoot(), // You could also split this up if you don't want the Entire Module imported - TransferHttpModule, // Our Http TransferData method - // i18n support TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: (createTranslateLoader), - deps: [Http, [ORIGIN_URL]] + deps: [HttpClient, [ORIGIN_URL]] } }), @@ -145,9 +149,9 @@ export function createTranslateLoader(http: Http, baseHref) { providers: [ LinkService, UserService, - // ConnectionResolver, TranslateModule - ] + ], + bootstrap: [AppComponent] }) export class AppModuleShared { } diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index bd6eadda..08e1b0fc 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -8,7 +8,7 @@ - Angular 4 Universal & ASP.NET Core + Angular 5 Universal & ASP.NET Core
    diff --git a/ClientApp/app/components/navmenu/navmenu.component.ts b/ClientApp/app/components/navmenu/navmenu.component.ts index ac36f036..fb0cf22c 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.ts +++ b/ClientApp/app/components/navmenu/navmenu.component.ts @@ -1,23 +1,23 @@ import { Component } from '@angular/core'; @Component({ - selector: 'nav-menu', + selector: 'app-nav-menu', templateUrl: './navmenu.component.html', styleUrls: ['./navmenu.component.css'] }) export class NavMenuComponent { - collapse: string = "collapse"; + collapse: string = 'collapse'; collapseNavbar(): void { if (this.collapse.length > 1) { - this.collapse = ""; + this.collapse = ''; } else { - this.collapse = "collapse"; + this.collapse = 'collapse'; } } collapseMenu() { - this.collapse = "collapse" + this.collapse = 'collapse'; } } diff --git a/ClientApp/app/components/user-detail/user-detail.component.ts b/ClientApp/app/components/user-detail/user-detail.component.ts index 9db88355..d21d3415 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.ts +++ b/ClientApp/app/components/user-detail/user-detail.component.ts @@ -3,7 +3,7 @@ import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @Component({ - selector: 'user-detail', + selector: 'app-user-detail', templateUrl: './user-detail.component.html' }) export class UserDetailComponent { diff --git a/ClientApp/app/containers/counter/counter.component.ts b/ClientApp/app/containers/counter/counter.component.ts index 69de17d9..5adb5195 100644 --- a/ClientApp/app/containers/counter/counter.component.ts +++ b/ClientApp/app/containers/counter/counter.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - selector: 'counter', + selector: 'app-counter', templateUrl: './counter.component.html' }) export class CounterComponent { diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index 06d801e7..3a4db63b 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -1,7 +1,7 @@ 

    {{ title }}

    - Enjoy the latest features from .NET Core & Angular 4.0! + Enjoy the latest features from .NET Core & Angular 5.0!
    For more info check the repo here: AspNetCore-Angular2-Universal repo

    @@ -12,7 +12,7 @@

    {{ 'HOME_FEATURE_LIST_TITLE' | translate }}

    • ASP.NET Core 2.0 :: ( Visual Studio 2017 )
    • - Angular 4.* front-end UI framework + Angular 5.* front-end UI framework
      • Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.
      • @@ -22,17 +22,10 @@

        {{ 'HOME_FEATURE_LIST_TITLE' | translate }}

      • The latest TypeScript 2.* features -
      • Webpack
          -
        • Hot Module Reloading/Replacement for an amazing development experience.
        • Tree-shaking
        @@ -40,7 +33,6 @@

        {{ 'HOME_FEATURE_LIST_TITLE' | translate }}

      • Bootstrap (ngx-bootstrap) : Bootstrap capable of being rendered even on the server.
      • Unit testing via karma & jasmine.
      • -
      diff --git a/ClientApp/app/containers/home/home.component.ts b/ClientApp/app/containers/home/home.component.ts index 3065c0b1..ff10f2d4 100644 --- a/ClientApp/app/containers/home/home.component.ts +++ b/ClientApp/app/containers/home/home.component.ts @@ -8,7 +8,7 @@ import { TranslateService } from '@ngx-translate/core'; }) export class HomeComponent implements OnInit { - title: string = 'Angular 4.0 Universal & ASP.NET Core 2.0 advanced starter-kit'; + title: string = 'Angular 5.x Universal & ASP.NET Core 2.0 advanced starter-kit'; // Use "constructor"s only for dependency injection constructor( diff --git a/ClientApp/app/containers/lazy/lazy.component.ts b/ClientApp/app/containers/lazy/lazy.component.ts index 25d6d1a4..97d260fd 100644 --- a/ClientApp/app/containers/lazy/lazy.component.ts +++ b/ClientApp/app/containers/lazy/lazy.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - selector: 'lazy-view', + selector: 'app-lazy-view', template: `

      Lazy-Loaded Component!

      diff --git a/ClientApp/app/containers/not-found/not-found.component.ts b/ClientApp/app/containers/not-found/not-found.component.ts index e39faa05..c59a5f8f 100644 --- a/ClientApp/app/containers/not-found/not-found.component.ts +++ b/ClientApp/app/containers/not-found/not-found.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; @Component({ - selector: 'not-found', + selector: 'app-not-found', templateUrl: './not-found.component.html' }) export class NotFoundComponent implements OnInit { diff --git a/ClientApp/app/containers/users/users.component.html b/ClientApp/app/containers/users/users.component.html index 5bb77691..1e661476 100644 --- a/ClientApp/app/containers/users/users.component.html +++ b/ClientApp/app/containers/users/users.component.html @@ -26,4 +26,4 @@

      Users

    - + diff --git a/ClientApp/app/containers/users/users.component.ts b/ClientApp/app/containers/users/users.component.ts index 9dd28781..35cc8517 100644 --- a/ClientApp/app/containers/users/users.component.ts +++ b/ClientApp/app/containers/users/users.component.ts @@ -7,7 +7,7 @@ import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @Component({ - selector: 'users', + selector: 'app-users', templateUrl: './users.component.html', styleUrls: ['./users.component.css'], animations: [ @@ -31,15 +31,16 @@ export class UsersComponent implements OnInit { selectedUser: IUser; // Use "constructor"s only for dependency injection - constructor(private userService: UserService) { } + constructor( + private userService: UserService + ) { } // Here you want to handle anything with @Input()'s @Output()'s // Data retrieval / etc - this is when the Component is "ready" and wired up ngOnInit() { this.userService.getUsers().subscribe(result => { - console.log('Get user result: ', result); - console.log('TransferHttp [GET] /api/users/allresult', result); - this.users = result as IUser[]; + console.log('HttpClient [GET] /api/users/allresult', result); + this.users = result; }); } @@ -50,10 +51,8 @@ export class UsersComponent implements OnInit { deleteUser(user) { this.userService.deleteUser(user).subscribe(result => { console.log('Delete user result: ', result); - if (result.ok) { - let position = this.users.indexOf(user); - this.users.splice(position, 1); - } + let position = this.users.indexOf(user); + this.users.splice(position, 1); }, error => { console.log(`There was an issue. ${error._body}.`); }); @@ -62,9 +61,7 @@ export class UsersComponent implements OnInit { addUser(newUserName) { this.userService.addUser(newUserName).subscribe(result => { console.log('Post user result: ', result); - if (result.ok) { - this.users.push(result.json()); - } + this.users.push(result); }, error => { console.log(`There was an issue. ${error._body}.`); }); diff --git a/ClientApp/app/shared/constants/baseurl.constants.ts b/ClientApp/app/shared/constants/baseurl.constants.ts deleted file mode 100644 index 58807bc4..00000000 --- a/ClientApp/app/shared/constants/baseurl.constants.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const ORIGIN_URL = new InjectionToken('ORIGIN_URL'); diff --git a/ClientApp/app/shared/constants/request.ts b/ClientApp/app/shared/constants/request.ts deleted file mode 100644 index 4c553d8a..00000000 --- a/ClientApp/app/shared/constants/request.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { InjectionToken } from '@angular/core'; - -export const REQUEST = new InjectionToken('REQUEST'); diff --git a/ClientApp/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts index 8bcc3bb3..9489e32f 100644 --- a/ClientApp/app/shared/user.service.ts +++ b/ClientApp/app/shared/user.service.ts @@ -1,42 +1,41 @@ -import { Injectable, Inject } from '@angular/core'; +import { Injectable, Inject, Injector } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; import { Http, URLSearchParams } from '@angular/http'; import { APP_BASE_HREF } from '@angular/common'; -import { ORIGIN_URL } from './constants/baseurl.constants'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine'; import { IUser } from '../models/User'; -import { TransferHttp } from '../../modules/transfer-http/transfer-http'; import { Observable } from 'rxjs/Observable'; + @Injectable() export class UserService { - constructor( - private transferHttp: TransferHttp, // Use only for GETS that you want re-used between Server render -> Client render - private http: Http, // Use for everything else - @Inject(ORIGIN_URL) private baseUrl: string) { - } + private baseUrl: string; - getUsers(): Observable { - // ** TransferHttp example / concept ** - // - Here we make an Http call on the server, save the result on the window object and pass it down with the SSR, - // The Client then re-uses this Http result instead of hitting the server again! + constructor( + private http: HttpClient, + private injector: Injector + ) { + this.baseUrl = this.injector.get(ORIGIN_URL); + } - // NOTE : transferHttp also automatically does .map(res => res.json()) for you, so no need for these calls - return this.transferHttp.get(`${this.baseUrl}/api/users`); - } + getUsers() { + return this.http.get(`${this.baseUrl}/api/users`); + } - getUser(user: IUser): Observable { - return this.transferHttp.get(`${this.baseUrl}/api/users/` + user.id); - } + getUser(user: IUser) { + return this.http.get(`${this.baseUrl}/api/users/` + user.id); + } - deleteUser(user: IUser): Observable { - return this.http.delete(`${this.baseUrl}/api/users/` + user.id); - } + deleteUser(user: IUser) { + return this.http.delete(`${this.baseUrl}/api/users/` + user.id); + } - updateUser(user: IUser): Observable { - return this.http.put(`${this.baseUrl}/api/users/` + user.id, user); - } + updateUser(user: IUser){ + return this.http.put(`${this.baseUrl}/api/users/` + user.id, user); + } - addUser(newUserName: string): Observable { - return this.http.post(`${this.baseUrl}/api/users`, { name: newUserName }) - } + addUser(newUserName: string) { + return this.http.post(`${this.baseUrl}/api/users`, { name: newUserName }); + } } diff --git a/ClientApp/boot.browser.ts b/ClientApp/boot.browser.ts index a7830543..329344f7 100644 --- a/ClientApp/boot.browser.ts +++ b/ClientApp/boot.browser.ts @@ -3,8 +3,6 @@ import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module.browser'; -const rootElemTagName = 'app'; // Update this if you change your root component selector - // // Enable either Hot Module Reloading or production mode if (module['hot']) { module['hot'].accept(); diff --git a/ClientApp/boot.server.PRODUCTION.ts b/ClientApp/boot.server.PRODUCTION.ts new file mode 100644 index 00000000..30f8deb0 --- /dev/null +++ b/ClientApp/boot.server.PRODUCTION.ts @@ -0,0 +1,38 @@ +import 'zone.js/dist/zone-node'; +import './polyfills/server.polyfills'; +import { enableProdMode } from '@angular/core'; +import { createServerRenderer } from 'aspnet-prerendering'; + +// Grab the (Node) server-specific NgModule +const { AppModuleNgFactory } = require('./app/app.module.server.ngfactory'); // <-- ignore this - this is Production only +import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; + +enableProdMode(); + +export default createServerRenderer((params) => { + + // Platform-server provider configuration + const setupOptions: IEngineOptions = { + appSelector: '', + ngModule: AppModuleNgFactory, + request: params, + providers: [ + // Optional - Any other Server providers you want to pass + // (remember you'll have to provide them for the Browser as well) + ] + }; + + return ngAspnetCoreEngine(setupOptions).then(response => { + + // Apply your transferData to response.globals + response.globals.transferData = createTransferScript({ + someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object', + fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController + }); + + return ({ + html: response.html, // our serialized + globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up + }); + }); +}); diff --git a/ClientApp/boot.server.ts b/ClientApp/boot.server.ts index 86d65086..c2c0fb47 100644 --- a/ClientApp/boot.server.ts +++ b/ClientApp/boot.server.ts @@ -1,15 +1,11 @@ import 'zone.js/dist/zone-node'; import './polyfills/server.polyfills'; import { enableProdMode } from '@angular/core'; -import { INITIAL_CONFIG } from '@angular/platform-server'; -import { APP_BASE_HREF } from '@angular/common'; -import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; +import { createServerRenderer } from 'aspnet-prerendering'; -import { ORIGIN_URL } from './app/shared/constants/baseurl.constants'; // Grab the (Node) server-specific NgModule import { AppModule } from './app/app.module.server'; -// Temporary * the engine will be on npm soon (`@universal/ng-aspnetcore-engine`) -import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from './polyfills/temporary-aspnetcore-engine'; +import { ngAspnetCoreEngine, IEngineOptions, createTransferScript } from '@nguniversal/aspnetcore-engine'; enableProdMode(); @@ -17,15 +13,17 @@ export default createServerRenderer((params) => { // Platform-server provider configuration const setupOptions: IEngineOptions = { - appSelector: '', + appSelector: '', ngModule: AppModule, request: params, providers: [ - // Optional - Any other Server providers you want to pass (remember you'll have to provide them for the Browser as well) + // Optional - Any other Server providers you want to pass + // (remember you'll have to provide them for the Browser as well) ] }; return ngAspnetCoreEngine(setupOptions).then(response => { + // Apply your transferData to response.globals response.globals.transferData = createTransferScript({ someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object', @@ -33,8 +31,8 @@ export default createServerRenderer((params) => { }); return ({ - html: response.html, - globals: response.globals + html: response.html, // our serialized + globals: response.globals // all of our styles/scripts/meta-tags/link-tags for aspnet to serve up }); }); }); diff --git a/ClientApp/modules/transfer-http/transfer-http.module.ts b/ClientApp/modules/transfer-http/transfer-http.module.ts deleted file mode 100644 index c2875b33..00000000 --- a/ClientApp/modules/transfer-http/transfer-http.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core'; -import { Http, HttpModule } from '@angular/http'; -import { TransferHttp } from './transfer-http'; - -@NgModule({ - providers: [ - TransferHttp - ] -}) -export class TransferHttpModule {} diff --git a/ClientApp/modules/transfer-http/transfer-http.ts b/ClientApp/modules/transfer-http/transfer-http.ts deleted file mode 100644 index 3f9b5d84..00000000 --- a/ClientApp/modules/transfer-http/transfer-http.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; -import { ConnectionBackend, Http, Request, RequestOptions, RequestOptionsArgs, Response } from '@angular/http'; -import { Observable } from 'rxjs/Observable'; -import { Subject } from 'rxjs/Subject'; -import { TransferState } from '../transfer-state/transfer-state'; -import { isPlatformServer } from '@angular/common'; - -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/observable/fromPromise'; - -@Injectable() -export class TransferHttp { - - private isServer = isPlatformServer(this.platformId); - - constructor( - @Inject(PLATFORM_ID) private platformId, - private http: Http, - protected transferState: TransferState - ) { } - - request(uri: string | Request, options?: RequestOptionsArgs): Observable { - return this.getData(uri, options, (url: string, options: RequestOptionsArgs) => { - return this.http.request(url, options); - }); - } - /** - * Performs a request with `get` http method. - */ - get(url: string, options?: RequestOptionsArgs): Observable { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.get(url, options); - }); - } - /** - * Performs a request with `post` http method. - */ - post(url: string, body: any, options?: RequestOptionsArgs): Observable { - return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { - return this.http.post(url, body, options); - }); - } - /** - * Performs a request with `put` http method. - */ - put(url: string, body: any, options?: RequestOptionsArgs): Observable { - - return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { - return this.http.put(url, body, options); - }); - } - /** - * Performs a request with `delete` http method. - */ - delete(url: string, options?: RequestOptionsArgs): Observable { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.delete(url, options); - }); - } - /** - * Performs a request with `patch` http method. - */ - patch(url: string, body: any, options?: RequestOptionsArgs): Observable { - return this.getPostData(url, body, options, (url: string, body: any, options: RequestOptionsArgs) => { - return this.http.patch(url, body.options); - }); - } - /** - * Performs a request with `head` http method. - */ - head(url: string, options?: RequestOptionsArgs): Observable { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.head(url, options); - }); - } - /** - * Performs a request with `options` http method. - */ - options(url: string, options?: RequestOptionsArgs): Observable { - return this.getData(url, options, (url: string, options: RequestOptionsArgs) => { - return this.http.options(url, options); - }); - } - - private getData(uri: string | Request, options: RequestOptionsArgs, callback: (uri: string | Request, options?: RequestOptionsArgs) => Observable) { - - let url = uri; - - if (typeof uri !== 'string') { - url = uri.url; - } - - const key = url + JSON.stringify(options); - - try { - return this.resolveData(key); - - } catch (e) { - return callback(url, options) - .map(res => res.json()) - .do(data => { - if (this.isServer) { - this.setCache(key, data); - } - }); - } - } - - private getPostData(uri: string | Request, body: any, options: RequestOptionsArgs, callback: (uri: string | Request, body: any, options?: RequestOptionsArgs) => Observable) { - - let url = uri; - - if (typeof uri !== 'string') { - url = uri.url; - } - - const key = url + JSON.stringify(body); - - try { - - return this.resolveData(key); - - } catch (e) { - return callback(uri, body, options) - .map(res => res.json()) - .do(data => { - if (this.isServer) { - this.setCache(key, data); - } - }); - } - } - - private resolveData(key: string) { - const data = this.getFromCache(key); - - if (!data) { - throw new Error(); - } - - return Observable.fromPromise(Promise.resolve(data)); - } - - private setCache(key, data) { - return this.transferState.set(key, data); - } - - private getFromCache(key): any { - return this.transferState.get(key); - } -} diff --git a/ClientApp/modules/transfer-state/browser-transfer-state.module.ts b/ClientApp/modules/transfer-state/browser-transfer-state.module.ts deleted file mode 100644 index 20e11421..00000000 --- a/ClientApp/modules/transfer-state/browser-transfer-state.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule, PLATFORM_ID } from '@angular/core'; -import { TransferState } from './transfer-state'; - -export function getTransferState(): TransferState { - const transferState = new TransferState(); - transferState.initialize(window['TRANSFER_STATE'] || {}); - return transferState; -} - -@NgModule({ - providers: [ - { - provide: TransferState, - useFactory: getTransferState - } - ] -}) -export class BrowserTransferStateModule { - -} diff --git a/ClientApp/modules/transfer-state/server-transfer-state.module.ts b/ClientApp/modules/transfer-state/server-transfer-state.module.ts deleted file mode 100644 index 1a77f653..00000000 --- a/ClientApp/modules/transfer-state/server-transfer-state.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { ServerTransferState } from './server-transfer-state'; -import { TransferState } from './transfer-state'; - -@NgModule({ - providers: [ - { provide: TransferState, useClass: ServerTransferState } - ] -}) -export class ServerTransferStateModule { - -} diff --git a/ClientApp/modules/transfer-state/server-transfer-state.ts b/ClientApp/modules/transfer-state/server-transfer-state.ts deleted file mode 100644 index b2890b26..00000000 --- a/ClientApp/modules/transfer-state/server-transfer-state.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Injectable, Optional, RendererFactory2, ViewEncapsulation, Inject, PLATFORM_ID } from '@angular/core'; -import { TransferState } from './transfer-state'; -import { PlatformState } from '@angular/platform-server'; -@Injectable() -export class ServerTransferState extends TransferState { - constructor(private state: PlatformState, private rendererFactory: RendererFactory2) { - super(); - } - - /** - * Inject the State into the bottom of the - */ - inject() { - try { - const document: any = this.state.getDocument(); - const transferStateString = JSON.stringify(this.toJson()); - const renderer = this.rendererFactory.createRenderer(document, { - id: '-1', - encapsulation: ViewEncapsulation.None, - styles: [], - data: {} - }); - - const body = document.body; - - const script = renderer.createElement('script'); - renderer.setValue(script, `window['TRANSFER_STATE'] = ${transferStateString}`); - renderer.appendChild(body, script); - } catch (e) { - console.log('Failed to append TRANSFER_STATE to body'); - console.error(e); - } - } - - -} diff --git a/ClientApp/modules/transfer-state/transfer-state.ts b/ClientApp/modules/transfer-state/transfer-state.ts deleted file mode 100644 index cc963b8f..00000000 --- a/ClientApp/modules/transfer-state/transfer-state.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; - -@Injectable() -export class TransferState { - private _map = new Map(); - - constructor() { } - - keys() { - return this._map.keys(); - } - - get(key: string): any { - const cachedValue = this._map.get(key); - this._map.delete(key); - return cachedValue; - } - - set(key: string, value: any): Map { - return this._map.set(key, value); - } - - toJson(): any { - const obj = {}; - Array.from(this.keys()) - .forEach(key => { - obj[key] = this.get(key); - }); - return obj; - } - - initialize(obj: any): void { - Object.keys(obj) - .forEach(key => { - this.set(key, obj[key]); - }); - } - - inject(): void { } -} diff --git a/ClientApp/polyfills/temporary-aspnetcore-engine.ts b/ClientApp/polyfills/temporary-aspnetcore-engine.ts deleted file mode 100644 index d3157a1b..00000000 --- a/ClientApp/polyfills/temporary-aspnetcore-engine.ts +++ /dev/null @@ -1,254 +0,0 @@ -/* ********* TEMPORARILY HERE ************** - * - will be on npm soon - - * import { ngAspnetCoreEngine } from `@nguniversal/aspnetcore-engine`; - */ -import { Type, NgModuleFactory, NgModuleRef, ApplicationRef, Provider, CompilerFactory, Compiler } from '@angular/core'; -import { platformServer, platformDynamicServer, PlatformState, INITIAL_CONFIG, renderModuleFactory } from '@angular/platform-server'; -import { ResourceLoader } from '@angular/compiler'; -import * as fs from 'fs'; - -import { REQUEST } from '../app/shared/constants/request'; -import { ORIGIN_URL } from '../app/shared/constants/baseurl.constants'; - -export function createTransferScript(transferData: Object): string { - return ``; -} - -export class FileLoader implements ResourceLoader { - get(url: string): Promise { - return new Promise((resolve, reject) => { - fs.readFile(url, (err: NodeJS.ErrnoException, buffer: Buffer) => { - if (err) { - return reject(err); - } - - resolve(buffer.toString()); - }); - }); - } -} - -export interface IRequestParams { - location: any; // e.g., Location object containing information '/some/path' - origin: string; // e.g., 'https://example.com:1234/' - url: string; // e.g., '/some/path' - baseUrl: string; // e.g., '' or '/myVirtualDir' - absoluteUrl: string; // e.g., 'https://example.com:1234/some/path' - domainTasks: Promise; - data: any; // any custom object passed through from .NET -} - -export interface IEngineOptions { - appSelector: string; - request: IRequestParams; - ngModule: Type<{}> | NgModuleFactory<{}>; - providers?: Provider[]; -}; - -export function ngAspnetCoreEngine( - options: IEngineOptions -): Promise<{ html: string, globals: { styles: string, title: string, meta: string, transferData?: {}, [key: string]: any } }> { - - options.providers = options.providers || []; - - const compilerFactory: CompilerFactory = platformDynamicServer().injector.get(CompilerFactory); - const compiler: Compiler = compilerFactory.createCompiler([ - { - providers: [ - { provide: ResourceLoader, useClass: FileLoader } - ] - } - ]); - - return new Promise((resolve, reject) => { - - try { - const moduleOrFactory = options.ngModule; - if (!moduleOrFactory) { - throw new Error('You must pass in a NgModule or NgModuleFactory to be bootstrapped'); - } - - const extraProviders = options.providers.concat( - options.providers, - [ - { - provide: INITIAL_CONFIG, - useValue: { - document: options.appSelector, - url: options.request.url - } - }, - { - provide: ORIGIN_URL, - useValue: options.request.origin - }, { - provide: REQUEST, - useValue: options.request.data.request - } - ] - ); - - const platform = platformServer(extraProviders); - - getFactory(moduleOrFactory, compiler) - .then((factory: NgModuleFactory<{}>) => { - - return platform.bootstrapModuleFactory(factory).then((moduleRef: NgModuleRef<{}>) => { - - const state: PlatformState = moduleRef.injector.get(PlatformState); - const appRef: ApplicationRef = moduleRef.injector.get(ApplicationRef); - - appRef.isStable - .filter((isStable: boolean) => isStable) - .first() - .subscribe((stable) => { - - // Fire the TransferState Cache - const bootstrap = moduleRef.instance['ngOnBootstrap']; - bootstrap && bootstrap(); - - // The parse5 Document itself - const AST_DOCUMENT = state.getDocument(); - - // Strip out the Angular application - const htmlDoc = state.renderToString(); - - const APP_HTML = htmlDoc.substring( - htmlDoc.indexOf('') + 6, - htmlDoc.indexOf('') - ); - - // Strip out Styles / Meta-tags / Title - const STYLES = []; - const SCRIPTS = []; - const META = []; - const LINKS = []; - let TITLE = ''; - - let STYLES_STRING: string = htmlDoc.indexOf('') + 8) - : null; - - const HEAD = AST_DOCUMENT.head; - - let count = 0; - - for (let i = 0; i < HEAD.children.length; i++) { - let element = HEAD.children[i]; - - if (element.name === 'title') { - TITLE = element.children[0].data; - } - - if (element.name === 'script') { - SCRIPTS.push( - `` - ); - } - - // Broken after 4.0 (worked in rc) - // if (element.name === 'style') { - // let styleTag = '`; - // STYLES.push(styleTag); - // } - - if (element.name === 'meta') { - count = count + 1; - let metaString = '\n`); - } - - if (element.name === 'link') { - let linkString = '\n`); - } - } - - resolve({ - html: APP_HTML, - globals: { - styles: STYLES_STRING, - title: TITLE, - scripts: SCRIPTS.join(' '), - meta: META.join(' '), - links: LINKS.join(' ') - } - }); - - moduleRef.destroy(); - - }, (err) => { - // isStable subscription error (Template / code error) - reject(err); - }); - - }, err => { - // bootstrapModuleFactory error - reject(err); - }); - - }, err => { - // getFactory error - reject(err); - }); - - } catch (ex) { - // try/catch error - reject(ex); - } - - }); - -} - -/* ********************** Private / Internal ****************** */ - -const factoryCacheMap = new Map, NgModuleFactory<{}>>(); -function getFactory( - moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler -): Promise> { - - return new Promise>((resolve, reject) => { - // If module has been compiled AoT - if (moduleOrFactory instanceof NgModuleFactory) { - resolve(moduleOrFactory); - return; - } else { - let moduleFactory = factoryCacheMap.get(moduleOrFactory); - - // If module factory is cached - if (moduleFactory) { - resolve(moduleFactory); - return; - } - - // Compile the module and cache it - compiler.compileModuleAsync(moduleOrFactory) - .then((factory) => { - factoryCacheMap.set(moduleOrFactory, factory); - resolve(factory); - }, (err => { - reject(err); - })); - } - }); -} diff --git a/ClientApp/tsconfig.app.json b/ClientApp/tsconfig.app.json index 5e2507db..3f094c64 100644 --- a/ClientApp/tsconfig.app.json +++ b/ClientApp/tsconfig.app.json @@ -8,6 +8,7 @@ }, "exclude": [ "test.ts", - "**/*.spec.ts" + "**/*.spec.ts", + "boot.server.PRODUCTION.ts" ] } diff --git a/README.md b/README.md index e5578667..9dd99499 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ -# ASP.NET Core 2.0 & Angular 4 (+) advanced starter - with Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.0 & Angular 5(+) Advanced Starter - with Server-side prerendering (for Angular SEO)! -> [(upcoming) Angular 5.0 demo Branch Here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/angular-5.0-WIP) +## By [DevHelp.Online](http://www.DevHelp.Online) + +> Updated to the latest Angular 5.x + +> Note ServerTransferModule still in the works - fix coming soon

    - ASP.NET Core 2.0 Angular 4+ Starter + ASP.NET Core 2.0 Angular 5+ Starter

    -### Harness the power of Angular 4+, ASP.NET Core 2.0, now with SEO ! +### Harness the power of Angular 5+, ASP.NET Core 2.0, now with SEO ! Angular SEO in action:

    - ASP.NET Core Angular4 SEO + ASP.NET Core Angular5 SEO

    ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter -for both ASP.NET Core 2.0 using Angular 4.0+, not only for the client-side, but to be rendered on the server for instant +for both ASP.NET Core 2.0 using Angular 5.0+, not only for the client-side, but to be rendered on the server for instant application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). @@ -51,7 +55,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Swagger WebAPI documentation when running in development mode - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) -- **Angular 4.0.0** : +- **Angular 5.0.0** : - (Minimal) Angular-CLI integration - This is to be used mainly for Generating Components/Services/etc. - Usage examples: @@ -139,9 +143,8 @@ export ASPNETCORE_ENVIRONMENT=Development # Upcoming Features: -- Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there) -- Potractor e2e testing -- Add basic Redux State store (Will also hold state durijg HMR builds) +- Fix HttpTransferCacheModule & ServerTransferModule to work with aspnet-engine +- ~~Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there)~~ ---- @@ -268,7 +271,7 @@ Angular application gets serialized into a String, sent to the Browser, along wi The short-version is that we invoke that Node process, passing in our Request object & invoke the `boot.server` file, and we get back a nice object that we pass into .NETs `ViewData` object, and sprinkle through out our `Views/Shared/_Layout.cshtml` and `/Views/Home/index.cshtml` files! -A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/ng-aspnetcore-engine) +A more detailed explanation can be found here: [ng-AspnetCore-Engine Readme](https://github.com/angular/universal/tree/master/modules/aspnetcore-engine) ```csharp // Prerender / Serialize application @@ -293,7 +296,7 @@ Take a look at the `_Layout.cshtml` file for example, notice how we let .NET han - @ViewData["Title"] - AspNET.Core Angular 4.0.0 (+) starter + @ViewData["Title"] - AspNET.Core Angular 5.0.0 (+) starter @@ -334,7 +337,7 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct - This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md) -> When building components in Angular 4 there are a few things to keep in mind. +> When building components in Angular 5 there are a few things to keep in mind. - **`window`**, **`document`**, **`navigator`**, and other browser types - _do not exist on the server_ - so using them, or any library that uses them (jQuery for example) will not work. You do have some options, if you truly need some of this functionality: - If you need to use them, consider limiting them to only your client and wrapping them situationally. You can use the Object injected using the PLATFORM_ID token to check whether the current platform is browser or server. @@ -379,7 +382,7 @@ constructor(element: ElementRef, renderer: Renderer2) { ### How can I disable SSR (Server-side rendering)? Simply comment out the logic within HomeController, and replace `@Html.Raw(ViewData["SpaHtml"])` with just your applications root -AppComponent tag ("app" in our case): ``. +AppComponent tag ("app-root" in our case): ``. > You could also remove any `isPlatformBrowser/etc` logic, and delete the boot.server, app.module.browser & app.module.server files, just make sure your `boot.browser` file points to `app.module`. @@ -395,7 +398,7 @@ This is now possible, with the recently updated Angular Material changes. We do ### How can I use jQuery and/or some jQuery plugins with this repo? > Note: If at all possible, try to avoid using jQuery or libraries dependent on it, as there are -better, more abstract ways of dealing with the DOM in Angular (4+) such as using the Renderer, etc. +better, more abstract ways of dealing with the DOM in Angular (5+) such as using the Renderer, etc. Yes, of course but there are a few things you need to setup before doing this. First, make sure jQuery is included in webpack vendor file, and that you have a webpack Plugin setup for it. `new webpack.ProvidePlugin({ $: 'jquery', jQuery: 'jquery' })` @@ -447,10 +450,14 @@ Copyright (c) 2016-2017 [Mark Pieszak](https://github.com/MarkPieszak) # DevHelp.Online - Angular & ASP.NET - Consulting | Training | Development -Check out **[www.DevHelp.Online](http://DevHelp.Online)** for more info! +Check out **[www.DevHelp.Online](http://DevHelp.Online)** for more info! Twitter [@DevHelpOnline](http://www.twitter.com/DevHelpOnline) Contact us at , and let's talk about your projects needs. +

    + DevHelp.Online - Angular ASPNET JavaScript Consulting Development and Training +

    + ---- ## Follow me online: diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 8a5a77f6..0a674a60 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -21,7 +21,7 @@ public async Task Index() { var prerenderResult = await Request.BuildPrerender(); - ViewData["SpaHtml"] = prerenderResult.Html; // our from Angular + ViewData["SpaHtml"] = prerenderResult.Html; // our from Angular ViewData["Title"] = prerenderResult.Globals["title"]; // set our from Angular ViewData["Styles"] = prerenderResult.Globals["styles"]; // put styles in the correct place ViewData["Scripts"] = prerenderResult.Globals["scripts"]; // scripts (that were in our header) diff --git a/Startup.cs b/Startup.cs index 7736f9e9..b7773ac0 100644 --- a/Startup.cs +++ b/Startup.cs @@ -54,7 +54,7 @@ public void ConfigureServices(IServiceCollection services) // Register the Swagger generator, defining one or more Swagger documents services.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new Info { Title = "Angular 4.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); + c.SwaggerDoc("v1", new Info { Title = "Angular 5.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); }); } @@ -74,7 +74,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true, - HotModuleReplacementEndpoint = "/dist/__webpack_hmr" + HotModuleReplacementEndpoint = "/dist/" }); app.UseSwagger(); app.UseSwaggerUI(c => diff --git a/Views/Home/Index.cshtml b/Views/Home/Index.cshtml index b0796562..8c18039d 100644 --- a/Views/Home/Index.cshtml +++ b/Views/Home/Index.cshtml @@ -1,4 +1,8 @@ -@Html.Raw(ViewData["SpaHtml"]) +<!-- Remove this if you want to remove Server-side rendering --> +@Html.Raw(ViewData["SpaHtml"]) + +<!-- if you only want Client-side rendering uncomment this --> +<!-- <app-root></app-root> --> <script src="http://github.com/~/dist/vendor.js" asp-append-version="true"></script> @section scripts { diff --git a/package.json b/package.json index 9ebbf620..b86fbe7f 100644 --- a/package.json +++ b/package.json @@ -11,24 +11,25 @@ "build:dev": "npm run build:vendor && npm run build:webpack", "build:webpack": "webpack --progress --color", "build:prod": "npm run clean && npm run build:vendor -- --env.prod && npm run build:webpack -- --env.prod", + "build:p": "npm run build:webpack -- --env.prod", "build:vendor": "webpack --config webpack.config.vendor.js --progress --color", "clean": "rimraf wwwroot/dist clientapp/dist" }, "dependencies": { - "@angular/animations": "^4.3.0", - "@angular/common": "^4.3.0", - "@angular/compiler": "^4.3.0", - "@angular/compiler-cli": "^4.3.0", - "@angular/core": "^4.3.0", - "@angular/forms": "^4.3.0", - "@angular/http": "^4.3.0", - "@angular/platform-browser": "^4.3.0", - "@angular/platform-browser-dynamic": "^4.3.0", - "@angular/platform-server": "^4.3.0", - "@angular/router": "^4.3.0", - "@nguniversal/aspnetcore-engine": "^1.0.0-beta.2", - "@ngx-translate/core": "^6.0.1", - "@ngx-translate/http-loader": "0.0.3", + "@angular/animations": "^5.0.0", + "@angular/common": "^5.0.0", + "@angular/compiler": "^5.0.0", + "@angular/core": "^5.0.0", + "@angular/forms": "^5.0.0", + "@angular/http": "^5.0.0", + "@angular/platform-browser": "^5.0.0", + "@angular/platform-browser-dynamic": "^5.0.0", + "@angular/platform-server": "^5.0.0", + "@angular/router": "^5.0.0", + "@nguniversal/aspnetcore-engine": "^5.0.0-beta.5", + "@nguniversal/common": "^5.0.0-beta.5", + "@ngx-translate/core": "^8.0.0", + "@ngx-translate/http-loader": "^2.0.0", "@types/node": "^7.0.12", "angular2-router-loader": "^0.3.5", "angular2-template-loader": "^0.6.2", @@ -49,16 +50,16 @@ "jquery": "^2.2.1", "json-loader": "^0.5.4", "moment": "2.18.1", - "ngx-bootstrap": "2.0.0-beta.3", + "ngx-bootstrap": "2.0.0-beta.6", "node-sass": "^4.5.2", "preboot": "^5.0.0", "raw-loader": "^0.5.1", "rimraf": "^2.6.2", - "rxjs": "^5.4.3", + "rxjs": "^5.5.6", "sass-loader": "^6.0.6", "style-loader": "^0.18.2", "to-string-loader": "^1.1.5", - "typescript": "2.5.2", + "typescript": "~2.5.0", "url-loader": "^0.5.7", "webpack": "^3.6.0", "webpack-hot-middleware": "^2.19.1", @@ -66,8 +67,9 @@ "zone.js": "^0.8.17" }, "devDependencies": { - "@angular/cli": "^1.3.2", - "@ngtools/webpack": "^1.3.0", + "@angular/cli": "^1.7.0-beta.1", + "@angular/compiler-cli": "^5.2.1", + "@ngtools/webpack": "^1.9.0", "@types/chai": "^3.4.34", "@types/jasmine": "^2.5.37", "chai": "^3.5.0", diff --git a/tslint.json b/tslint.json index 657d49b6..f318a0b8 100644 --- a/tslint.json +++ b/tslint.json @@ -1,4 +1,5 @@ { + "defaultSeverity": "warn", "rules": { "align": false, "ban": false, @@ -75,7 +76,7 @@ "no-switch-case-fall-through": true, "no-trailing-whitespace": false, "no-unused-expression": true, - "no-unused-variable": false, + "no-unused-variable": true, "no-use-before-declare": true, "no-var-keyword": true, "no-var-requires": false, diff --git a/webpack.config.js b/webpack.config.js index 02ff0a01..02ee879f 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -11,7 +11,7 @@ const path = require('path'); const webpack = require('webpack'); const merge = require('webpack-merge'); -const AotPlugin = require('@ngtools/webpack').AotPlugin; +const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; @@ -59,36 +59,60 @@ module.exports = (env) => { ] : [ // new BundleAnalyzerPlugin(), // Plugins that apply in production builds only - new webpack.optimize.UglifyJsPlugin(), - new AotPlugin({ - tsConfigPath: './tsconfig.json', - entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), - exclude: ['./**/*.server.ts'] - }) - ]) + new AngularCompilerPlugin({ + mainPath: path.join(__dirname, 'ClientApp/boot.browser.ts'), + tsConfigPath: './tsconfig.json', + entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), + exclude: ['./**/*.server.ts'] + }), + // new webpack.optimize.UglifyJsPlugin({ + // compress: false, + // mangle: false + // }) + ]), + devtool: isDevBuild ? 'cheap-eval-source-map' : false, + node: { + fs: "empty" + } }); // Configuration for server-side (prerendering) bundle suitable for running in Node const serverBundleConfig = merge(sharedConfig, { // resolve: { mainFields: ['main'] }, - entry: { 'main-server': './ClientApp/boot.server.ts' }, + entry: { + 'main-server': + isDevBuild ? './ClientApp/boot.server.ts' : './ClientApp/boot.server.PRODUCTION.ts' + }, plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require('./ClientApp/dist/vendor-manifest.json'), sourceType: 'commonjs2', name: './vendor' - }) - ].concat(isDevBuild ? [] : [ - new webpack.optimize.UglifyJsPlugin({ - compress: false, - mangle: false }), + new webpack.ContextReplacementPlugin( + // fixes WARNING Critical dependency: the request of a dependency is an expression + /(.+)?angular(\\|\/)core(.+)?/, + path.join(__dirname, 'src'), // location of your src + {} // a map of your routes + ), + new webpack.ContextReplacementPlugin( + // fixes WARNING Critical dependency: the request of a dependency is an expression + /(.+)?express(\\|\/)(.+)?/, + path.join(__dirname, 'src'), + {} + ) + ].concat(isDevBuild ? [] : [ + // new webpack.optimize.UglifyJsPlugin({ + // compress: false, + // mangle: false + // }), // Plugins that apply in production builds only - new AotPlugin({ - tsConfigPath: './tsconfig.json', - entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'), - exclude: ['./**/*.browser.ts'] + new AngularCompilerPlugin({ + mainPath: path.join(__dirname, 'ClientApp/boot.server.PRODUCTION.ts'), + tsConfigPath: './tsconfig.json', + entryModule: path.join(__dirname, 'ClientApp/app/app.module.server#AppModule'), + exclude: ['./**/*.browser.ts'] }) ]), output: { @@ -96,7 +120,8 @@ module.exports = (env) => { path: path.join(__dirname, './ClientApp/dist') }, target: 'node', - devtool: isDevBuild ? 'inline-source-map': false + // switch to "inline-source-map" if you want to debug the TS during SSR + devtool: isDevBuild ? 'cheap-eval-source-map' : false }); return [clientBundleConfig, serverBundleConfig]; diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js index 3e07b78b..6d35d3ba 100644 --- a/webpack.config.vendor.js +++ b/webpack.config.vendor.js @@ -46,7 +46,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.ContextReplacementPlugin(/(.+)?angular(\\|\/)core(.+)?/, 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 ] }; From 7252a4524bd83a3256883607bbc090cb2280b599 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Tue, 23 Jan 2018 15:24:42 -0500 Subject: [PATCH 084/142] docs(readme): gotchas --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9dd99499..8cffbc2f 100644 --- a/README.md +++ b/README.md @@ -339,6 +339,10 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct > When building components in Angular 5 there are a few things to keep in mind. + - Make sure you provide Absolute URLs when calling any APIs. (The server can't understand relative paths, so `/api/whatever` will fail). + + - API calls will be ran during a server, and once again during the client render, so make sure you're using transfering data that's important to you so that you don't see a flicker. + - **`window`**, **`document`**, **`navigator`**, and other browser types - _do not exist on the server_ - so using them, or any library that uses them (jQuery for example) will not work. You do have some options, if you truly need some of this functionality: - If you need to use them, consider limiting them to only your client and wrapping them situationally. You can use the Object injected using the PLATFORM_ID token to check whether the current platform is browser or server. @@ -444,7 +448,7 @@ Nothing's ever perfect, but please let me know by creating an issue (make sure t [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](/LICENSE) -Copyright (c) 2016-2017 [Mark Pieszak](https://github.com/MarkPieszak) +Copyright (c) 2016-2018 [Mark Pieszak](https://github.com/MarkPieszak) ---- From 45d6eaaa1063af25255a7d633524ed06f965471f Mon Sep 17 00:00:00 2001 From: Luiz Machado <machado@odahcam.com> Date: Thu, 25 Jan 2018 13:31:17 -0200 Subject: [PATCH 085/142] fix(webpack): moved context replacement plugin to dev builds only (#545) closes #543 --- webpack.config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 02ee879f..6c348162 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -89,7 +89,8 @@ module.exports = (env) => { manifest: require('./ClientApp/dist/vendor-manifest.json'), sourceType: 'commonjs2', name: './vendor' - }), + }) + ].concat(isDevBuild ? [ new webpack.ContextReplacementPlugin( // fixes WARNING Critical dependency: the request of a dependency is an expression /(.+)?angular(\\|\/)core(.+)?/, @@ -102,7 +103,7 @@ module.exports = (env) => { path.join(__dirname, 'src'), {} ) - ].concat(isDevBuild ? [] : [ + ] : [ // new webpack.optimize.UglifyJsPlugin({ // compress: false, // mangle: false From b7dd580acff001996ded91a13a29aa44a34eb2f0 Mon Sep 17 00:00:00 2001 From: Jan Sviland <jan@sviland.org> Date: Thu, 25 Jan 2018 16:31:52 +0100 Subject: [PATCH 086/142] browser caching (#542) closes #541 --- Startup.cs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/Startup.cs b/Startup.cs index b7773ac0..db1d5d8d 100644 --- a/Startup.cs +++ b/Startup.cs @@ -9,6 +9,8 @@ using Microsoft.EntityFrameworkCore; using AspCoreServer.Data; using Swashbuckle.AspNetCore.Swagger; +using Microsoft.AspNetCore.Http; +using Microsoft.Net.Http.Headers; namespace AspCoreServer { @@ -64,7 +66,30 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); - app.UseStaticFiles(); + // app.UseStaticFiles(); + + app.UseStaticFiles(new StaticFileOptions() + { + OnPrepareResponse = c => + { + //Do not add cache to json files. We need to have new versions when we add new translations. + + if (!c.Context.Request.Path.Value.Contains(".json")) + { + c.Context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromDays(30) // Cache everything except json for 30 days + }; + } + else + { + c.Context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromMinutes(15) // Cache json for 15 minutes + }; + } + } + }); DbInitializer.Initialize(context); From c8f3a1e5606f2e5115adf2b4dc6fc62d6b625380 Mon Sep 17 00:00:00 2001 From: Eugen Tatuev <eugentatuev@gmail.com> Date: Tue, 30 Jan 2018 08:33:03 +0300 Subject: [PATCH 087/142] fix(webpack): enable uglifyjs for non-vendor bundles --- webpack.config.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 6c348162..a54d3135 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,10 +65,11 @@ module.exports = (env) => { entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), exclude: ['./**/*.server.ts'] }), - // new webpack.optimize.UglifyJsPlugin({ - // compress: false, - // mangle: false - // }) + new webpack.optimize.UglifyJsPlugin({ + output: { + ascii_only: true, + } + }), ]), devtool: isDevBuild ? 'cheap-eval-source-map' : false, node: { @@ -104,10 +105,13 @@ module.exports = (env) => { {} ) ] : [ - // new webpack.optimize.UglifyJsPlugin({ - // compress: false, - // mangle: false - // }), + new webpack.optimize.UglifyJsPlugin({ + mangle: false, + compress: false, + output: { + ascii_only: true, + } + }), // Plugins that apply in production builds only new AngularCompilerPlugin({ mainPath: path.join(__dirname, 'ClientApp/boot.server.PRODUCTION.ts'), From 0ad0f60766bc08f3a1e2f36fc2944db874b44b9d Mon Sep 17 00:00:00 2001 From: Isaac Levin <isaac.r.levin@gmail.com> Date: Mon, 5 Feb 2018 12:25:45 -0500 Subject: [PATCH 088/142] Update to REST Api to better handle PUT/DELETE --- .../user-detail/user-detail.component.ts | 7 +- .../app/containers/users/users.component.html | 4 +- .../app/containers/users/users.component.ts | 120 ++++++++++-------- Server/RestAPI/UsersController.cs | 6 +- 4 files changed, 75 insertions(+), 62 deletions(-) diff --git a/ClientApp/app/components/user-detail/user-detail.component.ts b/ClientApp/app/components/user-detail/user-detail.component.ts index d21d3415..75a8369e 100644 --- a/ClientApp/app/components/user-detail/user-detail.component.ts +++ b/ClientApp/app/components/user-detail/user-detail.component.ts @@ -1,4 +1,4 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, Output, EventEmitter } from '@angular/core'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @@ -8,7 +8,7 @@ import { UserService } from '../../shared/user.service'; }) export class UserDetailComponent { @Input() user: IUser; - + @Output() userUpdate: EventEmitter<any> = new EventEmitter(); constructor(private userService: UserService) { } @@ -17,6 +17,7 @@ export class UserDetailComponent { console.log('Put user result: ', result); }, error => { console.log(`There was an issue. ${error._body}.`); - }); + }); + this.userUpdate.emit(null); } } diff --git a/ClientApp/app/containers/users/users.component.html b/ClientApp/app/containers/users/users.component.html index 1e661476..b90c06aa 100644 --- a/ClientApp/app/containers/users/users.component.html +++ b/ClientApp/app/containers/users/users.component.html @@ -1,4 +1,4 @@ -<h1>This is a RestAPI Example (hitting WebAPI in our case)</h1> +<h1>This is a RestAPI Example (hitting WebAPI in our case)</h1> <blockquote> Let's get some fake users from Rest:<br> @@ -26,4 +26,4 @@ <h2>Users</h2> </button> </li> </ul> -<app-user-detail [user]="selectedUser"></app-user-detail> +<app-user-detail (userUpdate)="onUserUpdate($event)" [user]="selectedUser"></app-user-detail> diff --git a/ClientApp/app/containers/users/users.component.ts b/ClientApp/app/containers/users/users.component.ts index 35cc8517..6bca6a73 100644 --- a/ClientApp/app/containers/users/users.component.ts +++ b/ClientApp/app/containers/users/users.component.ts @@ -1,69 +1,81 @@ -import { - Component, OnInit, - // animation imports - trigger, state, style, transition, animate, Inject +import { + Component, OnInit, + // animation imports + trigger, state, style, transition, animate, Inject } from '@angular/core'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; @Component({ - selector: 'app-users', - templateUrl: './users.component.html', - styleUrls: ['./users.component.css'], - animations: [ - // Animation example - // Triggered in the ngFor with [@flyInOut] - trigger('flyInOut', [ - state('in', style({ transform: 'translateY(0)' })), - transition('void => *', [ - style({ transform: 'translateY(-100%)' }), - animate(1000) - ]), - transition('* => void', [ - animate(1000, style({ transform: 'translateY(100%)' })) - ]) - ]) - ] + selector: 'app-users', + templateUrl: './users.component.html', + styleUrls: ['./users.component.css'], + animations: [ + // Animation example + // Triggered in the ngFor with [@flyInOut] + trigger('flyInOut', [ + state('in', style({ transform: 'translateY(0)' })), + transition('void => *', [ + style({ transform: 'translateY(-100%)' }), + animate(1000) + ]), + transition('* => void', [ + animate(1000, style({ transform: 'translateY(100%)' })) + ]) + ]) + ] }) export class UsersComponent implements OnInit { - users: IUser[]; - selectedUser: IUser; + users: IUser[]; + selectedUser: IUser; - // Use "constructor"s only for dependency injection - constructor( - private userService: UserService - ) { } + // Use "constructor"s only for dependency injection + constructor( + private userService: UserService + ) { } - // Here you want to handle anything with @Input()'s @Output()'s - // Data retrieval / etc - this is when the Component is "ready" and wired up - ngOnInit() { - this.userService.getUsers().subscribe(result => { - console.log('HttpClient [GET] /api/users/allresult', result); - this.users = result; - }); - } + // Here you want to handle anything with @Input()'s @Output()'s + // Data retrieval / etc - this is when the Component is "ready" and wired up + ngOnInit() { + this.userService.getUsers().subscribe(result => { + console.log('HttpClient [GET] /api/users/allresult', result); + this.users = result; + }); + } - onSelect(user: IUser): void { - this.selectedUser = user; - } + onSelect(user: IUser): void { + this.selectedUser = user; + } - deleteUser(user) { - this.userService.deleteUser(user).subscribe(result => { - console.log('Delete user result: ', result); - let position = this.users.indexOf(user); - this.users.splice(position, 1); - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); - } + deleteUser(user) { + this.clearUser(); + this.userService.deleteUser(user).subscribe(result => { + console.log('Delete user result: ', result); + let position = this.users.indexOf(user); + this.users.splice(position, 1); + }, error => { + console.log(`There was an issue. ${error._body}.`); + }); + } + + onUserUpdate(event) { + this.clearUser(); + } + + addUser(newUserName) { + this.clearUser(); + this.userService.addUser(newUserName).subscribe(result => { + console.log('Post user result: ', result); + this.users.push(result); + }, error => { + console.log(`There was an issue. ${error._body}.`); + }); + } - addUser(newUserName) { - this.userService.addUser(newUserName).subscribe(result => { - console.log('Post user result: ', result); - this.users.push(result); - }, error => { - console.log(`There was an issue. ${error._body}.`); - }); + clearUser() { + if (this.selectedUser) { + this.selectedUser = null; } + } } diff --git a/Server/RestAPI/UsersController.cs b/Server/RestAPI/UsersController.cs index 2938eb8e..3a7fe3d2 100644 --- a/Server/RestAPI/UsersController.cs +++ b/Server/RestAPI/UsersController.cs @@ -1,4 +1,4 @@ -using AspCoreServer.Data; +using AspCoreServer.Data; using AspCoreServer.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -89,7 +89,7 @@ public async Task<IActionResult> Put(int id, [FromBody]User userUpdateValue) { _context.Update(userUpdateValue); await _context.SaveChangesAsync(); - return Ok("Updated user - " + userUpdateValue.Name); + return Json("Updated user - " + userUpdateValue.Name); } } catch (DbUpdateException) @@ -116,7 +116,7 @@ public async Task<IActionResult> Delete(int id) { _context.User.Remove(userToRemove); await _context.SaveChangesAsync(); - return Ok("Deleted user - " + userToRemove.Name); + return Json("Deleted user - " + userToRemove.Name); } } } From 0f2609c93aaad591d73db99acc7e7957c5051bd8 Mon Sep 17 00:00:00 2001 From: peterdobson <petedobson@gmail.com> Date: Wed, 7 Mar 2018 03:16:04 +1100 Subject: [PATCH 089/142] fix(sitemap): update homecontroller sitemap tags Renamed the "sitemapindex" & "sitemap" SitemapXml tags to "urlset" & "url" to allow Google Search Console to process sitemap.xml without errors closes #562 --- Server/Controllers/HomeController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Server/Controllers/HomeController.cs b/Server/Controllers/HomeController.cs index 0a674a60..bd9ad008 100644 --- a/Server/Controllers/HomeController.cs +++ b/Server/Controllers/HomeController.cs @@ -38,16 +38,16 @@ public async Task<IActionResult> SitemapXml() { String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>"; - xml += "<sitemapindex xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; - xml += "<sitemap>"; + xml += "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9/">"; + xml += "<url>"; xml += "<loc>http://localhost:4251/home</loc>"; xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; - xml += "</sitemap>"; - xml += "<sitemap>"; + xml += "</url>"; + xml += "<url>"; xml += "<loc>http://localhost:4251/counter</loc>"; xml += "<lastmod>" + DateTime.Now.ToString("yyyy-MM-dd") + "</lastmod>"; - xml += "</sitemap>"; - xml += "</sitemapindex>"; + xml += "</url>"; + xml += "</urlset>"; return Content(xml, "text/xml"); From fdef986e352f980bf9091458fd7a1dd2dd8ec0f5 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 22 Mar 2018 15:08:45 -0400 Subject: [PATCH 090/142] fix(nguniversal): pin to beta.5 until new release --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b86fbe7f..d2adcd26 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "@angular/platform-browser-dynamic": "^5.0.0", "@angular/platform-server": "^5.0.0", "@angular/router": "^5.0.0", - "@nguniversal/aspnetcore-engine": "^5.0.0-beta.5", - "@nguniversal/common": "^5.0.0-beta.5", + "@nguniversal/aspnetcore-engine": "5.0.0-beta.5", + "@nguniversal/common": "5.0.0-beta.5", "@ngx-translate/core": "^8.0.0", "@ngx-translate/http-loader": "^2.0.0", "@types/node": "^7.0.12", From dfa802628d895cfbff61ccfb5c4556e50860fb42 Mon Sep 17 00:00:00 2001 From: CaerusKaru <caerus.karu@gmail.com> Date: Fri, 23 Mar 2018 18:40:24 -0400 Subject: [PATCH 091/142] chore: update to latest nguniversal and fix imports (#578) closes #577 --- ClientApp/app/app.component.ts | 2 +- ClientApp/app/app.module.browser.ts | 2 +- ClientApp/app/app.module.ts | 2 +- ClientApp/app/shared/user.service.ts | 2 +- package.json | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ClientApp/app/app.component.ts b/ClientApp/app/app.component.ts index 7a6a75ed..d8b28150 100644 --- a/ClientApp/app/app.component.ts +++ b/ClientApp/app/app.component.ts @@ -7,7 +7,7 @@ import { LinkService } from './shared/link.service'; // i18n support import { TranslateService } from '@ngx-translate/core'; -import { REQUEST } from '@nguniversal/aspnetcore-engine'; +import { REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; @Component({ selector: 'app-root', diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index 56d1c20c..fa983b67 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -3,7 +3,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { APP_BASE_HREF } from '@angular/common'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine'; +import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; import { BrowserTransferStateModule } from '@angular/platform-browser'; diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index d31250c4..5394d67d 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -24,7 +24,7 @@ import { NgxBootstrapComponent } from './containers/ngx-bootstrap-demo/ngx-boots import { LinkService } from './shared/link.service'; import { UserService } from './shared/user.service'; -import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; export function createTranslateLoader(http: HttpClient, baseHref) { // Temporary Azure hack diff --git a/ClientApp/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts index 9489e32f..6a3d7cb7 100644 --- a/ClientApp/app/shared/user.service.ts +++ b/ClientApp/app/shared/user.service.ts @@ -2,7 +2,7 @@ import { HttpClient } from '@angular/common/http'; import { Http, URLSearchParams } from '@angular/http'; import { APP_BASE_HREF } from '@angular/common'; -import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine'; +import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; import { IUser } from '../models/User'; import { Observable } from 'rxjs/Observable'; diff --git a/package.json b/package.json index d2adcd26..86dacd20 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,8 @@ "@angular/platform-browser-dynamic": "^5.0.0", "@angular/platform-server": "^5.0.0", "@angular/router": "^5.0.0", - "@nguniversal/aspnetcore-engine": "5.0.0-beta.5", - "@nguniversal/common": "5.0.0-beta.5", + "@nguniversal/aspnetcore-engine": "^5.0.0-beta.8", + "@nguniversal/common": "^5.0.0-beta.8", "@ngx-translate/core": "^8.0.0", "@ngx-translate/http-loader": "^2.0.0", "@types/node": "^7.0.12", From 3a8bfe242a31caba034f55162d9f1be972f406ab Mon Sep 17 00:00:00 2001 From: Rune Antonsen <rune.antonsen@gmail.com> Date: Tue, 27 Mar 2018 14:21:40 +0200 Subject: [PATCH 092/142] fix(polyfills): move reflect-metadata to polyfills.ts (#581) No need to have it in both browser.polyfills.ts and server.polyfills.ts when it's the same. Closes #580 --- ClientApp/polyfills/browser.polyfills.ts | 1 - ClientApp/polyfills/polyfills.ts | 3 +++ ClientApp/polyfills/server.polyfills.ts | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ClientApp/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts index fa02ffab..dd813a13 100644 --- a/ClientApp/polyfills/browser.polyfills.ts +++ b/ClientApp/polyfills/browser.polyfills.ts @@ -1,4 +1,3 @@ import './polyfills.ts'; import 'zone.js/dist/zone'; -import 'reflect-metadata'; diff --git a/ClientApp/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts index fe3a6bd6..39ccb969 100644 --- a/ClientApp/polyfills/polyfills.ts +++ b/ClientApp/polyfills/polyfills.ts @@ -18,6 +18,9 @@ import 'core-js/es6/map'; import 'core-js/es6/set'; + /** */ + import 'reflect-metadata'; + /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. diff --git a/ClientApp/polyfills/server.polyfills.ts b/ClientApp/polyfills/server.polyfills.ts index 7d3dc3bf..7044895f 100644 --- a/ClientApp/polyfills/server.polyfills.ts +++ b/ClientApp/polyfills/server.polyfills.ts @@ -1,4 +1,3 @@ import './polyfills.ts'; -import 'reflect-metadata'; import 'zone.js'; From 3269cf0ef6bd4b6aad3357cbdb472b1ca07dc4f0 Mon Sep 17 00:00:00 2001 From: Rune Antonsen <rune.antonsen@gmail.com> Date: Tue, 27 Mar 2018 14:46:57 +0200 Subject: [PATCH 093/142] fix(polyfills): web-animations-js should be in browser.polyfills.ts (#582) --- ClientApp/polyfills/browser.polyfills.ts | 2 ++ ClientApp/polyfills/polyfills.ts | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ClientApp/polyfills/browser.polyfills.ts b/ClientApp/polyfills/browser.polyfills.ts index dd813a13..1db98ac8 100644 --- a/ClientApp/polyfills/browser.polyfills.ts +++ b/ClientApp/polyfills/browser.polyfills.ts @@ -1,3 +1,5 @@ import './polyfills.ts'; import 'zone.js/dist/zone'; +import 'reflect-metadata'; +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. diff --git a/ClientApp/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts index 39ccb969..4382a0e1 100644 --- a/ClientApp/polyfills/polyfills.ts +++ b/ClientApp/polyfills/polyfills.ts @@ -24,8 +24,6 @@ /** IE10 and IE11 requires the following for NgClass support on SVG elements */ // import 'classlist.js'; // Run `npm install --save classlist.js`. -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - /** Evergreen browsers require these. **/ import 'core-js/es6/reflect'; import 'core-js/es7/reflect'; From 82afbb5781165081e6167e4679a89ed1caeba8b6 Mon Sep 17 00:00:00 2001 From: Flood <linusflood@gmail.com> Date: Fri, 6 Apr 2018 15:25:58 +0200 Subject: [PATCH 094/142] fix(cache): add cache busting to js files This fix makes the cache busting work. When using asp-append-version="true" in Index.cshtml like this: <script src="http://github.com/~/dist/vendor.js" asp-append-version="true"></script> It will turn that into: <script src="http://github.com/~/dist/vendor.js?v=37182361827361" ></script> When you publish. --- Views/_ViewImports.cshtml | 1 + 1 file changed, 1 insertion(+) diff --git a/Views/_ViewImports.cshtml b/Views/_ViewImports.cshtml index 8629c125..1872fd6b 100644 --- a/Views/_ViewImports.cshtml +++ b/Views/_ViewImports.cshtml @@ -1,2 +1,3 @@ @using AspCoreServer @addTagHelper "*, Microsoft.AspNetCore.SpaServices" +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" From a019c9b9f01c22e66e1b75538bb6f39991ad698c Mon Sep 17 00:00:00 2001 From: Bill <OceansideBill@users.noreply.github.com> Date: Sat, 19 May 2018 09:56:10 -0700 Subject: [PATCH 095/142] feat(rxjs6): added modifications to support updates to ngx-bootstrap and get translation functioning with RXJS 6. --- ClientApp/app/app.component.ts | 20 +++++---- ClientApp/app/app.module.ts | 4 +- ClientApp/app/shared/user.service.ts | 4 +- ClientApp/polyfills/polyfills.ts | 9 +--- ClientApp/polyfills/rx-imports.ts | 30 ------------- package.json | 67 ++++++++++++++-------------- 6 files changed, 50 insertions(+), 84 deletions(-) delete mode 100644 ClientApp/polyfills/rx-imports.ts diff --git a/ClientApp/app/app.component.ts b/ClientApp/app/app.component.ts index d8b28150..614cba3e 100644 --- a/ClientApp/app/app.component.ts +++ b/ClientApp/app/app.component.ts @@ -1,7 +1,9 @@ -import { Component, OnInit, OnDestroy, Inject, ViewEncapsulation, RendererFactory2, PLATFORM_ID, Injector } from '@angular/core'; + +import {mergeMap, map, filter} from 'rxjs/operators'; +import { Component, OnInit, OnDestroy, Inject, ViewEncapsulation, RendererFactory2, PLATFORM_ID, Injector } from '@angular/core'; import { Router, NavigationEnd, ActivatedRoute, PRIMARY_OUTLET } from '@angular/router'; import { Meta, Title, DOCUMENT, MetaDefinition } from '@angular/platform-browser'; -import { Subscription } from 'rxjs/Subscription'; +import { Subscription } from 'rxjs'; import { isPlatformServer } from '@angular/common'; import { LinkService } from './shared/link.service'; @@ -60,15 +62,15 @@ export class AppComponent implements OnInit, OnDestroy { private _changeTitleOnNavigation() { - this.routerSub$ = this.router.events - .filter(event => event instanceof NavigationEnd) - .map(() => this.activatedRoute) - .map(route => { + this.routerSub$ = this.router.events.pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.activatedRoute), + map(route => { while (route.firstChild) route = route.firstChild; return route; - }) - .filter(route => route.outlet === 'primary') - .mergeMap(route => route.data) + }), + filter(route => route.outlet === 'primary'), + mergeMap(route => route.data),) .subscribe((event) => { this._setMetaAndLinks(event); }); diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index 5394d67d..c0fa0985 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -7,7 +7,7 @@ import { FormsModule } from '@angular/forms'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; import { TransferHttpCacheModule } from '@nguniversal/common'; -import { Ng2BootstrapModule } from 'ngx-bootstrap'; +import { AccordionModule } from 'ngx-bootstrap'; // i18n support import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; @@ -57,7 +57,7 @@ export function createTranslateLoader(http: HttpClient, baseHref) { FormsModule, - Ng2BootstrapModule.forRoot(), // You could also split this up if you don't want the Entire Module imported + AccordionModule.forRoot(), // You could also split this up if you don't want the Entire Module imported // i18n support TranslateModule.forRoot({ diff --git a/ClientApp/app/shared/user.service.ts b/ClientApp/app/shared/user.service.ts index 6a3d7cb7..31110e7e 100644 --- a/ClientApp/app/shared/user.service.ts +++ b/ClientApp/app/shared/user.service.ts @@ -1,10 +1,10 @@ -import { Injectable, Inject, Injector } from '@angular/core'; +import { Injectable, Inject, Injector } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Http, URLSearchParams } from '@angular/http'; import { APP_BASE_HREF } from '@angular/common'; import { ORIGIN_URL } from '@nguniversal/aspnetcore-engine/tokens'; import { IUser } from '../models/User'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; @Injectable() diff --git a/ClientApp/polyfills/polyfills.ts b/ClientApp/polyfills/polyfills.ts index 4382a0e1..fd43e294 100644 --- a/ClientApp/polyfills/polyfills.ts +++ b/ClientApp/polyfills/polyfills.ts @@ -1,4 +1,4 @@ - + /*************************************************************************************************** * BROWSER POLYFILLS */ @@ -28,10 +28,3 @@ import 'core-js/es6/reflect'; import 'core-js/es7/reflect'; -/** - * Date, currency, decimal and percent pipes. - * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 - */ -// import 'intl'; // Run `npm install --save intl`. - -import './rx-imports'; diff --git a/ClientApp/polyfills/rx-imports.ts b/ClientApp/polyfills/rx-imports.ts deleted file mode 100644 index 506f102b..00000000 --- a/ClientApp/polyfills/rx-imports.ts +++ /dev/null @@ -1,30 +0,0 @@ - -/* -=- RxJs imports -=- - * - * Here you can place any RxJs imports so you don't have to constantly - * import them throughout your App :) - * - * This file is automatically imported into `polyfills.ts` (which is imported into browser/server modules) - */ - -// General Operators -import 'rxjs/add/operator/map'; -import 'rxjs/add/operator/do'; -import 'rxjs/add/operator/throttleTime'; -import 'rxjs/add/operator/distinctUntilChanged'; -import 'rxjs/add/operator/switchMap'; -import 'rxjs/add/operator/take'; -import 'rxjs/add/operator/debounceTime'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/mergeMap'; -import 'rxjs/add/operator/concat'; -import 'rxjs/add/operator/catch'; -import 'rxjs/add/operator/first'; - -// Observable operators -import 'rxjs/add/observable/fromEvent'; -import 'rxjs/add/observable/interval'; -import 'rxjs/add/observable/fromPromise'; -import 'rxjs/add/observable/of'; -import 'rxjs/add/observable/concat'; - diff --git a/package.json b/package.json index 86dacd20..848a12a2 100644 --- a/package.json +++ b/package.json @@ -28,65 +28,66 @@ "@angular/router": "^5.0.0", "@nguniversal/aspnetcore-engine": "^5.0.0-beta.8", "@nguniversal/common": "^5.0.0-beta.8", - "@ngx-translate/core": "^8.0.0", - "@ngx-translate/http-loader": "^2.0.0", + "@ngx-translate/core": "^9.1.1", + "@ngx-translate/http-loader": "^2.0.1", "@types/node": "^7.0.12", "angular2-router-loader": "^0.3.5", "angular2-template-loader": "^0.6.2", "aspnet-prerendering": "^3.0.1", - "aspnet-webpack": "^2.0.1", - "awesome-typescript-loader": "^3.0.0", - "bootstrap": "^3.3.7", + "aspnet-webpack": "^2.0.3", + "awesome-typescript-loader": "^3.2.3", + "bootstrap": "^4.1.1", "bootstrap-sass": "^3.3.7", - "core-js": "^2.5.1", - "css": "^2.2.1", - "css-loader": "^0.28.7", - "event-source-polyfill": "^0.0.9", - "expose-loader": "^0.7.3", - "extract-text-webpack-plugin": "^3.0.0", - "file-loader": "^0.11.2", - "html-loader": "^0.5.1", + "core-js": "^2.5.6", + "css": "^2.2.3", + "css-loader": "^0.28.11", + "event-source-polyfill": "^0.0.12", + "expose-loader": "^0.7.5", + "extract-text-webpack-plugin": "^3.0.2", + "file-loader": "^1.1.11", + "html-loader": "^0.5.5", "isomorphic-fetch": "^2.2.1", - "jquery": "^2.2.1", - "json-loader": "^0.5.4", + "jquery": "^3.3.1", + "json-loader": "^0.5.7", "moment": "2.18.1", - "ngx-bootstrap": "2.0.0-beta.6", - "node-sass": "^4.5.2", + "ngx-bootstrap": "2.0.5", + "node-sass": "^4.9.0", "preboot": "^5.0.0", "raw-loader": "^0.5.1", "rimraf": "^2.6.2", - "rxjs": "^5.5.6", + "rxjs": "^6.1.0", + "rxjs-compat": "^6.1.0", "sass-loader": "^6.0.6", "style-loader": "^0.18.2", "to-string-loader": "^1.1.5", - "typescript": "~2.5.0", - "url-loader": "^0.5.7", + "typescript": "~2.7.2", + "url-loader": "^1.0.1", "webpack": "^3.6.0", - "webpack-hot-middleware": "^2.19.1", + "webpack-hot-middleware": "^2.22.1", "webpack-merge": "^4.1.0", - "zone.js": "^0.8.17" + "zone.js": "^0.8.26" }, "devDependencies": { "@angular/cli": "^1.7.0-beta.1", "@angular/compiler-cli": "^5.2.1", "@ngtools/webpack": "^1.9.0", - "@types/chai": "^3.4.34", - "@types/jasmine": "^2.5.37", - "chai": "^3.5.0", - "codelyzer": "^3.0.0", - "istanbul-instrumenter-loader": "^3.0.0", + "@types/chai": "^4.1.3", + "@types/jasmine": "^2.8.7", + "chai": "^4.1.2", + "codelyzer": "^3.1.2", + "istanbul-instrumenter-loader": "^3.0.1", "jasmine-core": "^2.5.2", "karma": "^1.7.1", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", "karma-coverage": "^1.1.1", - "karma-jasmine": "^1.1.0", - "karma-mocha-reporter": "^2.2.4", + "karma-jasmine": "^1.1.2", + "karma-mocha-reporter": "^2.2.5", "karma-phantomjs-launcher": "^1.0.4", - "karma-remap-coverage": "^0.1.4", + "karma-remap-coverage": "^0.1.5", "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^2.0.3", - "tslint": "^5.7.0", - "webpack-bundle-analyzer": "^2.9.0" + "karma-webpack": "^2.0.4", + "tslint": "^5.10.0", + "webpack-bundle-analyzer": "^2.12.0" } } From 0360ad5a8ef1830dd88d6c76aceae4a0f1879bf3 Mon Sep 17 00:00:00 2001 From: Robert Zeni <robert.zeni@mohawkcollege.ca> Date: Mon, 28 May 2018 13:40:45 -0400 Subject: [PATCH 096/142] feat(angular6): upgrade .net core 2.1, bootstrap 4, ngx-bootsrap 3, rxjs 6, etc closes #619 --- .vscode/launch.json | 6 +- Asp2017.csproj | 12 ++-- Asp2017.sln | 25 ++++++++ ClientApp/app/app.module.browser.ts | 4 +- ClientApp/app/app.module.server.ts | 4 +- .../app/containers/home/home.component.html | 2 +- .../app/containers/users/users.component.ts | 11 +++- README.md | 8 +-- package.json | 64 ++++++++++--------- webpack.config.js | 47 ++++++++++---- webpack.config.vendor.js | 40 ++++++++++-- 11 files changed, 152 insertions(+), 71 deletions(-) create mode 100644 Asp2017.sln diff --git a/.vscode/launch.json b/.vscode/launch.json index 08d4edf9..c26ab807 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -20,7 +20,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, @@ -51,7 +51,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, @@ -82,7 +82,7 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - "program": "${workspaceRoot}/bin/Debug/netcoreapp2.0/Asp2017.dll", + "program": "${workspaceRoot}/bin/Debug/netcoreapp2.1/Asp2017.dll", "args": [], "cwd": "${workspaceRoot}", "stopAtEntry": false, diff --git a/Asp2017.csproj b/Asp2017.csproj index efb619c0..fe12d77e 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -1,17 +1,17 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> - <TargetFramework>netcoreapp2.0</TargetFramework> + <TargetFramework>netcoreapp2.1</TargetFramework> <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked> <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <!-- New Meta Package has SpaServices in It --> - <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> - <PackageReference Include="NETStandard.Library" Version="2.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.0.0" /> - <PackageReference Include="Swashbuckle.AspNetCore" Version="1.0.0" /> + <PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.0-rc1-final" /> + <PackageReference Include="NETStandard.Library" Version="2.0.3" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0-rc1-final" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-rc1-final" /> + <PackageReference Include="Swashbuckle.AspNetCore" Version="2.4.0" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" /> diff --git a/Asp2017.sln b/Asp2017.sln new file mode 100644 index 00000000..56a99822 --- /dev/null +++ b/Asp2017.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27703.2018 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Asp2017", "Asp2017.csproj", "{BC28E9F7-E6EC-447D-AABD-17683BEAD625}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC28E9F7-E6EC-447D-AABD-17683BEAD625}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DE341460-9041-458F-99D5-43FC7572CFA6} + EndGlobalSection +EndGlobal diff --git a/ClientApp/app/app.module.browser.ts b/ClientApp/app/app.module.browser.ts index fa983b67..f7371e80 100644 --- a/ClientApp/app/app.module.browser.ts +++ b/ClientApp/app/app.module.browser.ts @@ -7,7 +7,7 @@ import { ORIGIN_URL, REQUEST } from '@nguniversal/aspnetcore-engine/tokens'; import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; import { BrowserTransferStateModule } from '@angular/platform-browser'; -import { BrowserPrebootModule } from 'preboot/browser'; +import { PrebootModule } from 'preboot'; export function getOriginUrl() { return window.location.origin; @@ -21,7 +21,7 @@ export function getRequest() { @NgModule({ bootstrap: [AppComponent], imports: [ - BrowserPrebootModule.replayEvents(), + PrebootModule.withConfig({ appRoot: 'app-root' }), BrowserAnimationsModule, // Our Common AppModule diff --git a/ClientApp/app/app.module.server.ts b/ClientApp/app/app.module.server.ts index 25711851..642a774c 100644 --- a/ClientApp/app/app.module.server.ts +++ b/ClientApp/app/app.module.server.ts @@ -7,7 +7,7 @@ import { AppModuleShared } from './app.module'; import { AppComponent } from './app.component'; import { ServerTransferStateModule } from '@angular/platform-server'; -import { ServerPrebootModule } from 'preboot/server'; +import { PrebootModule } from 'preboot'; @NgModule({ bootstrap: [AppComponent], @@ -16,7 +16,7 @@ import { ServerPrebootModule } from 'preboot/server'; AppModuleShared, ServerModule, - ServerPrebootModule.recordEvents({ appRoot: 'app-root' }), + PrebootModule.withConfig({ appRoot: 'app-root' }), NoopAnimationsModule, // HttpTransferCacheModule still needs fixes for 5.0 diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index 3a4db63b..3c08c588 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -12,7 +12,7 @@ <h2>{{ 'HOME_FEATURE_LIST_TITLE' | translate }} </h2> <ul> <li>ASP.NET Core 2.0 :: ( Visual Studio 2017 )</li> <li> - Angular 5.* front-end UI framework + Angular 6.* front-end UI framework <ul> <li>Angular **platform-server** (aka: Universal) - server-side rendering for SEO, deep-linking, and incredible performance.</li> diff --git a/ClientApp/app/containers/users/users.component.ts b/ClientApp/app/containers/users/users.component.ts index 6bca6a73..f6a0aa2d 100644 --- a/ClientApp/app/containers/users/users.component.ts +++ b/ClientApp/app/containers/users/users.component.ts @@ -1,8 +1,13 @@ import { - Component, OnInit, - // animation imports - trigger, state, style, transition, animate, Inject + Component, OnInit, Inject } from '@angular/core'; +import { + trigger, + state, + style, + animate, + transition +} from '@angular/animations'; import { IUser } from '../../models/User'; import { UserService } from '../../shared/user.service'; diff --git a/README.md b/README.md index 8cffbc2f..d2b1db31 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# ASP.NET Core 2.0 & Angular 5(+) Advanced Starter - with Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.0 & Angular 6(+) Advanced Starter - with Server-side prerendering (for Angular SEO)! ## By [DevHelp.Online](http://www.DevHelp.Online) -> Updated to the latest Angular 5.x +> Updated to the latest Angular 6.x > Note ServerTransferModule still in the works - fix coming soon @@ -81,7 +81,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Codelyzer (for Real-time static code analysis) - VSCode & Atom provide real-time analysis out of the box. -- **ASP.NET Core 2.0** +- **ASP.NET Core 2.1** - Integration with NodeJS to provide pre-rendering, as well as any other Node module asset you want to use. @@ -113,7 +113,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual ### Visual Studio 2017 -Make sure you have .NET Core 2.0 installed and/or VS2017 15.3. +Make sure you have .NET Core 2.1 installed and/or VS2017 15.3. VS2017 will automatically install all the neccessary npm & .NET dependencies when you open the project. Simply push F5 to start debugging ! diff --git a/package.json b/package.json index 848a12a2..9765601e 100644 --- a/package.json +++ b/package.json @@ -16,26 +16,26 @@ "clean": "rimraf wwwroot/dist clientapp/dist" }, "dependencies": { - "@angular/animations": "^5.0.0", - "@angular/common": "^5.0.0", - "@angular/compiler": "^5.0.0", - "@angular/core": "^5.0.0", - "@angular/forms": "^5.0.0", - "@angular/http": "^5.0.0", - "@angular/platform-browser": "^5.0.0", - "@angular/platform-browser-dynamic": "^5.0.0", - "@angular/platform-server": "^5.0.0", - "@angular/router": "^5.0.0", - "@nguniversal/aspnetcore-engine": "^5.0.0-beta.8", - "@nguniversal/common": "^5.0.0-beta.8", - "@ngx-translate/core": "^9.1.1", - "@ngx-translate/http-loader": "^2.0.1", - "@types/node": "^7.0.12", + "@angular/animations": "^6.0.3", + "@angular/common": "^6.0.3", + "@angular/compiler": "^6.0.3", + "@angular/core": "^6.0.3", + "@angular/forms": "^6.0.3", + "@angular/http": "^6.0.3", + "@angular/platform-browser": "^6.0.3", + "@angular/platform-browser-dynamic": "^6.0.3", + "@angular/platform-server": "^6.0.3", + "@angular/router": "^6.0.3", + "@nguniversal/aspnetcore-engine": "^6.0.0", + "@nguniversal/common": "^6.0.0", + "@ngx-translate/core": "^10.0.2", + "@ngx-translate/http-loader": "^3.0.1", + "@types/node": "^10.1.2", "angular2-router-loader": "^0.3.5", "angular2-template-loader": "^0.6.2", "aspnet-prerendering": "^3.0.1", "aspnet-webpack": "^2.0.3", - "awesome-typescript-loader": "^3.2.3", + "awesome-typescript-loader": "^5.0.0", "bootstrap": "^4.1.1", "bootstrap-sass": "^3.3.7", "core-js": "^2.5.6", @@ -43,38 +43,38 @@ "css-loader": "^0.28.11", "event-source-polyfill": "^0.0.12", "expose-loader": "^0.7.5", - "extract-text-webpack-plugin": "^3.0.2", "file-loader": "^1.1.11", "html-loader": "^0.5.5", "isomorphic-fetch": "^2.2.1", "jquery": "^3.3.1", "json-loader": "^0.5.7", - "moment": "2.18.1", - "ngx-bootstrap": "2.0.5", + "moment": "2.22.1", + "ngx-bootstrap": "^3.0.0", "node-sass": "^4.9.0", - "preboot": "^5.0.0", + "preboot": "^6.0.0-beta.4", "raw-loader": "^0.5.1", "rimraf": "^2.6.2", - "rxjs": "^6.1.0", - "rxjs-compat": "^6.1.0", - "sass-loader": "^6.0.6", - "style-loader": "^0.18.2", + "rxjs": "^6.2.0", + "rxjs-compat": "^6.2.0", + "sass-loader": "^7.0.1", + "style-loader": "^0.21.0", "to-string-loader": "^1.1.5", "typescript": "~2.7.2", "url-loader": "^1.0.1", - "webpack": "^3.6.0", - "webpack-hot-middleware": "^2.22.1", - "webpack-merge": "^4.1.0", + "webpack": "^4.9.1", + "webpack-hot-middleware": "^2.22.2", + "webpack-merge": "^4.1.2", "zone.js": "^0.8.26" }, "devDependencies": { - "@angular/cli": "^1.7.0-beta.1", - "@angular/compiler-cli": "^5.2.1", - "@ngtools/webpack": "^1.9.0", + "@angular/cli": "^6.0.5", + "@angular/compiler-cli": "6.0.3", + "@ngtools/webpack": "^6.0.5", "@types/chai": "^4.1.3", "@types/jasmine": "^2.8.7", "chai": "^4.1.2", "codelyzer": "^3.1.2", + "extract-text-webpack-plugin": "^4.0.0-beta.0", "istanbul-instrumenter-loader": "^3.0.1", "jasmine-core": "^2.5.2", "karma": "^1.7.1", @@ -88,6 +88,8 @@ "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^2.0.4", "tslint": "^5.10.0", - "webpack-bundle-analyzer": "^2.12.0" + "uglifyjs-webpack-plugin": "^1.2.5", + "webpack-bundle-analyzer": "^2.13.1", + "webpack-cli": "^2.1.4" } } diff --git a/webpack.config.js b/webpack.config.js index a54d3135..d55cb8de 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,7 @@ module.exports = (env) => { // Configuration in common to both client-side and server-side bundles const isDevBuild = !(env && env.prod); const sharedConfig = { + mode: isDevBuild ? "development" : "production", stats: { modules: false }, context: __dirname, resolve: { extensions: [ '.js', '.ts' ] }, @@ -64,16 +65,26 @@ module.exports = (env) => { tsConfigPath: './tsconfig.json', entryModule: path.join(__dirname, 'ClientApp/app/app.module.browser#AppModule'), exclude: ['./**/*.server.ts'] - }), - new webpack.optimize.UglifyJsPlugin({ - output: { - ascii_only: true, - } - }), + }) ]), devtool: isDevBuild ? 'cheap-eval-source-map' : false, node: { fs: "empty" + }, + optimization: { + minimizer: [].concat(isDevBuild ? [] : [ + // we specify a custom UglifyJsPlugin here to get source maps in production + new UglifyJsPlugin({ + cache: true, + parallel: true, + uglifyOptions: { + compress: false, + ecma: 6, + mangle: true + }, + sourceMap: true + }) + ]) } }); @@ -105,13 +116,6 @@ module.exports = (env) => { {} ) ] : [ - new webpack.optimize.UglifyJsPlugin({ - mangle: false, - compress: false, - output: { - ascii_only: true, - } - }), // Plugins that apply in production builds only new AngularCompilerPlugin({ mainPath: path.join(__dirname, 'ClientApp/boot.server.PRODUCTION.ts'), @@ -126,7 +130,22 @@ module.exports = (env) => { }, target: 'node', // switch to "inline-source-map" if you want to debug the TS during SSR - devtool: isDevBuild ? 'cheap-eval-source-map' : false + devtool: isDevBuild ? 'cheap-eval-source-map' : false, + optimization: { + minimizer: [].concat(isDevBuild ? [] : [ + // we specify a custom UglifyJsPlugin here to get source maps in production + new UglifyJsPlugin({ + cache: true, + parallel: true, + uglifyOptions: { + compress: false, + ecma: 6, + mangle: true + }, + sourceMap: true + }) + ]) + } }); return [clientBundleConfig, serverBundleConfig]; diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js index 6d35d3ba..9a63c583 100644 --- a/webpack.config.vendor.js +++ b/webpack.config.vendor.js @@ -27,10 +27,11 @@ const nonTreeShakableModules = [ const allModules = treeShakableModules.concat(nonTreeShakableModules); module.exports = (env) => { - console.log(`env = ${JSON.stringify(env)}`) + console.log(`env = ${JSON.stringify(env)}`) const extractCSS = new ExtractTextPlugin('vendor.css'); const isDevBuild = !(env && env.prod); const sharedConfig = { + mode: isDevBuild ? "development" : "production", stats: { modules: false }, resolve: { extensions: [ '.js' ] }, module: { @@ -70,8 +71,23 @@ module.exports = (env) => { name: '[name]_[hash]' }) ].concat(isDevBuild ? [] : [ - new webpack.optimize.UglifyJsPlugin() - ]) + + ]), + optimization: { + minimizer: [].concat(isDevBuild ? [] : [ + // we specify a custom UglifyJsPlugin here to get source maps in production + new UglifyJsPlugin({ + cache: true, + parallel: true, + uglifyOptions: { + compress: false, + ecma: 6, + mangle: true + }, + sourceMap: true + }) + ]) + } }); const serverBundleConfig = merge(sharedConfig, { @@ -91,8 +107,22 @@ module.exports = (env) => { name: '[name]_[hash]' }) ].concat(isDevBuild ? [] : [ - new webpack.optimize.UglifyJsPlugin() - ]) + ]), + optimization: { + minimizer: [].concat(isDevBuild ? [] : [ + // we specify a custom UglifyJsPlugin here to get source maps in production + new UglifyJsPlugin({ + cache: true, + parallel: true, + uglifyOptions: { + compress: false, + ecma: 6, + mangle: true + }, + sourceMap: true + }) + ]) + } }); return [clientBundleConfig, serverBundleConfig]; From eff67f28a986be7fb36b16d002649e6ea37622e2 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 31 May 2018 14:56:48 -0400 Subject: [PATCH 097/142] fix(uglify, dotnetcore): update to 2.1, fix uglify issue, prod build fixed closes #632 --- .angular-cli.json | 25 -------------- Asp2017.csproj | 8 ++--- ClientApp/tsconfig.spec.json | 1 + angular.json | 63 ++++++++++++++++++++++++++++++++++++ package.json | 4 ++- webpack.config.js | 1 + webpack.config.vendor.js | 2 ++ 7 files changed, 74 insertions(+), 30 deletions(-) delete mode 100644 .angular-cli.json create mode 100644 angular.json diff --git a/.angular-cli.json b/.angular-cli.json deleted file mode 100644 index 4e8d017d..00000000 --- a/.angular-cli.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "./node_modules/@angular/cli/lib/config/schema.json", - "project": { - "name": "AspnetCore-Angular-Universal" - }, - "apps": [ - { - "root": "ClientApp" - } - ], - "defaults": { - "styleExt": "scss", - "component": { - "spec": false - } - }, - "lint":[ - { - "project": "ClientApp/tsconfig.app.json" - }, - { - "project": "ClientApp/tsconfig.spec.json" - } - ] -} diff --git a/Asp2017.csproj b/Asp2017.csproj index fe12d77e..c8975b88 100644 --- a/Asp2017.csproj +++ b/Asp2017.csproj @@ -7,15 +7,15 @@ </PropertyGroup> <ItemGroup> <!-- New Meta Package has SpaServices in It --> - <PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.0-rc1-final" /> + <PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.0" /> <PackageReference Include="NETStandard.Library" Version="2.0.3" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0-rc1-final" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0-rc1-final" /> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="2.4.0" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" /> - </ItemGroup> + </ItemGroup> <ItemGroup> <!-- Files not to show in IDE --> <None Remove="yarn.lock" /> diff --git a/ClientApp/tsconfig.spec.json b/ClientApp/tsconfig.spec.json index 584cb0a4..de4e2a75 100644 --- a/ClientApp/tsconfig.spec.json +++ b/ClientApp/tsconfig.spec.json @@ -11,6 +11,7 @@ ] }, "files": [ + "polyfills.ts" ], "include": [ "**/*.spec.ts", diff --git a/angular.json b/angular.json new file mode 100644 index 00000000..42179045 --- /dev/null +++ b/angular.json @@ -0,0 +1,63 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "AspnetCore-Angular-Universal": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/", + "index": "ClientApp/index.html", + "main": "ClientApp/main.ts", + "tsConfig": "ClientApp/tsconfig.app.json", + "assets": [], + "styles": [], + "scripts": [] + }, + "configurations": {} + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "AspnetCore-Angular-Universal:build" + }, + "configurations": {} + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "AspnetCore-Angular-Universal:build" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "ClientApp/tsconfig.app.json", + "ClientApp/tsconfig.spec.json" + ], + "exclude": [] + } + } + } + }, + "AspnetCore-Angular-Universal-e2e": { + "root": "", + "sourceRoot": "", + "projectType": "application" + } + }, + "defaultProject": "AspnetCore-Angular-Universal", + "schematics": { + "@schematics/angular:component": { + "spec": false, + "styleext": "scss" + }, + "@schematics/angular:directive": {} + } +} \ No newline at end of file diff --git a/package.json b/package.json index 9765601e..61767b74 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "angular4-aspnetcore-universal", "version": "1.0.0-rc3", "scripts": { + "clean:install": "npm run clean && rimraf ./node_modules ./bin ./obj ./package-lock.json && dotnet restore && npm i", "lint": "tslint -p tsconfig.json", "test": "npm run build:vendor && karma start ClientApp/test/karma.conf.js", "test:watch": "npm run test -- --auto-watch --no-single-run", @@ -90,6 +91,7 @@ "tslint": "^5.10.0", "uglifyjs-webpack-plugin": "^1.2.5", "webpack-bundle-analyzer": "^2.13.1", - "webpack-cli": "^2.1.4" + "webpack-cli": "^2.1.4", + "@angular-devkit/build-angular": "~0.6.6" } } diff --git a/webpack.config.js b/webpack.config.js index d55cb8de..029207ac 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,6 +14,7 @@ const merge = require('webpack-merge'); const AngularCompilerPlugin = require('@ngtools/webpack').AngularCompilerPlugin; const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; +const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const { sharedModuleRules } = require('./webpack.additions'); diff --git a/webpack.config.vendor.js b/webpack.config.vendor.js index 9a63c583..b335a1f9 100644 --- a/webpack.config.vendor.js +++ b/webpack.config.vendor.js @@ -2,6 +2,7 @@ const path = require('path'); const webpack = require('webpack'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const merge = require('webpack-merge'); +const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); const treeShakableModules = [ '@angular/animations', '@angular/common', @@ -24,6 +25,7 @@ const nonTreeShakableModules = [ 'event-source-polyfill', // 'jquery', ]; + const allModules = treeShakableModules.concat(nonTreeShakableModules); module.exports = (env) => { From 2e12109b0fe957154edaf2de6a15614b37597f24 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Thu, 31 May 2018 15:04:21 -0400 Subject: [PATCH 098/142] docs(readme): update verbiage --- README.md | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index d2b1db31..7006242d 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,22 @@ > Updated to the latest Angular 6.x -> Note ServerTransferModule still in the works - fix coming soon - <p align="center"> - <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.0 Angular 5+ Starter" title="ASP.NET Core 2.0 Angular 5+ Starter"> + <img src="http://github.com/docs/architecture.png" alt="ASP.NET Core 2.0 Angular 6+ Starter" title="ASP.NET Core 2.0 Angular 6+ Starter"> </p> -### Harness the power of Angular 5+, ASP.NET Core 2.0, now with SEO ! +### Harness the power of Angular 6+, ASP.NET Core 2.0, now with SEO ! Angular SEO in action: <p align="center"> - <img src="http://github.com/docs/angular2-seo.png" alt="ASP.NET Core Angular5 SEO" title="ASP.NET Core Angular5 SEO"> + <img src="http://github.com/docs/angular2-seo.png" alt="ASP.NET Core Angular6 SEO" title="ASP.NET Core Angular6 SEO"> </p> ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter -for both ASP.NET Core 2.0 using Angular 5.0+, not only for the client-side, but to be rendered on the server for instant +for both ASP.NET Core 2.0 using Angular 6.0+, not only for the client-side, but to be rendered on the server for instant application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). @@ -47,7 +45,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual > These are just some of the features found in this starter! -- ASP.NET 2.0 - VS2017 15.3 support now! +- ASP.NET 2.1 - VS2017 support now! - Azure delpoyment straight from VS2017 - Built in docker support through VS2017 - RestAPI (WebAPI) integration @@ -55,7 +53,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Swagger WebAPI documentation when running in development mode - SignalR Chat demo! (Thanks to [@hakonamatata](https://github.com/hakonamatata)) -- **Angular 5.0.0** : +- **Angular 6.0.0** : - (Minimal) Angular-CLI integration - This is to be used mainly for Generating Components/Services/etc. - Usage examples: @@ -69,7 +67,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - Can be easily replaced with bootstrap4 (3 is provided for browser support) - Bootstrap using SCSS / SASS for easy theming / styling! -- **Webpack build system (Webpack 2)** +- **Webpack build system (Webpack 4)** - HMR : Hot Module Reloading/Replacement - Production builds w/ AoT Compilation @@ -87,7 +85,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual - **Azure** - Microsoft Application Insights setup (for MVC & Web API routing) - - Client-side Angular2 Application Insights integration + - Client-side Angular Application Insights integration - If you're using Azure simply install `npm i -S @markpieszak/ng-application-insights` as a dependencies. - Note: Make sure only the Browser makes these calls ([usage info here](https://github.com/MarkPieszak/angular-application-insights/blob/master/README.md#usage)) - More information here: - https://github.com/MarkPieszak/angular-application-insights @@ -97,9 +95,7 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual instrumentationKey: 'Your-Application-Insights-instrumentationKey' }) ``` - - -> Looking for the older 2.x branch? Go [here](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/old-2.x-universal-branch) + ---- @@ -107,8 +103,8 @@ This utilizes all the latest standards, no gulp, no bower, no typings, no manual # Getting Started? -- **Make sure you have at least Node 6.x or higher (w/ npm 3+) installed!** -- **This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md)** +- **Make sure you have at least Node 8.11.1 or higher (w/ npm 5+) installed!** +- **This repository uses ASP.Net Core 2.1, which has a hard requirement on .NET Core Runtime 2.1 and .NET Core SDK 2.1. Please install these items from [here](https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1/?WT.mc_id=blog-twitter-timheuer)** ### Visual Studio 2017 @@ -143,8 +139,9 @@ export ASPNETCORE_ENVIRONMENT=Development # Upcoming Features: -- Fix HttpTransferCacheModule & ServerTransferModule to work with aspnet-engine -- ~~Update to use npm [ngAspnetCoreEngine](https://github.com/angular/universal/pull/682) (still need to tweak a few things there)~~ +- Clean API / structure / simplify application +- Refactor to latest RxJs pipeable syntax +- Attempt to integrate with Angular-CLI fully ---- @@ -296,7 +293,7 @@ Take a look at the `_Layout.cshtml` file for example, notice how we let .NET han <head> <base href="http://github.com/" /> <!-- Title will be the one you set in your Angular application --> - <title>@ViewData["Title"] - AspNET.Core Angular 5.0.0 (+) starter + @ViewData["Title"] - AspNET.Core Angular 6.0.0 (+) starter @@ -335,9 +332,9 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct # "Gotchas" -- This repository uses ASP.Net Core 2.0, which has a hard requirement on .NET Core Runtime 2.0.0 and .NET Core SDK 2.0.0. Please install these items from [here](https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.0.0-download.md) +- This repository uses ASP.Net Core 2.1, which has a hard requirement on .NET Core Runtime 2.1 and .NET Core SDK 2.1. Please install these items from [here](https://blogs.msdn.microsoft.com/dotnet/2018/05/30/announcing-net-core-2-1/?WT.mc_id=blog-twitter-timheuer) -> When building components in Angular 5 there are a few things to keep in mind. +> When building components in Angular 6 there are a few things to keep in mind. - Make sure you provide Absolute URLs when calling any APIs. (The server can't understand relative paths, so `/api/whatever` will fail). From 8f6356ca35d1164b3c25ce112253e26da5511e39 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 31 May 2018 15:04:55 -0400 Subject: [PATCH 099/142] docs(readme): update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7006242d..bdf713cc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ASP.NET Core 2.0 & Angular 6(+) Advanced Starter - with Server-side prerendering (for Angular SEO)! +# ASP.NET Core 2.1 & Angular 6(+) Advanced Starter - with Server-side prerendering (for Angular SEO)! ## By [DevHelp.Online](http://www.DevHelp.Online) @@ -19,7 +19,7 @@ Angular SEO in action: ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Angular](https://github.com/angular/angular) and is meant to be an advanced starter -for both ASP.NET Core 2.0 using Angular 6.0+, not only for the client-side, but to be rendered on the server for instant +for both ASP.NET Core 2.1 using Angular 6.0+, not only for the client-side, but to be rendered on the server for instant application paints (Note: If you don't need SSR [read here](#faq) on how to disable it). This is meant to be a Feature-Rich Starter application containing all of the latest technologies, best build systems available, and include many real-world examples and libraries needed in todays Single Page Applications (SPAs). From e1b8178d2845e54c584528cd1ea4e235777cd68f Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Thu, 31 May 2018 15:39:02 -0400 Subject: [PATCH 100/142] chore(wording): update 5.0 references in text to 6.0 --- ClientApp/app/containers/home/home.component.html | 6 +++--- Startup.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ClientApp/app/containers/home/home.component.html b/ClientApp/app/containers/home/home.component.html index 3c08c588..7b6799b6 100644 --- a/ClientApp/app/containers/home/home.component.html +++ b/ClientApp/app/containers/home/home.component.html @@ -1,8 +1,8 @@ 

    {{ title }}

    - Enjoy the latest features from .NET Core & Angular 5.0! -
    For more info check the repo here: AspNetCore-Angular2-Universal repo + Enjoy the latest features from .NET Core & Angular 6.x! +
    For more info check the repo here: AspNetCore-Angular-Universal repo

    @@ -10,7 +10,7 @@

    {{ 'HOME_FEATURE_LIST_TITLE' | translate }}

      -
    • ASP.NET Core 2.0 :: ( Visual Studio 2017 )
    • +
    • ASP.NET Core 2.1 :: ( Visual Studio 2017 )
    • Angular 6.* front-end UI framework
        diff --git a/Startup.cs b/Startup.cs index db1d5d8d..4b07eaed 100644 --- a/Startup.cs +++ b/Startup.cs @@ -56,7 +56,7 @@ public void ConfigureServices(IServiceCollection services) // Register the Swagger generator, defining one or more Swagger documents services.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new Info { Title = "Angular 5.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); + c.SwaggerDoc("v1", new Info { Title = "Angular 6.0 Universal & ASP.NET Core advanced starter-kit web API", Version = "v1" }); }); } From 352fab824aa428079f31a6902c1e95444660f7f8 Mon Sep 17 00:00:00 2001 From: GRIMMR3AP3R Date: Thu, 31 May 2018 14:46:41 -0500 Subject: [PATCH 101/142] feat(bootstrap4): add bootstrap4, docker support, fix bootstrap-sass issues * Real Bootstrap 4 * Real Boostrap 4 * Add docker support --- .dockerignore | 335 ++++++++++++++++++ ClientApp/app/_styles.scss | 45 +++ ClientApp/app/_variables.scss | 6 + ClientApp/app/app.component.html | 8 +- ClientApp/app/app.component.scss | 88 ++--- ClientApp/app/app.module.ts | 5 +- .../components/navmenu/navmenu.component.css | 106 ------ .../components/navmenu/navmenu.component.html | 89 +++-- .../components/navmenu/navmenu.component.scss | 114 ++++++ .../components/navmenu/navmenu.component.ts | 2 +- .../user-detail/user-detail.component.html | 25 +- .../user-detail/user-detail.component.scss | 4 + .../user-detail/user-detail.component.ts | 29 +- .../app/containers/home/home.component.html | 4 +- .../ngx-bootstrap.component.html | 2 +- .../app/containers/users/users.component.css | 64 ---- .../app/containers/users/users.component.html | 55 +-- .../app/containers/users/users.component.scss | 101 ++++++ .../app/containers/users/users.component.ts | 8 +- Dockerfile | 24 ++ Server/Data/DbInitializer.cs | 3 +- Views/Shared/_Layout.cshtml | 3 +- package.json | 1 - 23 files changed, 809 insertions(+), 312 deletions(-) create mode 100644 .dockerignore create mode 100644 ClientApp/app/_styles.scss create mode 100644 ClientApp/app/_variables.scss delete mode 100644 ClientApp/app/components/navmenu/navmenu.component.css create mode 100644 ClientApp/app/components/navmenu/navmenu.component.scss create mode 100644 ClientApp/app/components/user-detail/user-detail.component.scss delete mode 100644 ClientApp/app/containers/users/users.component.css create mode 100644 ClientApp/app/containers/users/users.component.scss create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..230e749b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,335 @@ +.dockerignore +.env +.git +.gitignore +.vs +.vscode +*/bin +*/obj +**/.toolstarget + + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ \ No newline at end of file diff --git a/ClientApp/app/_styles.scss b/ClientApp/app/_styles.scss new file mode 100644 index 00000000..42cf15fe --- /dev/null +++ b/ClientApp/app/_styles.scss @@ -0,0 +1,45 @@ +$body-bg: #f1f1f1; +$body-color: #111; +$theme-colors: ( "primary": #216DAD); +$theme-colors: ( "accent": #669ECD); + + +@import "http://github.com/~bootstrap/scss/bootstrap"; +.panel { + box-shadow: 0 3px 1px -2px rgba(0, 0, 0, .2), 0 2px 2px 0 rgba(0, 0, 0, .14), 0 1px 5px 0 rgba(0, 0, 0, .12); + height: 100%; + flex: 1; + background-color: rgba(255, 255, 255, .30); + border-radius: .25rem; + .title { + background-color: #86afd0; + color: #FFFFFF; + text-align: center; + border-top-left-radius: .25rem; + border-top-right-radius: .25rem; + } + .body { + display: flex; + } +} + +@include media-breakpoint-down(md) { + .panel { + .body { + flex-direction: column; + padding: 0px; + } + .title { + font-size: 1.5rem; + } + } +} + +@include media-breakpoint-up(md) { + .panel { + .body { + flex-direction: row; + padding: 1.5rem; + } + } +} diff --git a/ClientApp/app/_variables.scss b/ClientApp/app/_variables.scss new file mode 100644 index 00000000..2d34745a --- /dev/null +++ b/ClientApp/app/_variables.scss @@ -0,0 +1,6 @@ +@import "http://github.com/styles"; +$header-height:50px; +$menu-max-width:25%; +$navbar-default-bg: #312312; +$light-orange: #ff8c00; +$navbar-default-color: $light-orange; diff --git a/ClientApp/app/app.component.html b/ClientApp/app/app.component.html index 0345c682..45ca7f89 100644 --- a/ClientApp/app/app.component.html +++ b/ClientApp/app/app.component.html @@ -1,6 +1,6 @@ -
        - -
        -
        +
        + +
        +
        diff --git a/ClientApp/app/app.component.scss b/ClientApp/app/app.component.scss index 481063dc..8705be92 100644 --- a/ClientApp/app/app.component.scss +++ b/ClientApp/app/app.component.scss @@ -1,63 +1,71 @@ -$navbar-default-bg: #312312; -$light-orange: #ff8c00; -$navbar-default-color: $light-orange; +@import "http://github.com/variables"; +/* *** Overall APP Styling can go here *** + -------------------------------------------- + Note: This Component has ViewEncapsulation.None so the styles will bleed out -/* Import Bootstrap & Fonts */ -$icon-font-path: '~bootstrap-sass/assets/fonts/bootstrap/'; -@import "http://github.com/~bootstrap-sass/assets/stylesheets/bootstrap"; +*/ +body { + line-height: 18px; + padding-top: $header-height; +} +.body-content { + margin: auto; +} +h1 { + border-bottom: 3px theme-color("accent") solid; + font-size: 24px; +} -/* *** Overall APP Styling can go here *** - -------------------------------------------- - Note: This Component has ViewEncapsulation.None so the styles will bleed out +h2 { + font-size: 20px; +} -*/ -@media (max-width: 767px) { - body { - background: #f1f1f1; - line-height: 18px; - padding-top: 30px; - } +h1, +h2, +h3 { + padding: 3px 0; +} - h1 { - border-bottom: 3px #4189C7 solid; - font-size: 24px; - } +ul { + padding: 10px 25px; +} - h2 { - font-size: 20px; - } +ul li { + padding: 5px 0; +} - h1, h2, h3 { - padding: 3px 0; - } +blockquote { + margin: 25px 10px; + padding: 10px 35px 10px 10px; + border-left: 10px color("green") solid; + background: $gray-100; +} + +blockquote a, +blockquote a:hover { + color: $green; } -@media (min-width: 768px) { +@include media-breakpoint-up(lg) { body { - background: #f1f1f1; - line-height: 18px; - padding-top: 0px; + padding-top: 30px; + } + .body-content { + margin-left: $menu-max-width; } - h1 { border-bottom: 5px #4189C7 solid; font-size: 36px; } - h2 { font-size: 30px; } - - h1, h2, h3 { + h1, + h2, + h3 { padding: 10px 0; } } - -ul { padding: 10px 25px; } -ul li { padding: 5px 0; } - -blockquote { margin: 25px 10px; padding: 10px 35px 10px 10px; border-left: 10px #158a15 solid; background: #edffed; } -blockquote a, blockquote a:hover { color: #068006; } diff --git a/ClientApp/app/app.module.ts b/ClientApp/app/app.module.ts index c0fa0985..7cc9c4c0 100644 --- a/ClientApp/app/app.module.ts +++ b/ClientApp/app/app.module.ts @@ -3,7 +3,7 @@ import { RouterModule, PreloadAllModules } from '@angular/router'; import { CommonModule, APP_BASE_HREF } from '@angular/common'; import { HttpModule, Http } from '@angular/http'; import { HttpClientModule, HttpClient } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser'; import { TransferHttpCacheModule } from '@nguniversal/common'; @@ -54,9 +54,8 @@ export function createTranslateLoader(http: HttpClient, baseHref) { HttpClientModule, TransferHttpCacheModule, BrowserTransferStateModule, - - FormsModule, + ReactiveFormsModule, AccordionModule.forRoot(), // You could also split this up if you don't want the Entire Module imported // i18n support diff --git a/ClientApp/app/components/navmenu/navmenu.component.css b/ClientApp/app/components/navmenu/navmenu.component.css deleted file mode 100644 index 8d86aa03..00000000 --- a/ClientApp/app/components/navmenu/navmenu.component.css +++ /dev/null @@ -1,106 +0,0 @@ -li .glyphicon { - margin-right: 10px; -} - -/* Highlighting rules for nav menu items */ -li.link-active a, -li.link-active a:hover, -li.link-active a:focus { - background-color: #4189C7; - color: white; -} - -/* Keep the nav menu independent of scrolling and on top of other items */ -.main-nav { - position: fixed; - top: 0; - left: 0; - right: 0; - z-index: 1; -} - -.icon-bar { - background-color: #4189C7; -} - - -@media (max-width: 767px) { - /* Apply for small displays */ - .main-nav { - width: 100%; - } - - .navbar-brand { - font-size: 14px; - background-color: #f1f1f1; - } - .navbar-toggle { - padding: 0px 5px; - margin-top: 0px; - height: 26px; - } - - .navbar-link { - margin-top: 4px; - margin-left: 45px; - position: fixed; - } - - .navbar-collapse { - background-color: white; - } - - .navbar a { - /* If a menu item's text is too long, truncate it */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - padding-right: 5px; - } -} - -@media (min-width: 768px) { - /* On small screens, convert the nav menu to a vertical sidebar */ - .main-nav { - height: 100%; - max-width: 330px; - width: calc(25% - 20px); - } - .navbar { - border-radius: 0px; - border-width: 0px; - height: 100%; - } - .navbar-brand{ - width: 100%; - } - .navbar-link { - display: block; - width: 100% - } - .navbar-header { - float: none; - } - .navbar-collapse { - border-top: 1px solid #444; - padding: 0px; - } - .navbar ul { - float: none; - } - .navbar li { - float: none; - font-size: 15px; - margin: 6px; - } - .navbar li a { - padding: 10px 16px; - border-radius: 4px; - } - .navbar a { - /* If a menu item's text is too long, truncate it */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} diff --git a/ClientApp/app/components/navmenu/navmenu.component.html b/ClientApp/app/components/navmenu/navmenu.component.html index 08e1b0fc..36c187ed 100644 --- a/ClientApp/app/components/navmenu/navmenu.component.html +++ b/ClientApp/app/components/navmenu/navmenu.component.html @@ -1,50 +1,41 @@ -
      -

      DevHelp.Online

      +

      Trilon Consulting - Trilon.io

      Consulting | Development | Training | Workshops

      - Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript & ASP.NET!
      + Get your Team or Application up to speed by working with some of the leading industry experts in JavaScript, Node / NestJS, & ASP.NET!

      Follow us on Twitter!

      - @DevHelpOnline | + @trilon_io | @MarkPieszak

      From 83b0301b1db1ed34e1d16d8ef9fa4750c8b970d8 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 17 Mar 2019 11:55:39 -0400 Subject: [PATCH 135/142] docs: update --- README.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e6ed1417..b0b14e9f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -## By [Trilon.io](https://Trilon.io) +## Made with :heart: by [Trilon.io](https://Trilon.io) +

      + + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + +

      + +--- -> Updated to the latest Angular 7.x +## High-level architectural diagram

      ASP.NET Core 2.1 Angular 7+ Starter @@ -471,7 +478,9 @@ Check out **[Trilon.io](https://Trilon.io)** for more info! Twitter [@Trilon_io] Contact us at , and let's talk about your projects needs.

      - Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training +

      ## Follow Trilon online: From 7e2468a7d01472acf15f430b50ca596a64a62d6b Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 17 Mar 2019 11:57:52 -0400 Subject: [PATCH 136/142] docs: update --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index b0b14e9f..b6402f4b 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -## Made with :heart: by [Trilon.io](https://Trilon.io) +### Made with :heart: by [Trilon.io](https://Trilon.io)

      Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

      ---- - -## High-level architectural diagram

      ASP.NET Core 2.1 Angular 7+ Starter From 4aa16404f1b1a0dd1b4e70b7dd891b0006eabd9b Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Mon, 18 Mar 2019 12:32:39 -0400 Subject: [PATCH 137/142] fix: rxjs pin to 6.2.2 closes #714 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99ba5443..01bc52bc 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "preboot": "^7.0.0", "raw-loader": "^1.0.0", "rimraf": "^2.6.3", - "rxjs": "~6.4.0", + "rxjs": "6.2.2", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "to-string-loader": "^1.1.5", From 9cf8b7ba6ee15c01e2ec02a77421e7ee77eb6254 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 5 Apr 2019 12:17:42 -0400 Subject: [PATCH 138/142] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b6402f4b..8e933ca5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ### Made with :heart: by [Trilon.io](https://Trilon.io)

      - Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

      @@ -476,7 +476,7 @@ Contact us at , and let's talk about your projects needs.

      - Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

      From 82dc1897c03f9546b613b72e4a72040b85ab54f1 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 5 Apr 2019 12:45:04 -0400 Subject: [PATCH 139/142] Update README.md --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8e933ca5..ecac2050 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,6 @@

      - -

      - ASP.NET Core 2.1 Angular 7+ Starter -

      - ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: @@ -20,6 +15,12 @@ Angular SEO in action: ASP.NET Core Angular7 SEO

      +### Angular Universal Application Architecture + +

      + ASP.NET Core 2.1 Angular 7+ Starter +

      + ### What is this repo? Live Demo here: http://aspnetcore-angular2-universal.azurewebsites.net This repository is maintained by [Trilon.io](https://Trilon.io) and the [Angular](https://github.com/angular/angular) Universal team and is meant to be an advanced starter From b8ec2b0ff276a187e3821797fbd03201842d5311 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 5 Apr 2019 12:45:28 -0400 Subject: [PATCH 140/142] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ecac2050..482cc672 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@

      +--- + ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! Angular SEO in action: From 55f0105f807946cff515aaa0ef535de1329c2b9f Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Sun, 7 Apr 2019 17:56:32 -0400 Subject: [PATCH 141/142] Update README.md --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 482cc672..f52bdf4d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ # ASP.NET Core 2.1 & Angular 7(+) Advanced Starter - PWA & Server-side prerendering (for Angular SEO)! -### Made with :heart: by [Trilon.io](https://Trilon.io) -

      +--- + +
      +

      - Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training + Trilon.io - Angular Universal, NestJS, JavaScript Application Consulting Development and Training

      + +

      Made with :heart: by Trilon.io

      + --- ### Harness the power of Angular 7+, ASP.NET Core 2.1, now with SEO ! From 837b134516631afb3034d40278e2bc9f170acdba Mon Sep 17 00:00:00 2001 From: Detys Date: Mon, 27 May 2019 02:49:07 +0300 Subject: [PATCH 142/142] fix(seeding): seeding now follows .NET Core 2.0 best practices --- Program.cs | 60 ++++++++++++------- Server/Data/CoreEFStartup.cs | 17 ++++++ Server/Data/LoggingEFStartup.cs | 13 ++++ ...itializer.cs => SimpleContentEFStartup.cs} | 24 ++++---- 4 files changed, 80 insertions(+), 34 deletions(-) create mode 100644 Server/Data/CoreEFStartup.cs create mode 100644 Server/Data/LoggingEFStartup.cs rename Server/Data/{DbInitializer.cs => SimpleContentEFStartup.cs} (67%) diff --git a/Program.cs b/Program.cs index 69e56419..fdf46454 100644 --- a/Program.cs +++ b/Program.cs @@ -6,28 +6,42 @@ using Microsoft.Extensions.Logging; using System; using System.IO; +using System.Threading.Tasks; -public class Program { - public static void Main (string[] args) { - var host = BuildWebHost (args); - using (var scope = host.Services.CreateScope ()) { - var services = scope.ServiceProvider; - try { - var context = services.GetRequiredService(); - DbInitializer.Initialize(context); - } catch (Exception ex) { - var logger = services.GetRequiredService> (); - logger.LogError (ex, "An error occurred while seeding the database."); - } - } +public class Program +{ + public static async Task Main(string[] args) + { + var host = BuildWebHost(args); + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; - host.Run (); - } - public static IWebHost BuildWebHost (string[] args) => - WebHost.CreateDefaultBuilder (args) - .UseKestrel () - .UseContentRoot (Directory.GetCurrentDirectory ()) - .UseIISIntegration () - .UseStartup () - .Build (); - } + try + { + await EnsureDataStorageIsReady(services); + + } catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred while seeding the database."); + } + } + + host.Run(); + } + public static IWebHost BuildWebHost(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + private static async Task EnsureDataStorageIsReady(IServiceProvider services) + { + await CoreEFStartup.InitializeDatabaseAsync(services); + await SimpleContentEFStartup.InitializeDatabaseAsync(services); + await LoggingEFStartup.InitializeDatabaseAsync(services); + } +} diff --git a/Server/Data/CoreEFStartup.cs b/Server/Data/CoreEFStartup.cs new file mode 100644 index 00000000..d3947ef1 --- /dev/null +++ b/Server/Data/CoreEFStartup.cs @@ -0,0 +1,17 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; + +namespace AspCoreServer.Data +{ + public static class CoreEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + var context = services.GetRequiredService(); + + await context.Database.EnsureCreatedAsync(); + } + + } +} diff --git a/Server/Data/LoggingEFStartup.cs b/Server/Data/LoggingEFStartup.cs new file mode 100644 index 00000000..edf33498 --- /dev/null +++ b/Server/Data/LoggingEFStartup.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace AspCoreServer.Data +{ + public static class LoggingEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + //Implent to your hearts' content + } + } +} diff --git a/Server/Data/DbInitializer.cs b/Server/Data/SimpleContentEFStartup.cs similarity index 67% rename from Server/Data/DbInitializer.cs rename to Server/Data/SimpleContentEFStartup.cs index 919c9282..db160dee 100644 --- a/Server/Data/DbInitializer.cs +++ b/Server/Data/SimpleContentEFStartup.cs @@ -1,16 +1,20 @@ using System; -using System.Linq; -using AspCoreServer; +using System.Threading.Tasks; using AspCoreServer.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace AspCoreServer.Data { - public static class DbInitializer { - public static void Initialize (SpaDbContext context) { - context.Database.EnsureCreated (); +namespace AspCoreServer.Data +{ + public static class SimpleContentEFStartup + { + public static async Task InitializeDatabaseAsync(IServiceProvider services) + { + var context = services.GetRequiredService(); - if (context.User.Any ()) { + + if (await context.User.AnyAsync()) + { return; // DB has been seeded } var users = new User[] { @@ -27,11 +31,9 @@ public static void Initialize (SpaDbContext context) { new User () { Name = "Gaulomatic" }, new User () { Name = "GRIMMR3AP3R" } }; + await context.User.AddRangeAsync(users); - foreach (var s in users) { - context.User.Add (s); - } - context.SaveChanges (); + await context.SaveChangesAsync(); } } }