Skip to content

Commit 6d96b4f

Browse files
Html.ReactWithInit improvements (#858)
* Lazily initialize component JS when using Html.ReactWithInit * Make Html.ReactWithInit consistent with Html.React * Fix logic error and add a few tests
1 parent 60a61e9 commit 6d96b4f

9 files changed

+131
-30
lines changed

src/React.AspNet/HtmlHelperExtensions.cs

+8-4
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,10 @@ public static IHtmlString React<T>(
104104
/// <param name="htmlTag">HTML tag to wrap the component in. Defaults to &lt;div&gt;</param>
105105
/// <param name="containerId">ID to use for the container HTML tag. Defaults to an auto-generated ID</param>
106106
/// <param name="clientOnly">Skip rendering server-side and only output client-side initialisation code. Defaults to <c>false</c></param>
107+
/// <param name="serverOnly">Skip rendering React specific data-attributes, container and client-side initialisation during server side rendering. Defaults to <c>false</c></param>
107108
/// <param name="containerClass">HTML class(es) to set on the container tag</param>
108109
/// <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>
110+
/// <param name="renderFunctions">Functions to call during component render</param>
109111
/// <returns>The component's HTML</returns>
110112
public static IHtmlString ReactWithInit<T>(
111113
this IHtmlHelper htmlHelper,
@@ -114,8 +116,10 @@ public static IHtmlString ReactWithInit<T>(
114116
string htmlTag = null,
115117
string containerId = null,
116118
bool clientOnly = false,
119+
bool serverOnly = false,
117120
string containerClass = null,
118-
Action<Exception, string, string> exceptionHandler = null
121+
Action<Exception, string, string> exceptionHandler = null,
122+
IRenderFunctions renderFunctions = null
119123
)
120124
{
121125
try
@@ -133,11 +137,11 @@ public static IHtmlString ReactWithInit<T>(
133137

134138
return RenderToString(writer =>
135139
{
136-
reactComponent.RenderHtml(writer, clientOnly, exceptionHandler: exceptionHandler);
140+
reactComponent.RenderHtml(writer, clientOnly, serverOnly, exceptionHandler: exceptionHandler, renderFunctions);
137141
writer.WriteLine();
138-
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter));
142+
WriteScriptTag(writer, bodyWriter => reactComponent.RenderJavaScript(bodyWriter, waitForDOMContentLoad: true));
139143
});
140-
144+
141145
}
142146
finally
143147
{

src/React.Core/IReactComponent.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,6 @@ public interface IReactComponent
5656
/// <returns>HTML</returns>
5757
string RenderHtml(bool renderContainerOnly = false, bool renderServerOnly = false, Action<Exception, string, string> exceptionHandler = null, IRenderFunctions renderFunctions = null);
5858

59-
/// <summary>
60-
/// Renders the JavaScript required to initialise this component client-side. This will
61-
/// initialise the React component, which includes attach event handlers to the
62-
/// server-rendered HTML.
63-
/// </summary>
64-
/// <returns>JavaScript</returns>
65-
string RenderJavaScript();
66-
6759
/// <summary>
6860
/// Renders the HTML for this component. This will execute the component server-side and
6961
/// return the rendered HTML.
@@ -82,6 +74,14 @@ public interface IReactComponent
8274
/// server-rendered HTML.
8375
/// </summary>
8476
/// <returns>JavaScript</returns>
85-
void RenderJavaScript(TextWriter writer);
77+
string RenderJavaScript(bool waitForDOMContentLoad);
78+
79+
/// <summary>
80+
/// Renders the JavaScript required to initialise this component client-side. This will
81+
/// initialise the React component, which includes attach event handlers to the
82+
/// server-rendered HTML.
83+
/// </summary>
84+
/// <returns>JavaScript</returns>
85+
void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad);
8686
}
8787
}

src/React.Core/ReactComponent.cs

+14-3
Original file line numberDiff line numberDiff line change
@@ -231,9 +231,9 @@ public virtual void RenderHtml(TextWriter writer, bool renderContainerOnly = fal
231231
/// server-rendered HTML.
232232
/// </summary>
233233
/// <returns>JavaScript</returns>
234-
public virtual string RenderJavaScript()
234+
public virtual string RenderJavaScript(bool waitForDOMContentLoad)
235235
{
236-
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter));
236+
return GetStringFromWriter(renderJsWriter => RenderJavaScript(renderJsWriter, waitForDOMContentLoad));
237237
}
238238

239239
/// <summary>
@@ -242,15 +242,26 @@ public virtual string RenderJavaScript()
242242
/// server-rendered HTML.
243243
/// </summary>
244244
/// <param name="writer">The <see cref="T:System.IO.TextWriter" /> to which the content is written</param>
245+
/// <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>
245246
/// <returns>JavaScript</returns>
246-
public virtual void RenderJavaScript(TextWriter writer)
247+
public virtual void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad)
247248
{
249+
if (waitForDOMContentLoad)
250+
{
251+
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
252+
}
253+
248254
writer.Write(
249255
!_configuration.UseServerSideRendering || ClientOnly ? "ReactDOM.render(" : "ReactDOM.hydrate(");
250256
WriteComponentInitialiser(writer);
251257
writer.Write(", document.getElementById(\"");
252258
writer.Write(ContainerId);
253259
writer.Write("\"))");
260+
261+
if (waitForDOMContentLoad)
262+
{
263+
writer.Write("});");
264+
}
254265
}
255266

256267
/// <summary>

src/React.Core/ReactEnvironment.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ public virtual void GetInitJavaScript(TextWriter writer, bool clientOnly = false
342342
{
343343
if (!component.ServerOnly)
344344
{
345-
component.RenderJavaScript(writer);
345+
component.RenderJavaScript(writer, waitForDOMContentLoad: false);
346346
writer.WriteLine(';');
347347
}
348348
}

src/React.Router/ReactRouterComponent.cs

+11-1
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,23 @@ protected override void WriteComponentInitialiser(TextWriter writer)
9393
/// Client side React Router does not need context nor explicit path parameter.
9494
/// </summary>
9595
/// <returns>JavaScript</returns>
96-
public override void RenderJavaScript(TextWriter writer)
96+
public override void RenderJavaScript(TextWriter writer, bool waitForDOMContentLoad)
9797
{
98+
if (waitForDOMContentLoad)
99+
{
100+
writer.Write("window.addEventListener('DOMContentLoaded', function() {");
101+
}
102+
98103
writer.Write("ReactDOM.hydrate(");
99104
base.WriteComponentInitialiser(writer);
100105
writer.Write(", document.getElementById(\"");
101106
writer.Write(ContainerId);
102107
writer.Write("\"))");
108+
109+
if (waitForDOMContentLoad)
110+
{
111+
writer.Write("});");
112+
}
103113
}
104114
}
105115
}

tests/React.Tests/Core/ReactComponentTest.cs

+29-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
using System;
9+
using System.IO;
910
using JavaScriptEngineSwitcher.Core;
1011
using Moq;
1112
using React.Exceptions;
@@ -204,7 +205,7 @@ public void RenderJavaScriptShouldCallRenderComponent()
204205
{
205206
Props = new { hello = "World" }
206207
};
207-
var result = component.RenderJavaScript();
208+
var result = component.RenderJavaScript(false);
208209

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

229230
Assert.Equal(
230231
@"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
@@ -244,7 +245,7 @@ public void RenderJavaScriptShouldCallRenderComponentwithReactDOMHydrate()
244245
ClientOnly = false,
245246
Props = new { hello = "World" }
246247
};
247-
var result = component.RenderJavaScript();
248+
var result = component.RenderJavaScript(false);
248249

249250
Assert.Equal(
250251
@"ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
@@ -265,14 +266,37 @@ public void RenderJavaScriptShouldCallRenderComponentWithReactDomRenderWhenSsrDi
265266
ClientOnly = false,
266267
Props = new {hello = "World"}
267268
};
268-
var result = component.RenderJavaScript();
269-
269+
var result = component.RenderJavaScript(false);
270+
270271
Assert.Equal(
271272
@"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
272273
result
273274
);
274275
}
275276

277+
[Fact]
278+
public void RenderJavaScriptShouldHandleWaitForContentLoad()
279+
{
280+
var environment = new Mock<IReactEnvironment>();
281+
var config = CreateDefaultConfigMock();
282+
config.SetupGet(x => x.UseServerSideRendering).Returns(false);
283+
284+
var reactIdGenerator = new Mock<IReactIdGenerator>();
285+
var component = new ReactComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container")
286+
{
287+
ClientOnly = false,
288+
Props = new {hello = "World"}
289+
};
290+
using (var writer = new StringWriter())
291+
{
292+
component.RenderJavaScript(writer, waitForDOMContentLoad: true);
293+
Assert.Equal(
294+
@"window.addEventListener('DOMContentLoaded', function() {ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))});",
295+
writer.ToString()
296+
);
297+
}
298+
}
299+
276300
[Theory]
277301
[InlineData("Foo", true)]
278302
[InlineData("Foo.Bar", true)]

tests/React.Tests/Core/ReactEnvironmentTest.cs

+16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using Moq;
1414
using Xunit;
1515
using React.Exceptions;
16+
using System.IO;
1617

1718
namespace React.Tests.Core
1819
{
@@ -125,6 +126,21 @@ public void CreatesIReactComponent()
125126
Assert.Equal(";" + Environment.NewLine, environment.GetInitJavaScript());
126127
}
127128

129+
[Fact]
130+
public void GetInitJavaScript()
131+
{
132+
var mocks = new Mocks();
133+
var environment = mocks.CreateReactEnvironment();
134+
135+
var component = new Mock<IReactComponent>();
136+
137+
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();
138+
139+
environment.CreateComponent(component.Object);
140+
141+
Assert.Equal("JS;" + Environment.NewLine, environment.GetInitJavaScript());
142+
}
143+
128144
[Fact]
129145
public void ServerSideOnlyComponentRendersNoJavaScript()
130146
{

tests/React.Tests/Mvc/HtmlHelperExtensionsTests.cs

+23-6
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public void ReactWithInitShouldReturnHtmlAndScript()
3838
component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null, null))
3939
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler, IRenderFunctions renderFunctions) => writer.Write("HTML"));
4040

41-
component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS"));
41+
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"));
4242

4343
var environment = ConfigureMockEnvironment();
4444
environment.Setup(x => x.CreateComponent(
@@ -57,11 +57,28 @@ public void ReactWithInitShouldReturnHtmlAndScript()
5757
).ToHtmlString();
5858

5959
Assert.Equal(
60-
"HTML" + System.Environment.NewLine + "<script>JS</script>",
60+
"HTML" + System.Environment.NewLine + "<script>waiting for page load JS</script>",
6161
result.ToString()
6262
);
6363
}
6464

65+
[Fact]
66+
public void GetInitJavaScriptReturns()
67+
{
68+
var component = new Mock<IReactComponent>();
69+
70+
var environment = ConfigureMockEnvironment();
71+
72+
environment.Setup(x => x.GetInitJavaScript(It.IsAny<TextWriter>(), It.IsAny<bool>())).Callback((TextWriter writer, bool clientOnly) => writer.Write("JS"));
73+
74+
var renderJSResult = HtmlHelperExtensions.ReactInitJavaScript(htmlHelper: null, clientOnly: false);
75+
76+
Assert.Equal(
77+
"<script>JS</script>",
78+
renderJSResult.ToString()
79+
);
80+
}
81+
6582
[Fact]
6683
public void ScriptNonceIsReturned()
6784
{
@@ -77,7 +94,7 @@ public void ScriptNonceIsReturned()
7794
component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), false, false, null, null))
7895
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandle, IRenderFunctions renderFunctions) => writer.Write("HTML")).Verifiable();
7996

80-
component.Setup(x => x.RenderJavaScript(It.IsAny<TextWriter>())).Callback((TextWriter writer) => writer.Write("JS")).Verifiable();
97+
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();
8198

8299
var config = new Mock<IReactSiteConfiguration>();
83100

@@ -101,7 +118,7 @@ public void ScriptNonceIsReturned()
101118
).ToHtmlString();
102119

103120
Assert.Equal(
104-
"HTML" + System.Environment.NewLine + "<script>JS</script>",
121+
"HTML" + System.Environment.NewLine + "<script>waiting for page load JS</script>",
105122
result.ToString()
106123
);
107124

@@ -116,7 +133,7 @@ public void ScriptNonceIsReturned()
116133
).ToHtmlString();
117134

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

219236
component.Setup(x => x.RenderHtml(It.IsAny<TextWriter>(), It.IsAny<bool>(), It.IsAny<bool>(), It.IsAny<Action<Exception, string, string>>(), It.IsAny<IRenderFunctions>()))
220-
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler, IRenderFunctions renderFunctions) =>
237+
.Callback((TextWriter writer, bool renderContainerOnly, bool renderServerOnly, Action<Exception, string, string> exceptionHandler, IRenderFunctions renderFunctions) =>
221238
{
222239
renderFunctions.PreRender(_ => "one");
223240
writer.Write(renderFunctions.TransformRenderedHtml("HTML"));

tests/React.Tests/Router/ReactRouterComponentTest.cs

+20-1
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,32 @@ public void RenderJavaScriptShouldNotIncludeContextOrPath()
2727
{
2828
Props = new { hello = "World" }
2929
};
30-
var result = component.RenderJavaScript();
30+
var result = component.RenderJavaScript(false);
3131

3232
Assert.Equal(
3333
@"ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
3434
result
3535
);
3636
}
37+
38+
[Fact]
39+
public void RenderJavaScriptShouldHandleWaitForContentLoad()
40+
{
41+
var environment = new Mock<IReactEnvironment>();
42+
var config = new Mock<IReactSiteConfiguration>();
43+
var reactIdGenerator = new Mock<IReactIdGenerator>();
44+
45+
var component = new ReactRouterComponent(environment.Object, config.Object, reactIdGenerator.Object, "Foo", "container", "/bar")
46+
{
47+
Props = new { hello = "World" }
48+
};
49+
var result = component.RenderJavaScript(true);
50+
51+
Assert.Equal(
52+
@"window.addEventListener('DOMContentLoaded', function() {ReactDOM.hydrate(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))});",
53+
result
54+
);
55+
}
3756
}
3857
}
3958
#endif

0 commit comments

Comments
 (0)