From 30ac8d52c6a8e69327c3f6500bef107584f78788 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 5 Jul 2019 00:58:58 -0400 Subject: [PATCH 1/3] Lazily initialize component JS when using Html.ReactWithInit --- src/React.AspNet/HtmlHelperExtensions.cs | 2 +- src/React.Core/IReactComponent.cs | 18 ++++++------- src/React.Core/ReactComponent.cs | 15 +++++++++-- src/React.Core/ReactEnvironment.cs | 2 +- src/React.Router/ReactRouterComponent.cs | 12 ++++++++- tests/React.Tests/Core/ReactComponentTest.cs | 26 ++++++++++++++++++- .../Mvc/HtmlHelperExtensionsTests.cs | 6 ++--- 7 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index bf427a7e1..afc5ab3b7 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -135,7 +135,7 @@ public static IHtmlString ReactWithInit( { reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler); writer.WriteLine(); - WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter)); + WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter, waitForDOMContentLoad: true)); }); } diff --git a/src/React.Core/IReactComponent.cs b/src/React.Core/IReactComponent.cs index 3ba68a128..176638f54 100644 --- a/src/React.Core/IReactComponent.cs +++ b/src/React.Core/IReactComponent.cs @@ -56,14 +56,6 @@ public interface IReactComponent /// HTML string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action exceptionHandler = null, IRenderFunctions renderFunctions = null); - /// - /// 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. - /// - /// JavaScript - string RenderJavaScript(); - /// /// Renders the HTML for this component. This will execute the component server-side and /// return the rendered HTML. @@ -82,6 +74,14 @@ public interface IReactComponent /// server-rendered HTML. /// /// JavaScript - void RenderJavaScript(TextWriter writer); + string RenderJavaScript(); + + /// + /// 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. + /// + /// JavaScript + void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad); } } diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index 6ad394361..119ecf3b4 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -233,7 +233,7 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal /// JavaScript public virtual string RenderJavaScript() { - return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter)); + return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter, waitForDOMContentLoad: false)); } /// @@ -242,15 +242,26 @@ public virtual string RenderJavaScript() /// server-rendered HTML. /// /// The to which the content is written + /// Delays the component init until the page load event fires. Useful if the component script tags are located after the call to Html.ReactWithInit. /// JavaScript - 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("});"); + } } /// diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs index 9609aa29a..c5ce56d84 100644 --- a/src/React.Core/ReactEnvironment.cs +++ b/src/React.Core/ReactEnvironment.cs @@ -342,7 +342,7 @@ public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false { if (!component.ServerOnly) { - component.RenderJavaScript(writer); + component.RenderJavaScript(writer, waitForDOMContentLoad: true); writer.WriteLine(';'); } } diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs index 6cfe54858..932b69652 100644 --- a/src/React.Router/ReactRouterComponent.cs +++ b/src/React.Router/ReactRouterComponent.cs @@ -93,13 +93,23 @@ protected override void WriteComponentInitialiser(TextWriter writer) /// Client side React Router does not need context nor explicit path parameter. /// /// JavaScript - 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("});"); + } } } } diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs index 982bf9ba2..651a2e4e5 100644 --- a/tests/React.Tests/Core/ReactComponentTest.cs +++ b/tests/React.Tests/Core/ReactComponentTest.cs @@ -6,6 +6,7 @@ */ using System; +using System.IO; using JavaScriptEngineSwitcher.Core; using Moq; using React.Exceptions; @@ -266,13 +267,36 @@ public void RenderJavaScriptShouldCallRenderComponentWithReactDomRenderWhenSsrDi Props = new {hello = "World"} }; var result = component.RenderJavaScript(); - + Assert.Equal( @"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))", result ); } + [Fact] + public void RenderJavaScriptShouldHandleWaitForContentLoad() + { + var environment = new Mock(); + var config = CreateDefaultConfigMock(); + config.SetupGet(x => x.UseServerSideRendering).Returns(false); + + var reactIdGenerator = new Mock(); + 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)] diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs index a24015c9c..22d92213c 100644 --- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs +++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs @@ -38,7 +38,7 @@ public void ReactWithInitShouldReturnHtmlAndScript() component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null, null)) .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler, IRenderFunctions renderFunctions) => writer.Write("HTML")); - component.Setup(x => x.RenderJavaScript(It.IsAny())).Callback((TextWriter writer) => writer.Write("JS")); + component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write("JS")); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( @@ -77,7 +77,7 @@ public void ScriptNonceIsReturned() component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null, null)) .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandle, IRenderFunctions renderFunctions) => writer.Write("HTML")).Verifiable(); - component.Setup(x => x.RenderJavaScript(It.IsAny())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable(); + component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write("JS")).Verifiable(); var config = new Mock(); @@ -217,7 +217,7 @@ public void RenderFunctionsCalledNonLazily() fakeRenderFunctions.Setup(x => x.TransformRenderedHtml(It.IsAny())).Returns("HTML"); component.Setup(x => x.RenderHtml(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny>(), It.IsAny())) - .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler, IRenderFunctions renderFunctions) => + .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler, IRenderFunctions renderFunctions) => { renderFunctions.PreRender(_ => "one"); writer.Write(renderFunctions.TransformRenderedHtml("HTML")); From 3f09a289ce7ce31c63d0c4eb7b2687503a989358 Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 5 Jul 2019 01:01:32 -0400 Subject: [PATCH 2/3] Make Html.ReactWithInit consistent with Html.React --- src/React.AspNet/HtmlHelperExtensions.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs index afc5ab3b7..d40fb32ef 100644 --- a/src/React.AspNet/HtmlHelperExtensions.cs +++ b/src/React.AspNet/HtmlHelperExtensions.cs @@ -104,8 +104,10 @@ public static IHtmlString React( /// 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, 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) + /// Functions to call during component render /// The component's HTML public static IHtmlString ReactWithInit( this IHtmlHelper htmlHelper, @@ -114,8 +116,10 @@ public static IHtmlString ReactWithInit( string htmlTag = null, string containerId = null, bool clientOnly = false, + bool serverOnly = false, string containerClass = null, - Action exceptionHandler = null + Action exceptionHandler = null, + IRenderFunctions renderFunctions = null ) { try @@ -133,11 +137,11 @@ public static IHtmlString ReactWithInit( 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, waitForDOMContentLoad: true)); }); - + } finally { From 71d68a7baea4861af0e49ea65745879e90083cbb Mon Sep 17 00:00:00 2001 From: Dustin Masters Date: Fri, 5 Jul 2019 10:08:38 -0400 Subject: [PATCH 3/3] Fix logic error and add a few tests --- src/React.Core/IReactComponent.cs | 2 +- src/React.Core/ReactComponent.cs | 4 +-- src/React.Core/ReactEnvironment.cs | 2 +- tests/React.Tests/Core/ReactComponentTest.cs | 8 +++--- .../React.Tests/Core/ReactEnvironmentTest.cs | 16 +++++++++++ .../Mvc/HtmlHelperExtensionsTests.cs | 27 +++++++++++++++---- .../Router/ReactRouterComponentTest.cs | 21 ++++++++++++++- 7 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/React.Core/IReactComponent.cs b/src/React.Core/IReactComponent.cs index 176638f54..6e4e8127b 100644 --- a/src/React.Core/IReactComponent.cs +++ b/src/React.Core/IReactComponent.cs @@ -74,7 +74,7 @@ public interface IReactComponent /// server-rendered HTML. /// /// JavaScript - string RenderJavaScript(); + string RenderJavaScript(bool waitForDOMContentLoad); /// /// Renders the JavaScript required to initialise this component client-side. This will diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs index 119ecf3b4..9b54f10ff 100644 --- a/src/React.Core/ReactComponent.cs +++ b/src/React.Core/ReactComponent.cs @@ -231,9 +231,9 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal /// server-rendered HTML. /// /// JavaScript - public virtual string RenderJavaScript() + public virtual string RenderJavaScript(bool waitForDOMContentLoad) { - return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter, waitForDOMContentLoad: false)); + return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter, waitForDOMContentLoad)); } /// diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs index c5ce56d84..9c1a10db5 100644 --- a/src/React.Core/ReactEnvironment.cs +++ b/src/React.Core/ReactEnvironment.cs @@ -342,7 +342,7 @@ public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false { if (!component.ServerOnly) { - component.RenderJavaScript(writer, waitForDOMContentLoad: true); + component.RenderJavaScript(writer, waitForDOMContentLoad: false); writer.WriteLine(';'); } } diff --git a/tests/React.Tests/Core/ReactComponentTest.cs b/tests/React.Tests/Core/ReactComponentTest.cs index 651a2e4e5..1a0cbc93f 100644 --- a/tests/React.Tests/Core/ReactComponentTest.cs +++ b/tests/React.Tests/Core/ReactComponentTest.cs @@ -205,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""))", @@ -225,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""))", @@ -245,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""))", @@ -266,7 +266,7 @@ 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""))", diff --git a/tests/React.Tests/Core/ReactEnvironmentTest.cs b/tests/React.Tests/Core/ReactEnvironmentTest.cs index 5253f1ffe..4eab8bf5d 100644 --- a/tests/React.Tests/Core/ReactEnvironmentTest.cs +++ b/tests/React.Tests/Core/ReactEnvironmentTest.cs @@ -13,6 +13,7 @@ using Moq; using Xunit; using React.Exceptions; +using System.IO; namespace React.Tests.Core { @@ -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(); + + component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).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() { diff --git a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs index 22d92213c..b08ceffe5 100644 --- a/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs +++ b/tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs @@ -38,7 +38,7 @@ public void ReactWithInitShouldReturnHtmlAndScript() component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null, null)) .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandler, IRenderFunctions renderFunctions) => writer.Write("HTML")); - component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write("JS")); + component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write(waitForDOMContentLoad ? "waiting for page load JS" : "JS")); var environment = ConfigureMockEnvironment(); environment.Setup(x => x.CreateComponent( @@ -57,11 +57,28 @@ public void ReactWithInitShouldReturnHtmlAndScript() ).ToHtmlString(); Assert.Equal( - "HTML" + System.Environment.NewLine + "", + "HTML" + System.Environment.NewLine + "", result.ToString() ); } + [Fact] + public void GetInitJavaScriptReturns() + { + var component = new Mock(); + + var environment = ConfigureMockEnvironment(); + + environment.Setup(x => x.GetInitJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool clientOnly) => writer.Write("JS")); + + var renderJSResult = HtmlHelperExtensions.ReactInitJavaScript(htmlHelper: null, clientOnly: false); + + Assert.Equal( + "", + renderJSResult.ToString() + ); + } + [Fact] public void ScriptNonceIsReturned() { @@ -77,7 +94,7 @@ public void ScriptNonceIsReturned() component.Setup(x => x.RenderHtml(It.IsAny(), false, false, null, null)) .Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action exceptionHandle, IRenderFunctions renderFunctions) => writer.Write("HTML")).Verifiable(); - component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write("JS")).Verifiable(); + component.Setup(x => x.RenderJavaScript(It.IsAny(), It.IsAny())).Callback((TextWriter writer, bool waitForDOMContentLoad) => writer.Write(waitForDOMContentLoad ? "waiting for page load JS" : "JS")).Verifiable(); var config = new Mock(); @@ -101,7 +118,7 @@ public void ScriptNonceIsReturned() ).ToHtmlString(); Assert.Equal( - "HTML" + System.Environment.NewLine + "", + "HTML" + System.Environment.NewLine + "", result.ToString() ); @@ -116,7 +133,7 @@ public void ScriptNonceIsReturned() ).ToHtmlString(); Assert.Equal( - "HTML" + System.Environment.NewLine + "", + "HTML" + System.Environment.NewLine + "", result.ToString() ); } diff --git a/tests/React.Tests/Router/ReactRouterComponentTest.cs b/tests/React.Tests/Router/ReactRouterComponentTest.cs index 42f43d23d..954aee794 100644 --- a/tests/React.Tests/Router/ReactRouterComponentTest.cs +++ b/tests/React.Tests/Router/ReactRouterComponentTest.cs @@ -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(); + var config = new Mock(); + var reactIdGenerator = new Mock(); + + 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