From f639bb657cc1289e3cd74370fb1114bc5ef3ffa2 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 21 Mar 2017 18:57:55 -0400 Subject: [PATCH 01/10] feat(ng-aspnetcore-engine): adding initial new engine WIP --- modules/aspnetcore-engine/README.md | 73 +++++++++++++++++ modules/aspnetcore-engine/index.ts | 101 ++++++++++++++++++++++++ modules/aspnetcore-engine/package.json | 50 ++++++++++++ modules/aspnetcore-engine/tsconfig.json | 35 ++++++++ 4 files changed, 259 insertions(+) create mode 100644 modules/aspnetcore-engine/README.md create mode 100644 modules/aspnetcore-engine/index.ts create mode 100644 modules/aspnetcore-engine/package.json create mode 100644 modules/aspnetcore-engine/tsconfig.json diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md new file mode 100644 index 000000000..fa3e74061 --- /dev/null +++ b/modules/aspnetcore-engine/README.md @@ -0,0 +1,73 @@ +# Angular & ASP.NET Core Engine + +This is an ASP.NET Core Engine for running Angular Apps on the server for server side rendering. + +--- + +## Usage + +To use it, in your boot-server file, within your `createServerRenderer` function, call the `ngAspnetCoreEngine()` engine within a `new Promise()`. + + +```ts +// Polyfills +import 'es6-promise'; +import 'es6-shim'; +import 'reflect-metadata'; +import 'zone.js'; + +import { enableProdMode } from '@angular/core'; +import { INITIAL_CONFIG } from '@angular/platform-server'; + +import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; + +// Grab the (Node) server-specific NgModule +import { AppServerModule } from './app/app.server.module'; + +// ***** The ASPNETCore Angular Engine ***** +import { ngAspnetCoreEngine } from './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 + } + }]; + + return new Promise((resolve, reject) => { + // ***** + ngAspnetCoreEngine(providers, AppServerModule).then(response => { + resolve({ + html: response.html, + globals: response.globals + }); + }) + .catch(error => reject(error)); + + }); + +}); + + +``` + +## Bootstrap + +The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped + +```ts +@NgModule({ + bootstrap: [AppComponent] +}) +export class ServerAppModule { + // Make sure to define this an arrow function to keep the lexical scope + ngOnBootstrap = () => { + console.log('bootstrapped'); + } +} +``` \ No newline at end of file diff --git a/modules/aspnetcore-engine/index.ts b/modules/aspnetcore-engine/index.ts new file mode 100644 index 000000000..77e17a098 --- /dev/null +++ b/modules/aspnetcore-engine/index.ts @@ -0,0 +1,101 @@ +import { Type, NgModuleRef, ApplicationRef, Provider } from '@angular/core'; +import { platformDynamicServer, PlatformState } from '@angular/platform-server'; + +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); + + 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 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 === 'style') { + let styleTag = '`; + STYLES.push(styleTag); + } + + if (element.name === 'meta') { + count = count + 1; + console.log(`\n\n\n ******* Meta count = ${count}`); + let metaString = '\n`); + } + + if (element.name === 'link') { + let linkString = '\n`); + } + } + + resolve({ + html: APP_HTML, + globals: { + styles: STYLES.join(' '), + title: TITLE, + meta: META.join(' '), + links: LINKS.join(' ') + } + }); + + moduleRef.destroy(); + + }); + }).catch(err => { + reject(err); + }); + + }); +} diff --git a/modules/aspnetcore-engine/package.json b/modules/aspnetcore-engine/package.json new file mode 100644 index 000000000..513474062 --- /dev/null +++ b/modules/aspnetcore-engine/package.json @@ -0,0 +1,50 @@ +{ + "name": "@universal/ng-aspnetcore-engine", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "version": "1.0.0-beta.0", + "description": "ASP.NET Core Engine for running Server Angular Apps", + "homepage": "https://github.com/angular/universal", + "license": "MIT", + "author": { + "name": "MarkPieszak", + "email": "mpieszak84@gmail.com", + "url": "github.com/markpieszak" + }, + "contributors": [ + "MarkPieszak" + ], + "repository": { + "type": "git", + "url": "https://github.com/angular/universal" + }, + "bugs": { + "url": "https://github.com/angular/universal/issues" + }, + "config": { + "engine-strict": true + }, + "engines": { + "node": ">= 5.4.1 <= 7", + "npm": ">= 3" + }, + "scripts": { + "build": "tsc", + "prebuild": "rimraf dist" + }, + "peerDependencies": { + "@angular/core": "^4.0.0-rc.5 || ^4.0.0", + "@angular/platform-server": "^4.0.0-rc.5 || ^4.0.0" + }, + "devDependencies": { + "@angular/common": "^4.0.0-rc.5 || ^4.0.0", + "@angular/compiler": "^4.0.0-rc.5 || ^4.0.0", + "@angular/core": "^4.0.0-rc.5 || ^4.0.0", + "@angular/platform-browser": "^4.0.0-rc.5 || ^4.0.0", + "@angular/platform-server": "^4.0.0-rc.5 || ^4.0.0", + "rimraf": "^2.6.1", + "rxjs": "^5.2.0", + "typescript": "^2.2.1", + "zone.js": "^0.8.4" + } +} \ No newline at end of file diff --git a/modules/aspnetcore-engine/tsconfig.json b/modules/aspnetcore-engine/tsconfig.json new file mode 100644 index 000000000..e69476b65 --- /dev/null +++ b/modules/aspnetcore-engine/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es2015", + "moduleResolution": "node", + "declaration": true, + "noImplicitAny": false, + "noUnusedLocals": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedParameters": true, + "removeComments": false, + "baseUrl": ".", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": true, + "inlineSources": true, + "rootDir": ".", + "outDir": "dist", + "lib": [ + "dom", + "es6" + ], + "types": [ + ] + }, + "files": [ + "index.ts" + ], + "compileOnSave": false, + "buildOnSave": false, + "atom": { + "rewriteTsconfig": false + } +} \ No newline at end of file From 787b6b1bc480acd12de5e2f0acc5d4e07b147583 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 21 Mar 2017 19:28:21 -0400 Subject: [PATCH 02/10] update documentation & build --- modules/aspnetcore-engine/README.md | 135 ++++++++++++++++++++++++- modules/aspnetcore-engine/index.ts | 5 +- modules/aspnetcore-engine/package.json | 1 + 3 files changed, 138 insertions(+), 3 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index fa3e74061..2a5e99db7 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -4,9 +4,13 @@ This is an ASP.NET Core Engine for running Angular Apps on the server for server --- -## Usage +# Usage -To use it, in your boot-server file, within your `createServerRenderer` function, call the `ngAspnetCoreEngine()` engine within a `new Promise()`. +> Things have changed since the previous ASP.NET Core & Angular Universal useage. We're no longer using TagHelpers, but now invoking the **boot-server** file from the **Home Controller** *itself*, and passing all the data down to .NET. + +Within our boot-server file, things haven't changed much, you still have your `createServerRenderer()` function that's being exported (this is what's called within the Node process) which is expecting a `Promise` to be returned. + +Within that promise we simply call the ngAspnetCoreEngine itself, passing in our providers Array (here we give it the current `url` from the Server, and also our Root application, which in our case is just ``). ```ts @@ -53,11 +57,138 @@ export default createServerRenderer(params => { }); +``` + +# What about on the .NET MVC Controller side? + +Previously, this was all done with TagHelpers and you passed in your boot-server file to it: ``, but this hindered us from getting the SEO benefits of prerendering. + +Because .NET has control over the Html, using the ngAspnetCoreEngine, we're able to *pull out the important pieces*, and give them back to .NET to place them through out the View. + +Below is how you can invoke the boot-server file which gets everything started: + +> Hopefully in the future this will be cleaned up and less code as well. + +```csharp +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +using Microsoft.AspNetCore.SpaServices.Prerendering; +using Microsoft.AspNetCore.NodeServices; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http.Features; + +namespace WebApplicationBasic.Controllers +{ + public class HomeController : Controller + { + 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}"; + + // Prerender / Serialize application (with Universal) + var prerenderResult = await Prerenderer.RenderToString( + "/", + nodeServices, + new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), + unencodedAbsoluteUrl, + unencodedPathAndQuery, + null, + 30000, + Request.PathBase.ToString() + ); + + // This is where everything is now spliced out, and given to .NET in pieces + ViewData["SpaHtml"] = prerenderResult.Html; + ViewData["Title"] = prerenderResult.Globals["title"]; + ViewData["Styles"] = prerenderResult.Globals["styles"]; + ViewData["Meta"] = prerenderResult.Globals["meta"]; + ViewData["Links"] = prerenderResult.Globals["links"]; + + // Let's render that Home/Index view + return View(); + } + + public IActionResult Error() + { + return View(); + } + } +} + +``` + +# What updates do our Views need now? + +Now we have a whole assortment of SEO goodness we can spread around our .NET application. Not only do we have our serialized Application in a String... + +We also have ``, `<meta>`, `<link>'s`, and our applications `<styles>` + +In our _layout.cshtml, we're going to want to pass in our different `ViewData` pieces and place these where they needed to be. + +> Notice `ViewData[]` sprinkled through out. These came from our Angular application, but it returned an entire HTML document, we want to build up our document ourselves so .NET handles it! + +```html +<!DOCTYPE html> +<html> + <head> + <base href="/" /> + <!-- Title will be the one you set in your Angular application --> + <title>@ViewData["Title"] - AspNET.Core Angular 2+ Universal starter + @Html.Raw(ViewData["Meta"]) + @Html.Raw(ViewData["Links"]) + @Html.Raw(ViewData["Styles"]) + + + + + @RenderBody() + @RenderSection("scripts", required: false) + + ``` +--- + +# Your Home View - where the App gets displayed: + +You may have seen or used a TagHelper here in the past (that's where it used to invoke the Node process and everything), but now since we're doing everything +in the **Controller**, we only need to grab our `ViewData["SpaHtml"]` and inject it! + +This `SpaHtml` was set in our HomeController, and it's just a serialized string of your Angular application, but **only** the `/* inside is all serialized */` part, not the entire Html, since we split that up, and let .NET build out our Document. + +```html +@Html.Raw(ViewData["SpaHtml"]) + + + +@section scripts { + +} +``` + +--- + +# What happens after the App gets server rendered? + +Well now, your Client-side Angular will take over, and you'll have a fully functioning SPA. (With all these great SEO benefits of being server-rendered) ! + +:sparkles: + +--- + ## Bootstrap +> [TODO] : This needs to be explained further + The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped ```ts diff --git a/modules/aspnetcore-engine/index.ts b/modules/aspnetcore-engine/index.ts index 77e17a098..162a4d69d 100644 --- a/modules/aspnetcore-engine/index.ts +++ b/modules/aspnetcore-engine/index.ts @@ -1,6 +1,9 @@ import { Type, NgModuleRef, ApplicationRef, Provider } from '@angular/core'; import { platformDynamicServer, PlatformState } from '@angular/platform-server'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/first'; + export function ngAspnetCoreEngine( providers: Provider[], ngModule: Type<{}> @@ -18,7 +21,7 @@ export function ngAspnetCoreEngine( appRef.isStable .filter((isStable: boolean) => isStable) .first() - .subscribe((stable) => { + .subscribe(() => { // Fire the TransferCache const bootstrap = moduleRef.instance['ngOnBootstrap']; diff --git a/modules/aspnetcore-engine/package.json b/modules/aspnetcore-engine/package.json index 513474062..758443d5b 100644 --- a/modules/aspnetcore-engine/package.json +++ b/modules/aspnetcore-engine/package.json @@ -40,6 +40,7 @@ "@angular/common": "^4.0.0-rc.5 || ^4.0.0", "@angular/compiler": "^4.0.0-rc.5 || ^4.0.0", "@angular/core": "^4.0.0-rc.5 || ^4.0.0", + "@angular/http": "^4.0.0-rc.5 || ^4.0.0", "@angular/platform-browser": "^4.0.0-rc.5 || ^4.0.0", "@angular/platform-server": "^4.0.0-rc.5 || ^4.0.0", "rimraf": "^2.6.1", From 4c768b8f8af8d7bcd0b0a4b1934f72fd51c2c67e Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Tue, 21 Mar 2017 19:33:09 -0400 Subject: [PATCH 03/10] update documentation - startup.cs --- modules/aspnetcore-engine/README.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index 2a5e99db7..3f9d40cc3 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -59,7 +59,7 @@ export default createServerRenderer(params => { ``` -# What about on the .NET MVC Controller side? +# What about on the .NET side? Previously, this was all done with TagHelpers and you passed in your boot-server file to it: ``, but this hindered us from getting the SEO benefits of prerendering. @@ -69,6 +69,8 @@ Below is how you can invoke the boot-server file which gets everything started: > Hopefully in the future this will be cleaned up and less code as well. +### HomeController.cs + ```csharp using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; @@ -122,7 +124,17 @@ namespace WebApplicationBasic.Controllers } } } +``` +### Startup.cs : Make sure you add NodeServices to ConfigureServices: + +```csharp +public void ConfigureServices(IServiceCollection services) +{ + // ... other things ... + + services.AddNodeServices(); // <-- +} ``` # What updates do our Views need now? From 8d5f8d8cf86c528d1c2d062998601e6c347a0f76 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Wed, 22 Mar 2017 10:18:50 -0400 Subject: [PATCH 04/10] passing data from .NET -> Angular --- modules/aspnetcore-engine/README.md | 30 ++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index 3f9d40cc3..ca2618b0e 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -34,6 +34,14 @@ enableProdMode(); export default createServerRenderer(params => { + /* + * How can we access data we passed from .NET ? + * you'd access it directly from `params` under the name you passed it + * ie: params.cookies + * ------- + * Next you'd want to pass in some + */ + // Platform-server provider configuration const providers = [{ provide: INITIAL_CONFIG, @@ -41,20 +49,28 @@ export default createServerRenderer(params => { document: '', // * Our Root application document url: params.url } - }]; + } + /* Other providers you want to pass into the App would go here + * { provide: CookieService, useClass: ServerCookieService } + * ie: Just an example of Dependency injecting a Class for providing Cookies (that you passed down from the server) + (Where on the browser you'd have a different class handling cookies normally) + */ + ]; return new Promise((resolve, reject) => { - // ***** + // ***** Pass in those Providers & your Server NgModule, and that's it! ngAspnetCoreEngine(providers, AppServerModule).then(response => { resolve({ - html: response.html, - globals: response.globals + // Our `` serialized as a String + html: response.html, + + // A collection of ` & our tags` + // We'll use these to separately let .NET handle each one individually + globals: response.globals }); }) .catch(error => reject(error)); - }); - }); ``` @@ -102,7 +118,7 @@ namespace WebApplicationBasic.Controllers new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), unencodedAbsoluteUrl, unencodedPathAndQuery, - null, + null, // <-- Here you can pass any DATA you want 30000, Request.PathBase.ToString() ); From e64f165b78180ac8d529b32f7e7ef5dcdc5573c1 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 22 Mar 2017 10:22:04 -0400 Subject: [PATCH 05/10] describe where data is passed from .NET --- modules/aspnetcore-engine/README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index ca2618b0e..9c52ed5a6 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -39,7 +39,7 @@ export default createServerRenderer(params => { * you'd access it directly from `params` under the name you passed it * ie: params.cookies * ------- - * Next you'd want to pass in some + * We'll show in the next section WHERE you pass this Data in on the .NET side */ // Platform-server provider configuration @@ -111,6 +111,14 @@ namespace WebApplicationBasic.Controllers var unencodedPathAndQuery = requestFeature.RawTarget; var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}"; + // ********************************* + // This parameter is where you'd pass in an Object of data you want passed down to Angular + // to be used in the Server-rendering + // ie: Cookies from `ViewContext.HttpContext.Request.Cookies` + // ie: an Authentication token you already have generated from the Server + // * any data you want! * + var customData = null; + // Prerender / Serialize application (with Universal) var prerenderResult = await Prerenderer.RenderToString( "/", @@ -118,7 +126,9 @@ namespace WebApplicationBasic.Controllers new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), unencodedAbsoluteUrl, unencodedPathAndQuery, - null, // <-- Here you can pass any DATA you want + // Custom data will be passed down to Angular (within the boot-server file) + // Available there via `params.yourObject` + customData, 30000, Request.PathBase.ToString() ); From a57a9f2a7d782db729b84d0accb995bdd5c36831 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Wed, 22 Mar 2017 19:24:07 -0400 Subject: [PATCH 06/10] update api --- modules/aspnetcore-engine/README.md | 30 ++++++++++++----------------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index 9c52ed5a6..b5d4d9b47 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -22,15 +22,13 @@ import 'zone.js'; import { enableProdMode } from '@angular/core'; import { INITIAL_CONFIG } from '@angular/platform-server'; - import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; - // Grab the (Node) server-specific NgModule import { AppServerModule } from './app/app.server.module'; - // ***** The ASPNETCore Angular Engine ***** -import { ngAspnetCoreEngine } from './aspnetcore-engine'; -enableProdMode(); +import { ngAspnetCoreEngine } from '@universal/ng-aspnetcore-engine'; + +enableProdMode(); // for faster server rendered builds export default createServerRenderer(params => { @@ -57,19 +55,15 @@ export default createServerRenderer(params => { */ ]; - return new Promise((resolve, reject) => { - // ***** Pass in those Providers & your Server NgModule, and that's it! - ngAspnetCoreEngine(providers, AppServerModule).then(response => { - resolve({ - // Our `<app></app>` serialized as a String - html: response.html, - - // A collection of `<meta><link><styles> & our <title> tags` - // We'll use these to separately let .NET handle each one individually - globals: response.globals - }); - }) - .catch(error => reject(error)); + // ***** Pass in those Providers & your Server NgModule, and that's it! + return ngAspnetCoreEngine(providers, AppServerModule).then(response => { + resolve({ + // Our `<app></app>` serialized as a String + html: response.html, + // A collection of `<meta><link><styles> & our <title> tags` + // We'll use these to separately let .NET handle each one individually + globals: response.globals + }); }); }); From 455e68adf0498c13d6e073f65a06b72ce12b0011 Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Fri, 21 Apr 2017 19:35:06 -0400 Subject: [PATCH 07/10] separate out logic --- modules/aspnetcore-engine/index.ts | 106 +--------- .../src/create-transfer-script.ts | 3 + modules/aspnetcore-engine/src/file-loader.ts | 16 ++ .../src/interfaces/engine-options.ts | 9 + .../src/interfaces/request-params.ts | 9 + modules/aspnetcore-engine/src/main.ts | 198 ++++++++++++++++++ modules/aspnetcore-engine/src/tokens.ts | 4 + modules/aspnetcore-engine/tsconfig.json | 1 + 8 files changed, 245 insertions(+), 101 deletions(-) create mode 100644 modules/aspnetcore-engine/src/create-transfer-script.ts create mode 100644 modules/aspnetcore-engine/src/file-loader.ts create mode 100644 modules/aspnetcore-engine/src/interfaces/engine-options.ts create mode 100644 modules/aspnetcore-engine/src/interfaces/request-params.ts create mode 100644 modules/aspnetcore-engine/src/main.ts create mode 100644 modules/aspnetcore-engine/src/tokens.ts diff --git a/modules/aspnetcore-engine/index.ts b/modules/aspnetcore-engine/index.ts index 162a4d69d..1fdb6201a 100644 --- a/modules/aspnetcore-engine/index.ts +++ b/modules/aspnetcore-engine/index.ts @@ -1,104 +1,8 @@ -import { Type, NgModuleRef, ApplicationRef, Provider } from '@angular/core'; -import { platformDynamicServer, PlatformState } from '@angular/platform-server'; -import 'rxjs/add/operator/filter'; -import 'rxjs/add/operator/first'; +export { ngAspnetCoreEngine } from './src/main'; +export { createTransferScript } from './src/create-transfer-script'; -export function ngAspnetCoreEngine( - providers: Provider[], - ngModule: Type<{}> -): Promise<{ html: string, globals: { styles: string, title: string, meta: string, [key:string]: any } }> { +export { COOKIES, ORIGIN_URL } from './src/tokens'; - return new Promise((resolve, reject) => { - - const platform = platformDynamicServer(providers); - - return platform.bootstrapModule(<Type<{}>>ngModule).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(() => { - - // 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('<body>') + 6, - htmlDoc.indexOf('</body>') - ); - - // Strip out Styles / Meta-tags / Title - const STYLES = []; - const META = []; - const LINKS = []; - let TITLE = ''; - - 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 === 'style') { - let styleTag = '<style '; - for (let key in element.attribs) { - styleTag += `${key}="${element.attribs[key]}">`; - } - - styleTag += `${element.children[0].data}</style>`; - STYLES.push(styleTag); - } - - if (element.name === 'meta') { - count = count + 1; - console.log(`\n\n\n ******* Meta count = ${count}`); - let metaString = '<meta'; - for (let key in element.attribs) { - metaString += ` ${key}="${element.attribs[key]}"`; - } - META.push(`${metaString} />\n`); - } - - if (element.name === 'link') { - let linkString = '<link'; - for (let key in element.attribs) { - linkString += ` ${key}="${element.attribs[key]}"`; - } - LINKS.push(`${linkString} />\n`); - } - } - - resolve({ - html: APP_HTML, - globals: { - styles: STYLES.join(' '), - title: TITLE, - meta: META.join(' '), - links: LINKS.join(' ') - } - }); - - moduleRef.destroy(); - - }); - }).catch(err => { - reject(err); - }); - - }); -} +export { IEngineOptions } from './src/interfaces/engine-options'; +export { IRequestParams } from './src/interfaces/request-params'; diff --git a/modules/aspnetcore-engine/src/create-transfer-script.ts b/modules/aspnetcore-engine/src/create-transfer-script.ts new file mode 100644 index 000000000..15b299d07 --- /dev/null +++ b/modules/aspnetcore-engine/src/create-transfer-script.ts @@ -0,0 +1,3 @@ +export function createTransferScript(transferData: Object): string { + return `<script>window['TRANSFER_CACHE'] = ${JSON.stringify(transferData)};</script>`; +} \ No newline at end of file diff --git a/modules/aspnetcore-engine/src/file-loader.ts b/modules/aspnetcore-engine/src/file-loader.ts new file mode 100644 index 000000000..78a16012c --- /dev/null +++ b/modules/aspnetcore-engine/src/file-loader.ts @@ -0,0 +1,16 @@ +import { ResourceLoader } from '@angular/compiler'; + +export class FileLoader implements ResourceLoader { + get(url: string): Promise<string> { + return new Promise((resolve, reject) => { + // install node types + fs.readFile(url, (err: NodeJS.ErrnoException, buffer: Buffer) => { + if (err) { + return reject(err); + } + + resolve(buffer.toString()); + }); + }); + } +} diff --git a/modules/aspnetcore-engine/src/interfaces/engine-options.ts b/modules/aspnetcore-engine/src/interfaces/engine-options.ts new file mode 100644 index 000000000..8b9bd7355 --- /dev/null +++ b/modules/aspnetcore-engine/src/interfaces/engine-options.ts @@ -0,0 +1,9 @@ +import { IRequestParams } from "./request-params"; +import { Type, NgModuleFactory, Provider } from '@angular/core'; + +export interface IEngineOptions { + appSelector: string; + request: IRequestParams; + ngModule: Type<{}> | NgModuleFactory<{}>; + providers?: Provider[]; +}; diff --git a/modules/aspnetcore-engine/src/interfaces/request-params.ts b/modules/aspnetcore-engine/src/interfaces/request-params.ts new file mode 100644 index 000000000..2d989eca0 --- /dev/null +++ b/modules/aspnetcore-engine/src/interfaces/request-params.ts @@ -0,0 +1,9 @@ +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<any>; + data: any; // any custom object passed through from .NET +} \ No newline at end of file diff --git a/modules/aspnetcore-engine/src/main.ts b/modules/aspnetcore-engine/src/main.ts new file mode 100644 index 000000000..552422e21 --- /dev/null +++ b/modules/aspnetcore-engine/src/main.ts @@ -0,0 +1,198 @@ +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, ORIGIN_URL } from './tokens'; +import { FileLoader } from './file-loader'; + +import { IEngineOptions } from './interfaces/engine-options'; + +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('<body>') + 6, + htmlDoc.indexOf('</body>') + ); + + // Strip out Styles / Meta-tags / Title + const STYLES = []; + const META = []; + const LINKS = []; + let TITLE = ''; + + let STYLES_STRING = htmlDoc.substring( + htmlDoc.indexOf('<style ng-transition'), + htmlDoc.lastIndexOf('</style>') + 8 + ); + // STYLES_STRING = STYLES_STRING.replace(/\s/g, '').replace('<styleng-transition', '<style ng-transition'); + + 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; + } + + // Broken after 4.0 (worked in rc) + // if (element.name === 'style') { + // let styleTag = '<style '; + // for (let key in element.attribs) { + // if (key) { + // styleTag += `${key}="${element.attribs[key]}">`; + // } + // } + + // styleTag += `${element.children[0].data}</style>`; + // STYLES.push(styleTag); + // } + + if (element.name === 'meta') { + count = count + 1; + let metaString = '<meta'; + for (let key in element.attribs) { + if (key) { + metaString += ` ${key}="${element.attribs[key]}"`; + } + } + META.push(`${metaString} />\n`); + } + + if (element.name === 'link') { + let linkString = '<link'; + for (let key in element.attribs) { + if (key) { + linkString += ` ${key}="${element.attribs[key]}"`; + } + } + LINKS.push(`${linkString} />\n`); + } + } + + resolve({ + html: APP_HTML, + globals: { + styles: STYLES_STRING, + title: TITLE, + meta: META.join(' '), + links: LINKS.join(' ') + } + }); + + moduleRef.destroy(); + + }, (err) => { + reject(err); + }); + + }); + }); + + } catch (ex) { + reject(ex); + } + + }); +} + +/* ********************** Private / Internal ****************** */ + +const factoryCacheMap = new Map<Type<{}>, NgModuleFactory<{}>>(); +function getFactory( + moduleOrFactory: Type<{}> | NgModuleFactory<{}>, compiler: Compiler +): Promise<NgModuleFactory<{}>> { + return new Promise<NgModuleFactory<{}>>((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/modules/aspnetcore-engine/src/tokens.ts b/modules/aspnetcore-engine/src/tokens.ts new file mode 100644 index 000000000..701e7670b --- /dev/null +++ b/modules/aspnetcore-engine/src/tokens.ts @@ -0,0 +1,4 @@ +import { InjectionToken } from '@angular/core'; + +export const REQUEST = new InjectionToken<string>('REQUEST'); +export const ORIGIN_URL = new InjectionToken<string>('ORIGIN_URL'); diff --git a/modules/aspnetcore-engine/tsconfig.json b/modules/aspnetcore-engine/tsconfig.json index e69476b65..1b8e7f387 100644 --- a/modules/aspnetcore-engine/tsconfig.json +++ b/modules/aspnetcore-engine/tsconfig.json @@ -22,6 +22,7 @@ "es6" ], "types": [ + "node" ] }, "files": [ From 41b7ce693fada1e8869f02b57db56623e8ded63e Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Fri, 21 Apr 2017 20:01:48 -0400 Subject: [PATCH 08/10] updates & readme update --- modules/aspnetcore-engine/README.md | 114 +++++++++++++------ modules/aspnetcore-engine/index.ts | 2 +- modules/aspnetcore-engine/package.json | 21 ++-- modules/aspnetcore-engine/src/file-loader.ts | 1 + modules/aspnetcore-engine/src/main.ts | 17 ++- 5 files changed, 102 insertions(+), 53 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index b5d4d9b47..54c60842c 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -34,36 +34,45 @@ export default createServerRenderer(params => { /* * How can we access data we passed from .NET ? - * you'd access it directly from `params` under the name you passed it - * ie: params.cookies + * you'd access it directly from `params.data` under the name you passed it + * ie: params.data.WHATEVER_YOU_PASSED * ------- * We'll show in the next section WHERE you pass this Data in on the .NET side */ // Platform-server provider configuration - const providers = [{ - provide: INITIAL_CONFIG, - useValue: { - document: '<app></app>', // * Our Root application document - url: params.url - } - } - /* Other providers you want to pass into the App would go here - * { provide: CookieService, useClass: ServerCookieService } - * ie: Just an example of Dependency injecting a Class for providing Cookies (that you passed down from the server) - (Where on the browser you'd have a different class handling cookies normally) - */ - ]; + const setupOptions: IEngineOptions = { + appSelector: '<app></app>', + ngModule: ServerAppModule, + request: params, + providers: [ + /* Other providers you want to pass into the App would go here + * { provide: CookieService, useClass: ServerCookieService } + + * ie: Just an example of Dependency injecting a Class for providing Cookies (that you passed down from the server) + (Where on the browser you'd have a different class handling cookies normally) + */ + ] + }; // ***** Pass in those Providers & your Server NgModule, and that's it! - return ngAspnetCoreEngine(providers, AppServerModule).then(response => { - resolve({ - // Our `<app></app>` serialized as a String - html: response.html, - // A collection of `<meta><link><styles> & our <title> tags` - // We'll use these to separately let .NET handle each one individually - globals: response.globals - }); + return ngAspnetCoreEngine(setupOptions).then(response => { + + // Want to transfer data from Server -> Client? + + // Add transferData to the response.globals Object, and call createTransferScript({}) passing in the Object key/values of data + // createTransferScript() will JSON Stringify it and return it as a <script> window.TRANSFER_CACHE={}</script> + // That your browser can pluck and grab the data from + 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 + }); + }); }); @@ -108,22 +117,27 @@ namespace WebApplicationBasic.Controllers // ********************************* // This parameter is where you'd pass in an Object of data you want passed down to Angular // to be used in the Server-rendering - // ie: Cookies from `ViewContext.HttpContext.Request.Cookies` - // ie: an Authentication token you already have generated from the Server - // * any data you want! * - var customData = null; + + // ** TransferData concept ** + // Here we can pass any Custom Data we want ! + + // By default we're passing down the REQUEST Object (Cookies, Headers, Host) from the Request object here + TransferData transferData = new TransferData(); + transferData.request = AbstractHttpContextRequestInfo(Request); // You can automatically grab things from the REQUEST object in Angular because of this + 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( - "/", + "/", // baseURL nodeServices, new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"), unencodedAbsoluteUrl, unencodedPathAndQuery, - // Custom data will be passed down to Angular (within the boot-server file) - // Available there via `params.yourObject` - customData, - 30000, + // Our Transfer data here will be passed down to Angular (within the boot-server file) + // Available there via `params.data.yourData` + transferData, + 30000, // timeout duration Request.PathBase.ToString() ); @@ -133,15 +147,39 @@ namespace WebApplicationBasic.Controllers ViewData["Styles"] = prerenderResult.Globals["styles"]; ViewData["Meta"] = prerenderResult.Globals["meta"]; ViewData["Links"] = prerenderResult.Globals["links"]; + ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {}; // Let's render that Home/Index view return View(); } - public IActionResult Error() + private IRequest AbstractHttpContextRequestInfo(HttpRequest request) { - return View(); + + 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 + { + // By default we're expecting the REQUEST Object (in the aspnet engine), so leave this one here + public dynamic request { get; set; } + + // Your data here ? + public object thisCameFromDotNET { get; set; } } } ``` @@ -183,6 +221,10 @@ In our _layout.cshtml, we're going to want to pass in our different `ViewData` p <body> <!-- Our Home view will be rendered here --> @RenderBody() + + <!-- 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> @@ -219,9 +261,7 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct ## Bootstrap -> [TODO] : This needs to be explained further - -The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped +The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped, this is how the TransferData gets taken. ```ts @NgModule({ diff --git a/modules/aspnetcore-engine/index.ts b/modules/aspnetcore-engine/index.ts index 1fdb6201a..0a23d586a 100644 --- a/modules/aspnetcore-engine/index.ts +++ b/modules/aspnetcore-engine/index.ts @@ -2,7 +2,7 @@ export { ngAspnetCoreEngine } from './src/main'; export { createTransferScript } from './src/create-transfer-script'; -export { COOKIES, ORIGIN_URL } from './src/tokens'; +export { REQUEST, ORIGIN_URL } from './src/tokens'; export { IEngineOptions } from './src/interfaces/engine-options'; export { IRequestParams } from './src/interfaces/request-params'; diff --git a/modules/aspnetcore-engine/package.json b/modules/aspnetcore-engine/package.json index 758443d5b..cf3df18af 100644 --- a/modules/aspnetcore-engine/package.json +++ b/modules/aspnetcore-engine/package.json @@ -33,19 +33,22 @@ "prebuild": "rimraf dist" }, "peerDependencies": { - "@angular/core": "^4.0.0-rc.5 || ^4.0.0", - "@angular/platform-server": "^4.0.0-rc.5 || ^4.0.0" + "@angular/core": "^4.0.0", + "@angular/platform-server": "^4.0.0" }, "devDependencies": { - "@angular/common": "^4.0.0-rc.5 || ^4.0.0", - "@angular/compiler": "^4.0.0-rc.5 || ^4.0.0", - "@angular/core": "^4.0.0-rc.5 || ^4.0.0", - "@angular/http": "^4.0.0-rc.5 || ^4.0.0", - "@angular/platform-browser": "^4.0.0-rc.5 || ^4.0.0", - "@angular/platform-server": "^4.0.0-rc.5 || ^4.0.0", + "@angular/common": "^4.0.0", + "@angular/compiler": "^4.0.0", + "@angular/core": "^4.0.0", + "@angular/http": "^4.0.0", + "@angular/platform-browser": "^4.0.0", + "@angular/platform-server": "^4.0.0", "rimraf": "^2.6.1", "rxjs": "^5.2.0", "typescript": "^2.2.1", "zone.js": "^0.8.4" + }, + "dependencies": { + "@types/node": "^7.0.13" } -} \ No newline at end of file +} diff --git a/modules/aspnetcore-engine/src/file-loader.ts b/modules/aspnetcore-engine/src/file-loader.ts index 78a16012c..430fc9174 100644 --- a/modules/aspnetcore-engine/src/file-loader.ts +++ b/modules/aspnetcore-engine/src/file-loader.ts @@ -1,3 +1,4 @@ +import * as fs from 'fs'; import { ResourceLoader } from '@angular/compiler'; export class FileLoader implements ResourceLoader { diff --git a/modules/aspnetcore-engine/src/main.ts b/modules/aspnetcore-engine/src/main.ts index 552422e21..0c1afbd60 100644 --- a/modules/aspnetcore-engine/src/main.ts +++ b/modules/aspnetcore-engine/src/main.ts @@ -1,13 +1,15 @@ -import { Type, NgModuleFactory, NgModuleRef, ApplicationRef, Provider, CompilerFactory, Compiler } from '@angular/core'; -import { platformServer, platformDynamicServer, PlatformState, INITIAL_CONFIG, renderModuleFactory } from '@angular/platform-server'; +import { Type, NgModuleFactory, NgModuleRef, ApplicationRef, CompilerFactory, Compiler } from '@angular/core'; +import { platformServer, platformDynamicServer, PlatformState, INITIAL_CONFIG } from '@angular/platform-server'; import { ResourceLoader } from '@angular/compiler'; -import * as fs from 'fs'; import { REQUEST, ORIGIN_URL } from './tokens'; import { FileLoader } from './file-loader'; import { IEngineOptions } from './interfaces/engine-options'; +import 'rxjs/add/operator/filter'; +import 'rxjs/add/operator/first'; + export function ngAspnetCoreEngine( options: IEngineOptions ): Promise<{ html: string, globals: { styles: string, title: string, meta: string, transferData?: {}, [key: string]: any } }> { @@ -64,7 +66,7 @@ export function ngAspnetCoreEngine( appRef.isStable .filter((isStable: boolean) => isStable) .first() - .subscribe((stable) => { + .subscribe(() => { // Fire the TransferState Cache const bootstrap = moduleRef.instance['ngOnBootstrap']; @@ -82,7 +84,7 @@ export function ngAspnetCoreEngine( ); // Strip out Styles / Meta-tags / Title - const STYLES = []; + // const STYLES = []; const META = []; const LINKS = []; let TITLE = ''; @@ -104,7 +106,9 @@ export function ngAspnetCoreEngine( TITLE = element.children[0].data; } - // Broken after 4.0 (worked in rc) + // Broken after 4.0 (worked in rc) - needs investigation + // As other things could be in <style> so we ideally want to get them this way + // if (element.name === 'style') { // let styleTag = '<style '; // for (let key in element.attribs) { @@ -139,6 +143,7 @@ export function ngAspnetCoreEngine( } } + // Return parsed App resolve({ html: APP_HTML, globals: { From cd85f48c7eefa63cc66bb05b167442de3397663c Mon Sep 17 00:00:00 2001 From: Mark Pieszak <mpieszak84@gmail.com> Date: Fri, 21 Apr 2017 20:19:20 -0400 Subject: [PATCH 09/10] update readme --- modules/aspnetcore-engine/README.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index 54c60842c..01bb94405 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -211,7 +211,7 @@ In our _layout.cshtml, we're going to want to pass in our different `ViewData` p <head> <base href="/" /> <!-- Title will be the one you set in your Angular application --> - <title>@ViewData["Title"] - AspNET.Core Angular 2+ Universal starter + @ViewData["Title"] @Html.Raw(ViewData["Meta"]) @Html.Raw(ViewData["Links"]) @@ -262,6 +262,7 @@ Well now, your Client-side Angular will take over, and you'll have a fully funct ## Bootstrap The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped, this is how the TransferData gets taken. +Check [https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/master/Client/modules](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/master/Client/modules) to see how to setup your Transfer classes. ```ts @NgModule({ @@ -273,4 +274,8 @@ export class ServerAppModule { console.log('bootstrapped'); } } -``` \ No newline at end of file +``` + +## Example Application utilizing this Engine + +#### [Asp.net Core & Angular advanced starter application](https://github.com/MarkPieszak/aspnetcore-angular2-universal) \ No newline at end of file From 54dec51f09fa0f13743fe92bd822866fff1267e1 Mon Sep 17 00:00:00 2001 From: Mark Pieszak Date: Fri, 21 Apr 2017 20:32:09 -0400 Subject: [PATCH 10/10] add docs about tokens --- modules/aspnetcore-engine/README.md | 65 ++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/modules/aspnetcore-engine/README.md b/modules/aspnetcore-engine/README.md index 01bb94405..e3e57c9eb 100644 --- a/modules/aspnetcore-engine/README.md +++ b/modules/aspnetcore-engine/README.md @@ -4,6 +4,10 @@ This is an ASP.NET Core Engine for running Angular Apps on the server for server --- +## Example Application utilizing this Engine + +#### [Asp.net Core & Angular advanced starter application](https://github.com/MarkPieszak/aspnetcore-angular2-universal) + # Usage > Things have changed since the previous ASP.NET Core & Angular Universal useage. We're no longer using TagHelpers, but now invoking the **boot-server** file from the **Home Controller** *itself*, and passing all the data down to .NET. @@ -276,6 +280,63 @@ export class ServerAppModule { } ``` -## Example Application utilizing this Engine +# Tokens + +Along with the engine doing serializing and separating out the chunks of your Application (so we can let .NET handle it), you may have noticed we passed in the HttpRequest object from .NET into it as well. + +This was done so that we could take a few things from it, and using dependency injection, "provide" a few things to the Angular application. + +```typescript +ORIGIN_URL +// and +REQUEST + +// imported +import { ORIGIN_URL, REQUEST } from '@ng-universal/ng-aspnetcore-engine'; +``` + +Make sure in your BrowserModule you provide these tokens as well, if you're going to use them! + +```typescript +@NgModule({ + ..., + providers: [ + { + // 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) + } + ] +} export class BrowserAppModule() {} +``` + +Don't forget that the server needs Absolute URLs for paths when doing Http requests! So if your server api is at the same location as this Angular app, you can't just do `http.get('/api/whatever')` so use the ORIGIN_URL Injection Token. + +```typescript + import { ORIGIN_URL } from '@ng-universal/ng-aspnetcore-engine'; + + constructor(@Inject(ORIGIN_URL) private originUrl: string, private http: Http) { + this.http.get(`${this.originUrl}/api/whatever`) + } +``` + +As for the REQUEST object, you'll find Cookies, Headers, and Host (from .NET that we passed down in our HomeController. They'll all be accessible from that Injection Token as well. + +```typescript + import { REQUEST } from '@ng-universal/ng-aspnetcore-engine'; + + constructor(@Inject(REQUEST) private request) { + // this.request.cookies + // this.request.headers + // etc + } + +``` + + -#### [Asp.net Core & Angular advanced starter application](https://github.com/MarkPieszak/aspnetcore-angular2-universal) \ No newline at end of file