diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index b136ca42f..514c38840 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -54,7 +54,7 @@ private static IReactEnvironment Environment /// HTML tag to wrap the component in. Defaults to <div> /// ID to use for the container HTML tag. Defaults to an auto-generated ID /// Skip rendering server-side and only output client-side initialisation code. Defaults to false - /// Skip rendering React specific data-attributes during server side rendering. Defaults to false + /// Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to false /// HTML class(es) to set on the container tag /// A custom exception handler that will be called if a component throws during a render. Args: (Exception ex, string componentName, string containerId) /// The component's HTML @@ -72,7 +72,7 @@ public static IHtmlString React( { try { - var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly); + var reactComponent = Environment.CreateComponent(componentName, props, containerId, clientOnly, serverOnly); if (!string.IsNullOrEmpty(htmlTag)) { reactComponent.ContainerTag = htmlTag; diff --git a/src/React.Core/IReactComponent.cs b/src/React.Core/IReactComponent.cs index 3cdf7f794..b4a6d4428 100644 --- a/src/React.Core/IReactComponent.cs +++ b/src/React.Core/IReactComponent.cs @@ -41,6 +41,11 @@ public interface IReactComponent /// string ContainerClass { get; set; } + /// + /// Get or sets if this components only should be rendered server side + /// + bool ServerOnly { get; set; } + /// /// Renders the HTML for this component. This will execute the component server-side and /// return the rendered HTML. diff --git a/src/React.Core/IReactEnvironment.cs b/src/React.Core/IReactEnvironment.cs index 39fccd721..56cbbf5a1 100644 --- a/src/React.Core/IReactEnvironment.cs +++ b/src/React.Core/IReactEnvironment.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -81,8 +81,9 @@ public interface IReactEnvironment /// Props to use /// ID to use for the container HTML tag. Defaults to an auto-generated ID /// True if server-side rendering will be bypassed. Defaults to false. + /// True if this component only should be rendered server-side. Defaults to false. /// The component - IReactComponent CreateComponent(string componentName, T props, string containerId = null, bool clientOnly = false); + IReactComponent CreateComponent(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false); /// /// Adds the provided to the list of components to render client side. diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index e736604f0..012100403 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -68,6 +68,11 @@ public class ReactComponent : IReactComponent /// public string ContainerClass { get; set; } + /// + /// Get or sets if this components only should be rendered server side + /// + public bool ServerOnly { get; set; } + /// /// Gets or sets the props for this component /// @@ -130,6 +135,11 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe ? string.Format("ReactDOMServer.renderToStaticMarkup({0})", GetComponentInitialiser()) : string.Format("ReactDOMServer.renderToString({0})", GetComponentInitialiser()); html = _environment.Execute(reactRenderCommand); + + if (renderServerOnly) + { + return html; + } } catch (JsRuntimeException ex) { diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs index e3a706812..6b1101762 100644 --- a/src/React.Core/ReactEnvironment.cs +++ b/src/React.Core/ReactEnvironment.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -287,8 +287,9 @@ public virtual bool HasVariable(string name) /// Props to use /// ID to use for the container HTML tag. Defaults to an auto-generated ID /// True if server-side rendering will be bypassed. Defaults to false. + /// True if this component only should be rendered server-side. Defaults to false. /// The component - public virtual IReactComponent CreateComponent(string componentName, T props, string containerId = null, bool clientOnly = false) + public virtual IReactComponent CreateComponent(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false) { if (!clientOnly) { @@ -297,7 +298,8 @@ public virtual IReactComponent CreateComponent(string componentName, T props, var component = new ReactComponent(this, _config, componentName, containerId) { - Props = props + Props = props, + ServerOnly = serverOnly }; _components.Add(component); return component; @@ -339,8 +341,11 @@ public virtual string GetInitJavaScript(bool clientOnly = false) foreach (var component in _components) { - fullScript.Append(component.RenderJavaScript()); - fullScript.AppendLine(";"); + if(!component.ServerOnly) + { + fullScript.Append(component.RenderJavaScript()); + fullScript.AppendLine(";"); + } } return fullScript.ToString(); diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs index 234825b05..66fe070da 100644 --- a/tests/React.Tests/Core/ReactComponentTest.cs +++ b/tests/React.Tests/Core/ReactComponentTest.cs @@ -124,6 +124,45 @@ public void RenderHtmlShouldWrapComponentInCustomElement() Assert.Equal(@"[HTML]", result); } + [Fact] + public void RenderHtmlShouldNotRenderComponentWhenContainerOnly() + { + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"ReactDOMServer.renderToString(React.createElement(Foo, {""hello"":""World""}))")) + .Returns("[HTML]"); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" }, + ContainerTag = "span" + }; + var result = component.RenderHtml(true, false); + + Assert.Equal(@"", result); + } + + [Fact] + public void RenderHtmlShouldNotWrapComponentWhenServerSideOnly() + { + var config = new Mock(); + config.Setup(x => x.UseServerSideRendering).Returns(true); + var environment = new Mock(); + environment.Setup(x => x.Execute("typeof Foo !== 'undefined'")).Returns(true); + environment.Setup(x => x.Execute(@"ReactDOMServer.renderToStaticMarkup(React.createElement(Foo, {""hello"":""World""}))")) + .Returns("[HTML]"); + + var component = new ReactComponent(environment.Object, config.Object, "Foo", "container") + { + Props = new { hello = "World" }, + }; + var result = component.RenderHtml(false, true); + + Assert.Equal(@"[HTML]", result); + } + [Fact] public void RenderHtmlShouldAddClassToElement() { diff --git a/tests/React.Tests/Core/ReactEnvironmentTest.cs b/tests/React.Tests/Core/ReactEnvironmentTest.cs index 4017051ba..9c6a4914e 100644 --- a/tests/React.Tests/Core/ReactEnvironmentTest.cs +++ b/tests/React.Tests/Core/ReactEnvironmentTest.cs @@ -1,4 +1,4 @@ -/* +/* * Copyright (c) 2014-Present, Facebook, Inc. * All rights reserved. * @@ -137,6 +137,17 @@ public void CreatesIReactComponent() Assert.Equal(";\r\n", environment.GetInitJavaScript()); } + [Fact] + public void ServerSideOnlyComponentRendersNoJavaScript() + { + var mocks = new Mocks(); + var environment = mocks.CreateReactEnvironment(); + + environment.CreateComponent("HelloWorld", new { name = "Daniel" }, serverOnly: true); + + Assert.Equal(string.Empty, environment.GetInitJavaScript()); + } + public class Mocks { public Mock Engine { get; private set; } diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs index 14d1253a4..635c8ca6a 100644 --- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs +++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs @@ -8,13 +8,13 @@ */ using Moq; -using React.Web.Mvc; using Xunit; +using React.Web.Mvc; namespace React.Tests.Mvc { public class HtmlHelperExtensionsTests - { + { /// /// Creates a mock and registers it with the IoC container /// This is only required because can not be @@ -36,8 +36,9 @@ public void ReactWithInitShouldReturnHtmlAndScript() var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( "ComponentName", - new {}, + new { }, null, + false, false )).Returns(component.Object); @@ -63,7 +64,8 @@ public void EngineIsReturnedToPoolAfterRender() "ComponentName", new { }, null, - true + true, + false )).Returns(component.Object); environment.Verify(x => x.ReturnEngineToPool(), Times.Never); @@ -73,9 +75,9 @@ public void EngineIsReturnedToPoolAfterRender() props: new { }, htmlTag: "span", clientOnly: true, - serverOnly: true + serverOnly: false ); - component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true), null), Times.Once); + component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == false), null), Times.Once); environment.Verify(x => x.ReturnEngineToPool(), Times.Once); } @@ -87,9 +89,10 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( "ComponentName", - new {}, + new { }, null, - true + true, + false )).Returns(component.Object); var result = HtmlHelperExtensions.React( @@ -98,20 +101,22 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue() props: new { }, htmlTag: "span", clientOnly: true, - serverOnly: true + serverOnly: false ); - component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true), null), Times.Once); + component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == false), null), Times.Once); } [Fact] - public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() { + public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() + { var component = new Mock(); - component.Setup(x => x.RenderHtml(true, true, null)).Returns("HTML"); + component.Setup(x => x.RenderHtml(false, true, null)).Returns("HTML"); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( "ComponentName", new { }, null, + false, true )).Returns(component.Object); @@ -120,10 +125,10 @@ public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() { componentName: "ComponentName", props: new { }, htmlTag: "span", - clientOnly: true, + clientOnly: false, serverOnly: true ); - component.Verify(x => x.RenderHtml(It.Is(y => y == true), It.Is(z => z == true), null), Times.Once); + component.Verify(x => x.RenderHtml(It.Is(y => y == false), It.Is(z => z == true), null), Times.Once); } } -} +}