From 7c3d22c7b6feba745f95a4e33d2aa43278f929a4 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 4 Nov 2015 12:19:43 -0800 Subject: [PATCH] Add react tag helper. Clean up code and make it more consistent. --- .../AngularPrerenderTagHelper.cs | 24 ++++----- .../AngularRenderer.cs | 24 +++++++++ .../Content/Node/angular-rendering.js | 39 +++++++++++---- .../project.json | 2 +- .../Content/Node/react-rendering.js | 30 +++++++++--- .../ReactPrerenderTagHelper.cs | 49 +++++++++++++++++++ .../ReactRenderer.cs | 12 ++--- .../project.json | 5 +- samples/angular/MusicStore/project.json | 2 +- .../ReactGrid/Controllers/HomeController.cs | 14 +----- samples/react/ReactGrid/Startup.cs | 3 -- .../react/ReactGrid/Views/Home/Index.cshtml | 2 +- .../react/ReactGrid/Views/_ViewImports.cshtml | 1 + samples/react/ReactGrid/project.json | 2 +- 14 files changed, 146 insertions(+), 63 deletions(-) create mode 100644 Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs create mode 100644 Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs index d920a17..0de90ea 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs @@ -7,17 +7,10 @@ using Microsoft.AspNet.Http.Extensions; namespace Microsoft.AspNet.NodeServices.Angular { [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] - public class AngularRunAtServerTagHelper : TagHelper + public class AngularPrerenderTagHelper : TagHelper { - static StringAsTempFile nodeScript; static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI - static AngularRunAtServerTagHelper() { - // Consider populating this lazily - var script = EmbeddedResourceReader.Read(typeof (AngularRunAtServerTagHelper), "/Content/Node/angular-rendering.js"); - nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit - } - const string PrerenderModuleAttributeName = "asp-ng2-prerender-module"; const string PrerenderExportAttributeName = "asp-ng2-prerender-export"; @@ -30,7 +23,7 @@ namespace Microsoft.AspNet.NodeServices.Angular private IHttpContextAccessor contextAccessor; private INodeServices nodeServices; - public AngularRunAtServerTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) + public AngularPrerenderTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; @@ -44,12 +37,13 @@ namespace Microsoft.AspNet.NodeServices.Angular public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var result = await this.nodeServices.InvokeExport(nodeScript.FileName, "renderComponent", new { - componentModule = this.ModuleName, - componentExport = this.ExportName, - tagName = output.TagName, - baseUrl = UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request) - }); + var result = await AngularRenderer.RenderToString( + nodeServices: this.nodeServices, + componentModuleName: this.ModuleName, + componentExportName: this.ExportName, + componentTagName: output.TagName, + requestUrl: UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request) + ); output.SuppressOutput(); output.PostElement.AppendEncoded(result); } diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs b/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs new file mode 100644 index 0000000..4ca5868 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices.Angular +{ + public static class AngularRenderer + { + private static StringAsTempFile nodeScript; + + static AngularRenderer() { + // Consider populating this lazily + var script = EmbeddedResourceReader.Read(typeof (AngularRenderer), "/Content/Node/angular-rendering.js"); + nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + } + + public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string componentTagName, string requestUrl) { + return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { + moduleName = componentModuleName, + exportName = componentExportName, + tagName = componentTagName, + requestUrl = requestUrl + }); + } + } +} diff --git a/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js b/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js index 57b7df8..460400e 100644 --- a/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js +++ b/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js @@ -3,20 +3,39 @@ var ngUniversal = require('angular2-universal-patched'); var ng = require('angular2/angular2'); var ngRouter = require('angular2/router'); -module.exports = { - renderComponent: function(callback, options) { - // Find the component class. Use options.componentExport if specified, otherwise convert tag-name to PascalCase. - var loadedModule = require(path.resolve(process.cwd(), options.componentModule)); - var componentExport = options.componentExport || options.tagName.replace(/(-|^)([a-z])/g, function (m1, m2, char) { return char.toUpperCase(); }); - var component = loadedModule[componentExport]; - if (!component) { - throw new Error('The module "' + options.componentModule + '" has no export named "' + componentExport + '"'); - } +function getExportOrThrow(moduleInstance, moduleFilename, exportName) { + if (!(exportName in moduleInstance)) { + throw new Error('The module "' + moduleFilename + '" has no export named "' + exportName + '"'); + } + return moduleInstance[exportName]; +} +function findAngularComponent(options) { + var resolvedPath = path.resolve(process.cwd(), options.moduleName); + var loadedModule = require(resolvedPath); + if (options.exportName) { + // If exportName is specified explicitly, use it + return getExportOrThrow(loadedModule, resolvedPath, options.exportName); + } else if (typeof loadedModule === 'function') { + // Otherwise, if the module itself is a function, assume that is the component + return loadedModule; + } else if (typeof loadedModule.default === 'function') { + // Otherwise, if the module has a default export which is a function, assume that is the component + return loadedModule.default; + } else { + // Otherwise, guess the export name by converting tag-name to PascalCase + var tagNameAsPossibleExport = options.tagName.replace(/(-|^)([a-z])/g, function (m1, m2, char) { return char.toUpperCase(); }); + return getExportOrThrow(loadedModule, resolvedPath, tagNameAsPossibleExport); + } +} + +module.exports = { + renderToString: function(callback, options) { + var component = findAngularComponent(options); var serverBindings = [ ngRouter.ROUTER_BINDINGS, ngUniversal.HTTP_PROVIDERS, - ng.provide(ngUniversal.BASE_URL, { useValue: options.baseUrl }), + ng.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }), ngUniversal.SERVER_LOCATION_PROVIDERS ]; diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json index 241590e..a266210 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha3", + "version": "1.0.0-alpha4", "description": "Microsoft.AspNet.NodeServices.Angular Class Library", "authors": [ "Microsoft" diff --git a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js index b60e2d9..74db483 100644 --- a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js +++ b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js @@ -10,6 +10,26 @@ var origJsLoader = require.extensions['.js']; require.extensions['.js'] = loadViaBabel; require.extensions['.jsx'] = loadViaBabel; +function findReactComponent(options) { + var resolvedPath = path.resolve(process.cwd(), options.moduleName); + var loadedModule = require(resolvedPath); + if (options.exportName) { + // If exportName is specified explicitly, use it + if (!(options.exportName in loadedModule)) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"'); + } + return loadedModule[options.exportName]; + } else if (typeof loadedModule === 'function') { + // Otherwise, if the module itself is a function, assume that is the component + return loadedModule; + } else if (typeof loadedModule.default === 'function') { + // Otherwise, if the module has a default export which is a function, assume that is the component + return loadedModule.default; + } else { + throw new Error('Cannot find React component, because no export name was specified, and the module "' + resolvedPath + '" has no default exported class.'); + } +} + function loadViaBabel(module, filename) { // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are. // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict @@ -25,14 +45,8 @@ function loadViaBabel(module, filename) { module.exports = { renderToString: function(callback, options) { - var resolvedPath = path.resolve(process.cwd(), options.moduleName); - var requestedModule = require(resolvedPath); - var component = options.exportName ? requestedModule[options.exportName] : requestedModule; - if (!component) { - throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"'); - } - - var history = createMemoryHistory(options.baseUrl); + var component = findReactComponent(options); + var history = createMemoryHistory(options.requestUrl); var reactElement = React.createElement(component, { history: history }); var html = ReactDOMServer.renderToString(reactElement); callback(null, html); diff --git a/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs new file mode 100644 index 0000000..7ca084a --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Extensions; + +namespace Microsoft.AspNet.NodeServices.React +{ + [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] + public class ReactPrerenderTagHelper : TagHelper + { + static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI + + const string PrerenderModuleAttributeName = "asp-react-prerender-module"; + const string PrerenderExportAttributeName = "asp-react-prerender-export"; + + [HtmlAttributeName(PrerenderModuleAttributeName)] + public string ModuleName { get; set; } + + [HtmlAttributeName(PrerenderExportAttributeName)] + public string ExportName { get; set; } + + private IHttpContextAccessor contextAccessor; + private INodeServices nodeServices; + + public ReactPrerenderTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) + { + this.contextAccessor = contextAccessor; + this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; + + // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() + // in your startup file, but then again it might be confusing that you don't need to. + if (this.nodeServices == null) { + this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http); + } + } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var request = this.contextAccessor.HttpContext.Request; + var result = await ReactRenderer.RenderToString( + nodeServices: this.nodeServices, + componentModuleName: this.ModuleName, + componentExportName: this.ExportName, + requestUrl: request.Path + request.QueryString.Value); + output.Content.SetContentEncoded(result); + } + } +} diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs index 86e0794..d48a694 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs +++ b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs @@ -12,15 +12,11 @@ namespace Microsoft.AspNet.NodeServices.React nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit } - public static Task RenderToString(INodeServices nodeServices, string moduleName, string baseUrl) { - return RenderToString(nodeServices, moduleName, /* exportName */ null, baseUrl); - } - - public static async Task RenderToString(INodeServices nodeServices, string moduleName, string exportName, string baseUrl) { + public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string requestUrl) { return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { - moduleName, - exportName, - baseUrl + moduleName = componentModuleName, + exportName = componentExportName, + requestUrl = requestUrl }); } } diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json index 36356cf..6fa226d 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha3", + "version": "1.0.0-alpha4", "description": "Microsoft.AspNet.NodeServices.React Class Library", "authors": [ "Microsoft" @@ -25,7 +25,8 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha3", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" }, "resource": [ "Content/**/*" diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 4568226..94df1a6 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,7 +19,7 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha4" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/Controllers/HomeController.cs b/samples/react/ReactGrid/Controllers/HomeController.cs index 101330b..4a9899c 100755 --- a/samples/react/ReactGrid/Controllers/HomeController.cs +++ b/samples/react/ReactGrid/Controllers/HomeController.cs @@ -1,24 +1,12 @@ using System.Threading.Tasks; using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.NodeServices; -using Microsoft.AspNet.NodeServices.React; namespace ReactExample.Controllers { public class HomeController : Controller { - private INodeServices nodeServices; - - public HomeController(INodeServices nodeServices) { - this.nodeServices = nodeServices; - } - - public async Task Index(int pageIndex) + public IActionResult Index(int pageIndex) { - ViewData["ReactOutput"] = await ReactRenderer.RenderToString(this.nodeServices, - moduleName: "ReactApp/components/ReactApp.jsx", - baseUrl: Request.Path - ); return View(); } diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs index 9a3a6c9..48ccde8 100755 --- a/samples/react/ReactGrid/Startup.cs +++ b/samples/react/ReactGrid/Startup.cs @@ -27,9 +27,6 @@ namespace ReactExample { // Add MVC services to the services container. services.AddMvc(); - - // Enable Node Services - services.AddNodeServices(); } // Configure is called after ConfigureServices is called. diff --git a/samples/react/ReactGrid/Views/Home/Index.cshtml b/samples/react/ReactGrid/Views/Home/Index.cshtml index bef6868..76f8a6c 100755 --- a/samples/react/ReactGrid/Views/Home/Index.cshtml +++ b/samples/react/ReactGrid/Views/Home/Index.cshtml @@ -1,4 +1,4 @@ -
@Html.Raw(ViewData["ReactOutput"])
+
@section scripts { diff --git a/samples/react/ReactGrid/Views/_ViewImports.cshtml b/samples/react/ReactGrid/Views/_ViewImports.cshtml index 7839f6c..8237608 100755 --- a/samples/react/ReactGrid/Views/_ViewImports.cshtml +++ b/samples/react/ReactGrid/Views/_ViewImports.cshtml @@ -1,2 +1,3 @@ @using ReactExample @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" +@addTagHelper "*, Microsoft.AspNet.NodeServices.React" diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index 5d6f8e0..ed9de5e 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha4" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel"