Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Html.ReactWithInit improvements #858

Merged
merged 3 commits into from
Jul 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/React.AspNet/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,10 @@ public static IHtmlString React<T>(
/// <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, 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,
Expand All @@ -114,8 +116,10 @@ public static IHtmlString ReactWithInit<T>(
string htmlTag = null,
string containerId = null,
bool clientOnly = false,
bool serverOnly = false,
string containerClass = null,
Action<Exception, string, string> exceptionHandler = null
Action<Exception, string, string> exceptionHandler = null,
IRenderFunctions renderFunctions = null
)
{
try
Expand All @@ -133,11 +137,11 @@ public static IHtmlString ReactWithInit<T>(

return RenderToString(writer =>
{
reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler: exceptionHandler, renderFunctions);
writer.WriteLine();
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter, waitForDOMContentLoad: true));
});

}
finally
{
Expand Down
18 changes: 9 additions & 9 deletions src/React.Core/IReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,6 @@ public interface IReactComponent
/// <returns>HTML</returns>
string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, IRenderFunctions renderFunctions = null);

/// <summary>
/// Renders the JavaScript required to initialise this component client-side. This will
/// initialise the React component, which includes attach event handlers to the
/// server-rendered HTML.
/// </summary>
/// <returns>JavaScript</returns>
string RenderJavaScript();

/// <summary>
/// Renders the HTML for this component. This will execute the component server-side and
/// return the rendered HTML.
Expand All @@ -82,6 +74,14 @@ public interface IReactComponent
/// server-rendered HTML.
/// </summary>
/// <returns>JavaScript</returns>
void RenderJavaScript(TextWriter writer);
string RenderJavaScript(bool waitForDOMContentLoad);

/// <summary>
/// Renders the JavaScript required to initialise this component client-side. This will
/// initialise the React component, which includes attach event handlers to the
/// server-rendered HTML.
/// </summary>
/// <returns>JavaScript</returns>
void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad);
}
}
17 changes: 14 additions & 3 deletions src/React.Core/ReactComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,9 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal
/// server-rendered HTML.
/// </summary>
/// <returns>JavaScript</returns>
public virtual string RenderJavaScript()
public virtual string RenderJavaScript(bool waitForDOMContentLoad)
{
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter));
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter, waitForDOMContentLoad));
}

/// <summary>
Expand All @@ -242,15 +242,26 @@ public virtual string RenderJavaScript()
/// server-rendered HTML.
/// </summary>
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
/// <param name="waitForDOMContentLoad">Delays the component init until the page load event fires. Useful if the component script tags are located after the call to Html.ReactWithInit. </param>
/// <returns>JavaScript</returns>
public virtual void RenderJavaScript(TextWriter writer)
public virtual void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad)
{
if (waitForDOMContentLoad)
{
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
}

writer.Write(
!_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
WriteComponentInitialiser(writer);
writer.Write(", document.getElementById(\"");
writer.Write(ContainerId);
writer.Write("\"))");

if (waitForDOMContentLoad)
{
writer.Write("});");
}
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/React.Core/ReactEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false
{
if (!component.ServerOnly)
{
component.RenderJavaScript(writer);
component.RenderJavaScript(writer, waitForDOMContentLoad: false);
writer.WriteLine(';');
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/React.Router/ReactRouterComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,13 +93,23 @@ protected override void WriteComponentInitialiser(TextWriter writer)
/// Client side React Router does not need context nor explicit path parameter.
/// </summary>
/// <returns>JavaScript</returns>
public override void RenderJavaScript(TextWriter writer)
public override void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad)
{
if (waitForDOMContentLoad)
{
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
}

writer.Write("ReactDOM.hydrate(");
base.WriteComponentInitialiser(writer);
writer.Write(", document.getElementById(\"");
writer.Write(ContainerId);
writer.Write("\"))");

if (waitForDOMContentLoad)
{
writer.Write("});");
}
}
}
}
34 changes: 29 additions & 5 deletions tests/React.Tests/Core/ReactComponentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

using System;
using System.IO;
using JavaScriptEngineSwitcher.Core;
using Moq;
using React.Exceptions;
Expand Down Expand Up @@ -204,7 +205,7 @@ public void RenderJavaScriptShouldCallRenderComponent()
{
Props = new { hello = "World" }
};
var result = component.RenderJavaScript();
var result = component.RenderJavaScript(false);

Assert.Equal(
@"ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
Expand All @@ -224,7 +225,7 @@ public void RenderJavaScriptShouldCallRenderComponentWithReactDOMRender()
ClientOnly = true,
Props = new { hello = "World" }
};
var result = component.RenderJavaScript();
var result = component.RenderJavaScript(false);

Assert.Equal(
@"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
Expand All @@ -244,7 +245,7 @@ public void RenderJavaScriptShouldCallRenderComponentwithReactDOMHydrate()
ClientOnly = false,
Props = new { hello = "World" }
};
var result = component.RenderJavaScript();
var result = component.RenderJavaScript(false);

Assert.Equal(
@"ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
Expand All @@ -265,14 +266,37 @@ public void RenderJavaScriptShouldCallRenderComponentWithReactDomRenderWhenSsrDi
ClientOnly = false,
Props = new {hello = "World"}
};
var result = component.RenderJavaScript();
var result = component.RenderJavaScript(false);

Assert.Equal(
@"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
result
);
}

[Fact]
public void RenderJavaScriptShouldHandleWaitForContentLoad()
{
var environment = new Mock<IReactEnvironment>();
var config = CreateDefaultConfigMock();
config.SetupGet(x => x.UseServerSideRendering).Returns(false);

var reactIdGenerator = new Mock<IReactIdGenerator>();
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
{
ClientOnly = false,
Props = new {hello = "World"}
};
using (var writer = new StringWriter())
{
component.RenderJavaScript(writer, waitForDOMContentLoad: true);
Assert.Equal(
@"window.addEventListener('DOMContentLoaded', function() {ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))});",
writer.ToString()
);
}
}

[Theory]
[InlineData("Foo", true)]
[InlineData("Foo.Bar", true)]
Expand Down
16 changes: 16 additions & 0 deletions tests/React.Tests/Core/ReactEnvironmentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Moq;
using Xunit;
using React.Exceptions;
using System.IO;

namespace React.Tests.Core
{
Expand Down Expand Up @@ -125,6 +126,21 @@ public void CreatesIReactComponent()
Assert.Equal(";" + Environment.NewLine, environment.GetInitJavaScript());
}

[Fact]
public void GetInitJavaScript()
{
var mocks = new Mocks();
var environment = mocks.CreateReactEnvironment();

var component = new Mock<IReactComponent>();

component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>(), It.IsAny<bool>())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write(waitForDOMContentLoad ? "waiting for page load JS" : "JS")).Verifiable();

environment.CreateComponent(component.Object);

Assert.Equal("JS;" + Environment.NewLine, environment.GetInitJavaScript());
}

[Fact]
public void ServerSideOnlyComponentRendersNoJavaScript()
{
Expand Down
29 changes: 23 additions & 6 deletions tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void ReactWithInitShouldReturnHtmlAndScript()
component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null, null))
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler, IRenderFunctions renderFunctions) => writer.Write("HTML"));

component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS"));
component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>(), It.IsAny<bool>())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write(waitForDOMContentLoad ? "waiting for page load JS" : "JS"));

var environment = ConfigureMockEnvironment();
environment.Setup(x => x.CreateComponent(
Expand All @@ -57,11 +57,28 @@ public void ReactWithInitShouldReturnHtmlAndScript()
).ToHtmlString();

Assert.Equal(
"HTML" + System.Environment.NewLine + "<script>JS</script>",
"HTML" + System.Environment.NewLine + "<script>waiting for page load JS</script>",
result.ToString()
);
}

[Fact]
public void GetInitJavaScriptReturns()
{
var component = new Mock<IReactComponent>();

var environment = ConfigureMockEnvironment();

environment.Setup(x => x.GetInitJavaScript(It.IsAny<TextWriter>(), It.IsAny<bool>())).Callback((TextWriter writer, bool clientOnly) => writer.Write("JS"));

var renderJSResult = HtmlHelperExtensions.ReactInitJavaScript(htmlHelper: null, clientOnly: false);

Assert.Equal(
"<script>JS</script>",
renderJSResult.ToString()
);
}

[Fact]
public void ScriptNonceIsReturned()
{
Expand All @@ -77,7 +94,7 @@ public void ScriptNonceIsReturned()
component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null, null))
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandle, IRenderFunctions renderFunctions) => writer.Write("HTML")).Verifiable();

component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable();
component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>(), It.IsAny<bool>())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write(waitForDOMContentLoad ? "waiting for page load JS" : "JS")).Verifiable();

var config = new Mock<IReactSiteConfiguration>();

Expand All @@ -101,7 +118,7 @@ public void ScriptNonceIsReturned()
).ToHtmlString();

Assert.Equal(
"HTML" + System.Environment.NewLine + "<script>JS</script>",
"HTML" + System.Environment.NewLine + "<script>waiting for page load JS</script>",
result.ToString()
);

Expand All @@ -116,7 +133,7 @@ public void ScriptNonceIsReturned()
).ToHtmlString();

Assert.Equal(
"HTML" + System.Environment.NewLine + "<script nonce=\"" + nonce + "\">JS</script>",
"HTML" + System.Environment.NewLine + "<script nonce=\"" + nonce + "\">waiting for page load JS</script>",
result.ToString()
);
}
Expand Down Expand Up @@ -217,7 +234,7 @@ public void RenderFunctionsCalledNonLazily()
fakeRenderFunctions.Setup(x => x.TransformRenderedHtml(It.IsAny<string>())).Returns("HTML");

component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<Action<Exception, string, string>>(), It.IsAny<IRenderFunctions>()))
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler, IRenderFunctions renderFunctions) =>
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler, IRenderFunctions renderFunctions) =>
{
renderFunctions.PreRender(_ => "one");
writer.Write(renderFunctions.TransformRenderedHtml("HTML"));
Expand Down
21 changes: 20 additions & 1 deletion tests/React.Tests/Router/ReactRouterComponentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,32 @@ public void RenderJavaScriptShouldNotIncludeContextOrPath()
{
Props = new { hello = "World" }
};
var result = component.RenderJavaScript();
var result = component.RenderJavaScript(false);

Assert.Equal(
@"ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
result
);
}

[Fact]
public void RenderJavaScriptShouldHandleWaitForContentLoad()
{
var environment = new Mock<IReactEnvironment>();
var config = new Mock<IReactSiteConfiguration>();
var reactIdGenerator = new Mock<IReactIdGenerator>();

var component = new ReactRouterComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container", "/bar")
{
Props = new { hello = "World" }
};
var result = component.RenderJavaScript(true);

Assert.Equal(
@"window.addEventListener('DOMContentLoaded', function() {ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))});",
result
);
}
}
}
#endif