src/React.AspNet/HtmlHelperExtensions.cs (154 lines of code) (raw):
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
using System;
using System.IO;
using System.Linq;
using System.Text;
#if LEGACYASPNET
using System.Web;
using IHtmlHelper = System.Web.Mvc.HtmlHelper;
using IUrlHelper = System.Web.Mvc.UrlHelper;
#else
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Html;
using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
using Microsoft.AspNetCore.Mvc;
#endif
#if LEGACYASPNET
namespace React.Web.Mvc
#else
namespace React.AspNet
#endif
{
/// <summary>
/// HTML Helpers for utilising React from an ASP.NET MVC application.
/// </summary>
public static class HtmlHelperExtensions
{
[ThreadStatic]
private static StringWriter _sharedStringWriter;
/// <summary>
/// Gets the React environment
/// </summary>
private static IReactEnvironment Environment
{
get
{
return ReactEnvironment.GetCurrentOrThrow;
}
}
/// <summary>
/// Renders the specified React component
/// </summary>
/// <typeparam name="T">Type of the props</typeparam>
/// <param name="htmlHelper">HTML helper</param>
/// <param name="componentName">Name of the component</param>
/// <param name="props">Props to initialise the component with</param>
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to <div></param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
/// <param name="renderFunctions">Functions to call during component render</param>
/// <returns>The component's HTML</returns>
public static IHtmlString React<T>(
this IHtmlHelper htmlHelper,
string componentName,
T props,
string htmlTag = null,
string containerId = null,
bool clientOnly = false,
bool serverOnly = false,
string containerClass = null,
Action<Exception, string, string> exceptionHandler = null,
IRenderFunctions renderFunctions = null
)
{
try
{
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly, serverOnly);
if (!string.IsNullOrEmpty(htmlTag))
{
reactComponent.ContainerTag = htmlTag;
}
if (!string.IsNullOrEmpty(containerClass))
{
reactComponent.ContainerClass = containerClass;
}
return RenderToString(writer => reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler, renderFunctions));
}
finally
{
Environment.ReturnEngineToPool();
}
}
/// <summary>
/// Renders the specified React component, along with its client-side initialisation code.
/// Normally you would use the <see cref="React{T}"/> method, but <see cref="ReactWithInit{T}"/>
/// is useful when rendering self-contained partial views.
/// </summary>
/// <typeparam name="T">Type of the props</typeparam>
/// <param name="htmlHelper">HTML helper</param>
/// <param name="componentName">Name of the component</param>
/// <param name="props">Props to initialise the component with</param>
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to <div></param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
/// <param name="exceptionHandler">A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId)</param>
/// <param name="renderFunctions">Functions to call during component render</param>
/// <returns>The component's HTML</returns>
public static IHtmlString ReactWithInit<T>(
this IHtmlHelper htmlHelper,
string componentName,
T props,
string htmlTag = null,
string containerId = null,
bool clientOnly = false,
bool serverOnly = false,
string containerClass = null,
Action<Exception, string, string> exceptionHandler = null,
IRenderFunctions renderFunctions = null
)
{
try
{
var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly);
if (!string.IsNullOrEmpty(htmlTag))
{
reactComponent.ContainerTag = htmlTag;
}
if (!string.IsNullOrEmpty(containerClass))
{
reactComponent.ContainerClass = containerClass;
}
return RenderToString(writer =>
{
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler: exceptionHandler, renderFunctions);
writer.WriteLine();
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter, waitForDOMContentLoad: true));
});
}
finally
{
Environment.ReturnEngineToPool();
}
}
/// <summary>
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
/// </summary>
/// <returns>JavaScript for all components</returns>
public static IHtmlString ReactInitJavaScript(this IHtmlHelper htmlHelper, bool clientOnly = false)
{
try
{
return RenderToString(writer =>
{
WriteScriptTag(writer, bodyWriter => Environment.GetInitJavaScript(bodyWriter, clientOnly));
});
}
finally
{
Environment.ReturnEngineToPool();
}
}
/// <summary>
/// Returns script tags based on the webpack asset manifest
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="urlHelper">Optional IUrlHelper instance. Enables the use of tilde/relative (~/) paths inside the expose-components.js file.</param>
/// <returns></returns>
public static IHtmlString ReactGetScriptPaths(this IHtmlHelper htmlHelper, IUrlHelper urlHelper = null)
{
string nonce = Environment.Configuration.ScriptNonceProvider != null
? $" nonce=\"{Environment.Configuration.ScriptNonceProvider()}\""
: "";
return new HtmlString(string.Join("", Environment.GetScriptPaths()
.Select(scriptPath => $"<script{nonce} src=\"{(urlHelper == null ? scriptPath : urlHelper.Content(scriptPath))}\"></script>")));
}
/// <summary>
/// Returns style tags based on the webpack asset manifest
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="urlHelper">Optional IUrlHelper instance. Enables the use of tilde/relative (~/) paths inside the expose-components.js file.</param>
/// <returns></returns>
public static IHtmlString ReactGetStylePaths(this IHtmlHelper htmlHelper, IUrlHelper urlHelper = null)
{
return new HtmlString(string.Join("", Environment.GetStylePaths()
.Select(stylePath => $"<link rel=\"stylesheet\" href=\"{(urlHelper == null ? stylePath : urlHelper.Content(stylePath))}\" />")));
}
private static IHtmlString RenderToString(Action<StringWriter> withWriter)
{
var stringWriter = _sharedStringWriter;
if (stringWriter != null)
{
stringWriter.GetStringBuilder().Clear();
}
else
{
_sharedStringWriter = stringWriter = new StringWriter(new StringBuilder(128));
}
withWriter(stringWriter);
return new HtmlString(stringWriter.ToString());
}
private static void WriteScriptTag(TextWriter writer, Action<TextWriter> bodyWriter)
{
writer.Write("<script");
if (Environment.Configuration.ScriptNonceProvider != null)
{
writer.Write(" nonce=\"");
writer.Write(Environment.Configuration.ScriptNonceProvider());
writer.Write("\"");
}
writer.Write(">");
bodyWriter(writer);
writer.Write("</script>");
}
}
}