Skip to content
6 changes: 3 additions & 3 deletions src/React.AspNet/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -54,7 +54,7 @@ private static IReactEnvironment Environment
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to &lt;div&gt;</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 during server side rendering. 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>
/// <returns>The component's HTML</returns>
Expand All @@ -72,7 +72,7 @@ public static IHtmlString React<T>(
{
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;
Expand Down
5 changes: 5 additions & 0 deletions src/React.Core/IReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public interface IReactComponent
/// </summary>
string ContainerClass { get; set; }

/// <summary>
/// Get or sets if this components only should be rendered server side
/// </summary>
bool ServerOnly { get; set; }

/// <summary>
/// Renders the HTML for this component. This will execute the component server-side and
/// return the rendered HTML.
Expand Down
5 changes: 3 additions & 2 deletions src/React.Core/IReactEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -81,8 +81,9 @@ public interface IReactEnvironment
/// <param name="props">Props to use</param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <param name="serverOnly">True if this component only should be rendered server-side. Defaults to false.</param>
/// <returns>The component</returns>
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false);
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false);

/// <summary>
/// Adds the provided <see cref="IReactComponent"/> to the list of components to render client side.
Expand Down
10 changes: 10 additions & 0 deletions src/React.Core/ReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public class ReactComponent : IReactComponent
/// </summary>
public string ContainerClass { get; set; }

/// <summary>
/// Get or sets if this components only should be rendered server side
/// </summary>
public bool ServerOnly { get; set; }

/// <summary>
/// Gets or sets the props for this component
/// </summary>
Expand Down Expand Up @@ -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<string>(reactRenderCommand);

if (renderServerOnly)
{
return html;
}
}
catch (JsRuntimeException ex)
{
Expand Down
15 changes: 10 additions & 5 deletions src/React.Core/ReactEnvironment.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -287,8 +287,9 @@ public virtual bool HasVariable(string name)
/// <param name="props">Props to use</param>
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <param name="serverOnly">True if this component only should be rendered server-side. Defaults to false.</param>
/// <returns>The component</returns>
public virtual IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false)
public virtual IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false, bool serverOnly = false)
{
if (!clientOnly)
{
Expand All @@ -297,7 +298,8 @@ public virtual IReactComponent CreateComponent<T>(string componentName, T props,

var component = new ReactComponent(this, _config, componentName, containerId)
{
Props = props
Props = props,
ServerOnly = serverOnly
};
_components.Add(component);
return component;
Expand Down Expand Up @@ -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();
Expand Down
39 changes: 39 additions & 0 deletions tests/React.Tests/Core/ReactComponentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,45 @@ public void RenderHtmlShouldWrapComponentInCustomElement()
Assert.Equal(@"<span id=""container"">[HTML]</span>", result);
}

[Fact]
public void RenderHtmlShouldNotRenderComponentWhenContainerOnly()
{
var config = new Mock<IReactSiteConfiguration>();
config.Setup(x => x.UseServerSideRendering).Returns(true);
var environment = new Mock<IReactEnvironment>();
environment.Setup(x => x.Execute<bool>("typeof Foo !== 'undefined'")).Returns(true);
environment.Setup(x => x.Execute<string>(@"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(@"<span id=""container""></span>", result);
}

[Fact]
public void RenderHtmlShouldNotWrapComponentWhenServerSideOnly()
{
var config = new Mock<IReactSiteConfiguration>();
config.Setup(x => x.UseServerSideRendering).Returns(true);
var environment = new Mock<IReactEnvironment>();
environment.Setup(x => x.Execute<bool>("typeof Foo !== 'undefined'")).Returns(true);
environment.Setup(x => x.Execute<string>(@"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()
{
Expand Down
13 changes: 12 additions & 1 deletion tests/React.Tests/Core/ReactEnvironmentTest.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* Copyright (c) 2014-Present, Facebook, Inc.
* All rights reserved.
*
Expand Down Expand Up @@ -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<PooledJsEngine> Engine { get; private set; }
Expand Down
35 changes: 20 additions & 15 deletions tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
*/

using Moq;
using React.Web.Mvc;
using Xunit;
using React.Web.Mvc;

namespace React.Tests.Mvc
{
public class HtmlHelperExtensionsTests
{
{
/// <summary>
/// Creates a mock <see cref="IReactEnvironment"/> and registers it with the IoC container
/// This is only required because <see cref="HtmlHelperExtensions"/> can not be
Expand All @@ -36,8 +36,9 @@ public void ReactWithInitShouldReturnHtmlAndScript()
var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
"ComponentName",
new {},
new { },
null,
false,
false
)).Returns(component.Object);

Expand All @@ -63,7 +64,8 @@ public void EngineIsReturnedToPoolAfterRender()
"ComponentName",
new { },
null,
true
true,
false
)).Returns(component.Object);

environment.Verify(x => x.ReturnEngineToPool(), Times.Never);
Expand All @@ -73,9 +75,9 @@ public void EngineIsReturnedToPoolAfterRender()
props: new { },
htmlTag: "span",
clientOnly: true,
serverOnly: true
serverOnly: false
);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
environment.Verify(x => x.ReturnEngineToPool(), Times.Once);
}

Expand All @@ -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(
Expand All @@ -98,20 +101,22 @@ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
props: new { },
htmlTag: "span",
clientOnly: true,
serverOnly: true
serverOnly: false
);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == true), It.Is<bool>(z => z == false), null), Times.Once);
}

[Fact]
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue() {
public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
{
var component = new Mock<IReactComponent>();
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);

Expand All @@ -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<bool>(y => y == true), It.Is<bool>(z => z == true), null), Times.Once);
component.Verify(x => x.RenderHtml(It.Is<bool>(y => y == false), It.Is<bool>(z => z == true), null), Times.Once);
}
}
}
}