diff --git a/src/.editorconfig b/.editorconfig
similarity index 100%
rename from src/.editorconfig
rename to .editorconfig
diff --git a/build.proj b/build.proj
index 4de1430b7..ea5cdb8a2 100644
--- a/build.proj
+++ b/build.proj
@@ -27,6 +27,7 @@ of patent rights can be found in the PATENTS file in the same directory.
+
diff --git a/src/React.AspNet/HtmlHelperExtensions.cs b/src/React.AspNet/HtmlHelperExtensions.cs
index f1cdd0201..cb6b168c9 100644
--- a/src/React.AspNet/HtmlHelperExtensions.cs
+++ b/src/React.AspNet/HtmlHelperExtensions.cs
@@ -39,22 +39,7 @@ private static IReactEnvironment Environment
{
get
{
- try
- {
- return ReactEnvironment.Current;
- }
- catch (TinyIoCResolutionException ex)
- {
- throw new ReactNotInitialisedException(
-#if LEGACYASPNET
- "ReactJS.NET has not been initialised correctly.",
-#else
- "ReactJS.NET has not been initialised correctly. Please ensure you have " +
- "called services.AddReact() and app.UseReact() in your Startup.cs file.",
-#endif
- ex
- );
- }
+ return ReactEnvironment.GetCurrentOrThrow;
}
}
diff --git a/src/React.Core/IReactEnvironment.cs b/src/React.Core/IReactEnvironment.cs
index f73eb6e42..39fccd721 100644
--- a/src/React.Core/IReactEnvironment.cs
+++ b/src/React.Core/IReactEnvironment.cs
@@ -84,6 +84,14 @@ public interface IReactEnvironment
/// The component
IReactComponent CreateComponent(string componentName, T props, string containerId = null, bool clientOnly = false);
+ ///
+ /// Adds the provided to the list of components to render client side.
+ ///
+ /// Component to add to client side render list
+ /// True if server-side rendering will be bypassed. Defaults to false.
+ /// The component
+ IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false);
+
///
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
diff --git a/src/React.Core/ReactComponent.cs b/src/React.Core/ReactComponent.cs
index 971e4ce4b..140a1bb5b 100644
--- a/src/React.Core/ReactComponent.cs
+++ b/src/React.Core/ReactComponent.cs
@@ -142,7 +142,7 @@ public virtual string RenderHtml(bool renderContainerOnly = false, bool renderSe
attributes,
html,
ContainerTag
- );
+ );
}
catch (JsRuntimeException ex)
{
diff --git a/src/React.Core/ReactEnvironment.cs b/src/React.Core/ReactEnvironment.cs
index 97d8cc296..e3a706812 100644
--- a/src/React.Core/ReactEnvironment.cs
+++ b/src/React.Core/ReactEnvironment.cs
@@ -18,6 +18,7 @@
using JSPool;
using Newtonsoft.Json;
using React.Exceptions;
+using React.TinyIoC;
namespace React
{
@@ -85,6 +86,34 @@ public static IReactEnvironment Current
get { return AssemblyRegistration.Container.Resolve(); }
}
+ ///
+ /// Gets the for the current request. If no environment
+ /// has been created for the current request, creates a new one.
+ /// Also provides more specific error information in the event that ReactJS.NET is misconfigured.
+ ///
+ public static IReactEnvironment GetCurrentOrThrow
+ {
+ get
+ {
+ try
+ {
+ return Current;
+ }
+ catch (TinyIoCResolutionException ex)
+ {
+ throw new ReactNotInitialisedException(
+#if NET451
+ "ReactJS.NET has not been initialised correctly.",
+#else
+ "ReactJS.NET has not been initialised correctly. Please ensure you have " +
+ "called services.AddReact() and app.UseReact() in your Startup.cs file.",
+#endif
+ ex
+ );
+ }
+ }
+ }
+
///
/// Initializes a new instance of the class.
///
@@ -274,6 +303,23 @@ public virtual IReactComponent CreateComponent(string componentName, T props,
return component;
}
+ ///
+ /// Adds the provided to the list of components to render client side.
+ ///
+ /// Component to add to client side render list
+ /// True if server-side rendering will be bypassed. Defaults to false.
+ /// The component
+ public virtual IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false)
+ {
+ if (!clientOnly)
+ {
+ EnsureUserScriptsLoaded();
+ }
+
+ _components.Add(component);
+ return component;
+ }
+
///
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
diff --git a/src/React.Core/Resources/shims.js b/src/React.Core/Resources/shims.js
index 2ac285f21..ba2edb908 100644
--- a/src/React.Core/Resources/shims.js
+++ b/src/React.Core/Resources/shims.js
@@ -67,3 +67,31 @@ function ReactNET_initReact() {
// :'(
return false;
}
+
+/**
+ * Polyfill for engines that do not support Object.assign
+ */
+if (typeof Object.assign !== 'function') {
+ Object.assign = function (target, varArgs) { // .length of function is 2
+ 'use strict';
+ if (target == null) { // TypeError if undefined or null
+ throw new TypeError('Cannot convert undefined or null to object');
+ }
+
+ var to = Object(target);
+
+ for (var index = 1; index < arguments.length; index++) {
+ var nextSource = arguments[index];
+
+ if (nextSource != null) { // Skip over if undefined or null
+ for (var nextKey in nextSource) {
+ // Avoid bugs when hasOwnProperty is shadowed
+ if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+ to[nextKey] = nextSource[nextKey];
+ }
+ }
+ }
+ }
+ return to;
+ };
+}
\ No newline at end of file
diff --git a/src/React.Router/Content/Views/web.config.transform b/src/React.Router/Content/Views/web.config.transform
new file mode 100644
index 000000000..c562609d3
--- /dev/null
+++ b/src/React.Router/Content/Views/web.config.transform
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/React.Router/ExecutionResult.cs b/src/React.Router/ExecutionResult.cs
new file mode 100644
index 000000000..ade20da8a
--- /dev/null
+++ b/src/React.Router/ExecutionResult.cs
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+namespace React.Router
+{
+ ///
+ /// Contains the context object used during execution in addition to
+ /// the string result of rendering the React Router component.
+ ///
+ public class ExecutionResult
+ {
+ ///
+ /// String result of ReactDOMServer render of provided component.
+ ///
+ public string RenderResult { get; set; }
+
+ ///
+ /// Context object used during JS engine execution.
+ ///
+ public RoutingContext Context { get; set; }
+ }
+}
diff --git a/src/React.Router/HtmlHelperExtensions.cs b/src/React.Router/HtmlHelperExtensions.cs
new file mode 100644
index 000000000..cabd14c6f
--- /dev/null
+++ b/src/React.Router/HtmlHelperExtensions.cs
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+using System;
+using React.Exceptions;
+using React.TinyIoC;
+
+#if NET451
+using System.Web;
+using System.Web.Mvc;
+using HttpResponse = System.Web.HttpResponseBase;
+using IHtmlHelper = System.Web.Mvc.HtmlHelper;
+#else
+using Microsoft.AspNetCore.Mvc.Rendering;
+using IHtmlString = Microsoft.AspNetCore.Html.IHtmlContent;
+using HttpResponse = Microsoft.AspNetCore.Http.HttpResponse;
+using Microsoft.AspNetCore.Html;
+#endif
+
+namespace React.Router
+{
+ ///
+ /// Render a React StaticRouter Component with context.
+ ///
+ public static class HtmlHelperExtensions
+ {
+ ///
+ /// Gets the React environment
+ ///
+ private static IReactEnvironment Environment
+ {
+ get
+ {
+ return ReactEnvironment.GetCurrentOrThrow;
+ }
+ }
+
+ ///
+ /// Render a React StaticRouter Component with context object.
+ /// Can optionally be provided with a custom context handler to handle the various status codes.
+ ///
+ /// MVC Razor
+ /// Name of React Static Router component. Expose component globally to ReactJS.NET
+ /// Props to initialise the component with
+ /// F.x. from Request.Path. Used by React Static Router to determine context and routing.
+ /// Optional custom context handler, can be used instead of providing a Response object
+ /// 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
+ /// HTML class(es) to set on the container tag
+ /// containing the rendered markup for provided React Router component
+ public static IHtmlString ReactRouterWithContext(
+ this IHtmlHelper htmlHelper,
+ string componentName,
+ T props,
+ string path = null,
+ string htmlTag = null,
+ string containerId = null,
+ bool clientOnly = false,
+ bool serverOnly = false,
+ string containerClass = null,
+ Action contextHandler = null
+ )
+ {
+ try
+ {
+ var response = htmlHelper.ViewContext.HttpContext.Response;
+ path = path ?? htmlHelper.ViewContext.HttpContext.Request.Path;
+
+ var reactComponent
+ = Environment.CreateRouterComponent(
+ componentName,
+ props,
+ path,
+ containerId,
+ clientOnly
+ );
+
+ if (!string.IsNullOrEmpty(htmlTag))
+ {
+ reactComponent.ContainerTag = htmlTag;
+ }
+ if (!string.IsNullOrEmpty(containerClass))
+ {
+ reactComponent.ContainerClass = containerClass;
+ }
+
+ var executionResult = reactComponent.RenderRouterWithContext(clientOnly, serverOnly);
+
+ if (executionResult.Context?.status != null)
+ {
+ // Use provided contextHandler
+ if (contextHandler != null)
+ {
+ contextHandler(response, executionResult.Context);
+ }
+ // Handle routing context internally
+ else
+ {
+ SetServerResponse.ModifyResponse(executionResult.Context, response);
+ }
+ }
+
+ return new HtmlString(executionResult.RenderResult);
+ }
+ finally
+ {
+ Environment.ReturnEngineToPool();
+ }
+ }
+ }
+}
diff --git a/src/React.Router/Properties/AssemblyInfo.cs b/src/React.Router/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..c2cbf2eae
--- /dev/null
+++ b/src/React.Router/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+using System.Reflection;
+using System.Runtime.InteropServices;
+
+[assembly: AssemblyTitle("React.Router")]
+[assembly: AssemblyDescription("React Router support for ReactJS.NET")]
+[assembly: ComVisible(false)]
+[assembly: Guid("277850fc-8765-4042-945f-a50b8f2525a9")]
diff --git a/src/React.Router/React.Router.csproj b/src/React.Router/React.Router.csproj
new file mode 100644
index 000000000..c03204a80
--- /dev/null
+++ b/src/React.Router/React.Router.csproj
@@ -0,0 +1,54 @@
+
+
+
+ React Router support for ReactJS.NET.
+ Copyright 2014-Present Facebook, Inc
+ ReactJS.NET Router
+ Daniel Lo Nigro, Gunnar Már Óttarsson
+ net451;netstandard1.6
+ true
+ React.Router
+ ../key.snk
+ true
+ true
+ React.Router
+ asp.net;mvc;asp;javascript;js;react;facebook;reactjs;babel;router;react router
+ http://reactjs.net/img/logo_64.png
+ http://reactjs.net/
+ https://github.com/reactjs/React.NET#licence
+ false
+
+
+
+ TRACE;DEBUG;ASPNETCORE;NET451
+
+
+
+
+
+
+ true
+ content\
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
diff --git a/src/React.Router/ReactEnvironmentExtensions.cs b/src/React.Router/ReactEnvironmentExtensions.cs
new file mode 100644
index 000000000..3137538cd
--- /dev/null
+++ b/src/React.Router/ReactEnvironmentExtensions.cs
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+namespace React.Router
+{
+ ///
+ /// extension for rendering a React Router Component with context
+ ///
+ public static class ReactEnvironmentExtensions
+ {
+ ///
+ /// Create a React Router Component with context and add it to the list of components to render client side,
+ /// if applicable.
+ ///
+ /// Type of the props
+ /// React Environment
+ /// Name of the component
+ /// Props to use
+ /// F.x. from Request.Path. Used by React Static Router to determine context and routing.
+ /// 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.
+ ///
+ public static ReactRouterComponent CreateRouterComponent(
+ this IReactEnvironment env,
+ string componentName,
+ T props,
+ string path,
+ string containerId = null,
+ bool clientOnly = false
+ )
+ {
+ var config = AssemblyRegistration.Container.Resolve();
+
+ var component = new ReactRouterComponent(
+ env,
+ config,
+ componentName,
+ containerId,
+ path
+ )
+ {
+ Props = props,
+ };
+
+ return env.CreateComponent(component, clientOnly) as ReactRouterComponent;
+ }
+ }
+}
diff --git a/src/React.Router/ReactRouterComponent.cs b/src/React.Router/ReactRouterComponent.cs
new file mode 100644
index 000000000..c2d553027
--- /dev/null
+++ b/src/React.Router/ReactRouterComponent.cs
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+using JavaScriptEngineSwitcher.Core;
+using Newtonsoft.Json;
+using React.Exceptions;
+
+namespace React.Router
+{
+ ///
+ /// Represents a React Router JavaScript component.
+ ///
+ public class ReactRouterComponent : ReactComponent
+ {
+ ///
+ /// F.x. from Request.Path. Used by React Static Router to determine context and routing.
+ ///
+ protected string _path;
+
+ ///
+ /// Initialises a new instance of the class.
+ ///
+ /// The environment.
+ /// Site-wide configuration.
+ /// Name of the component.
+ /// The ID of the container DIV for this component
+ /// F.x. from Request.Path. Used by React Static Router to determine context and routing.
+ public ReactRouterComponent(
+ IReactEnvironment environment,
+ IReactSiteConfiguration configuration,
+ string componentName,
+ string containerId,
+ string path
+ ) : base(environment, configuration, componentName, containerId)
+ {
+ _path = path;
+ }
+
+ ///
+ /// Render a React StaticRouter Component with context object.
+ ///
+ /// Only renders component container. Used for client-side only rendering. Does not make sense in this context but included for consistency
+ /// Only renders the common HTML mark up and not any React specific data attributes. Used for server-side only rendering.
+ /// Object containing HTML in string format and the React Router context object
+ public virtual ExecutionResult RenderRouterWithContext(bool renderContainerOnly = false, bool renderServerOnly = false)
+ {
+ _environment.Execute("var context = {};");
+
+ var html = RenderHtml(renderContainerOnly, renderServerOnly);
+
+ var contextString = _environment.Execute("JSON.stringify(context);");
+
+ return new ExecutionResult
+ {
+ RenderResult = html,
+ Context = JsonConvert.DeserializeObject(contextString),
+ };
+ }
+
+ ///
+ /// Gets the JavaScript code to initialise the component
+ ///
+ /// JavaScript for component initialisation
+ protected override string GetComponentInitialiser()
+ {
+ return string.Format(
+ @"React.createElement
+ ({0}, Object.assign({1}, {{ path: '{2}', context: context }}))",
+ ComponentName,
+ _serializedProps,
+ _path
+ );
+ }
+
+ ///
+ /// 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.
+ /// Uses base Component initialiser.
+ /// Client side React Router does not need context nor explicit path parameter.
+ ///
+ /// JavaScript
+ public override string RenderJavaScript()
+ {
+ return string.Format(
+ "ReactDOM.render({0}, document.getElementById({1}))",
+ base.GetComponentInitialiser(),
+ JsonConvert.SerializeObject(ContainerId, _configuration.JsonSerializerSettings) // SerializeObject accepts null settings
+ );
+ }
+ }
+}
diff --git a/src/React.Router/ReactRouterException.cs b/src/React.Router/ReactRouterException.cs
new file mode 100644
index 000000000..419397be4
--- /dev/null
+++ b/src/React.Router/ReactRouterException.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+using System;
+using System.Runtime.Serialization;
+
+namespace React.Router
+{
+ ///
+ /// React Router Exception
+ ///
+#if NET451
+ [Serializable]
+#endif
+ public class ReactRouterException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ReactRouterException() : base() { }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The message that describes the error.
+ public ReactRouterException(string message) : base(message) { }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The error message that explains the reason for the exception.
+ /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified.
+ public ReactRouterException(string message, Exception innerException)
+ : base(message, innerException) { }
+
+
+#if NET451
+ ///
+ /// Used by deserialization
+ ///
+ protected ReactRouterException(SerializationInfo info, StreamingContext context)
+ : base(info, context)
+ { }
+#endif
+ }
+}
diff --git a/src/React.Router/RoutingContext.cs b/src/React.Router/RoutingContext.cs
new file mode 100644
index 000000000..49e00ca9e
--- /dev/null
+++ b/src/React.Router/RoutingContext.cs
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+namespace React.Router
+{
+ ///
+ /// Context object used during render of React Router component
+ ///
+ public class RoutingContext
+ {
+ ///
+ /// HTTP Status Code.
+ /// If present signifies that the given status code should be returned by server.
+ ///
+ public int? status { get; set; }
+
+ ///
+ /// URL to redirect to.
+ /// If included this signals that React Router determined a redirect should happen.
+ ///
+ public string url { get; set; }
+ }
+}
diff --git a/src/React.Router/SetServerResponse.cs b/src/React.Router/SetServerResponse.cs
new file mode 100644
index 000000000..3f225a3bc
--- /dev/null
+++ b/src/React.Router/SetServerResponse.cs
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+#if NET451
+using HttpResponse = System.Web.HttpResponseBase;
+#else
+using HttpResponse = Microsoft.AspNetCore.Http.HttpResponse;
+#endif
+
+namespace React.Router
+{
+ ///
+ /// Helper class that takes the values in the
+ /// to modify the servers response.
+ /// F.x. return an http status code of 404 not found
+ /// or redirect the client to a new URL.
+ ///
+ public static class SetServerResponse
+ {
+ ///
+ /// Uses the values in the to modify
+ /// the servers response.
+ /// F.x. return an http status code of 404 not found
+ /// or redirect the client to a new URL.
+ ///
+ ///
+ /// The routing context returned by React Router.
+ /// It contains new values for the server response.
+ ///
+ /// The response object to use.
+ public static void ModifyResponse(RoutingContext context, HttpResponse Response)
+ {
+ var statusCode = context.status.Value;
+
+ // 300-399
+ if (statusCode >= 300 && statusCode < 400)
+ {
+ if (!string.IsNullOrEmpty(context.url))
+ {
+ if (statusCode == 301)
+ {
+#if NET451
+ Response.RedirectPermanent(context.url);
+#else
+ Response.Redirect(context.url, true);
+#endif
+ }
+ else // 302 and all others
+ {
+ Response.Redirect(context.url);
+ }
+ }
+ else
+ {
+ throw new ReactRouterException("Router requested redirect but no url provided.");
+ }
+ }
+ else
+ {
+ Response.StatusCode = statusCode;
+ }
+ }
+ }
+}
diff --git a/src/React.sln b/src/React.sln
index e34a4eeb5..db8d048dc 100644
--- a/src/React.sln
+++ b/src/React.sln
@@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Library", "Library", "{681C
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CB51F03F-49BD-4B79-8AD4-67962230E76B}"
ProjectSection(SolutionItems) = preProject
+ .editorconfig = .editorconfig
..\.gitignore = ..\.gitignore
..\build.proj = ..\build.proj
..\dev-build-push.bat = ..\dev-build-push.bat
@@ -63,6 +64,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "React.AspNet", "React.AspNe
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "React.Sample.Mvc6", "React.Sample.Mvc6\React.Sample.Mvc6.csproj", "{6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "React.Router", "React.Router\React.Router.csproj", "{D076273B-C5EA-47C7-923D-523E4C5EE30D}"
+EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "React.AspNet.Middleware", "React.AspNet.Middleware\React.AspNet.Middleware.csproj", "{7E1C3999-1982-476D-9307-12B30737B41E}"
EndProject
Global
@@ -131,6 +134,10 @@ Global
{6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6E2C1144-703C-4FF5-893D-B9C1A2C55C3B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D076273B-C5EA-47C7-923D-523E4C5EE30D}.Release|Any CPU.Build.0 = Release|Any CPU
{7E1C3999-1982-476D-9307-12B30737B41E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E1C3999-1982-476D-9307-12B30737B41E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E1C3999-1982-476D-9307-12B30737B41E}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -155,6 +162,7 @@ Global
{6AA0D75E-5797-4690-BEFC-098A60C511A3} = {F567B25C-E869-4C93-9C96-077761250F87}
{631FCC55-0219-46DC-838A-C5A3E878943A} = {681C45FB-103C-48BC-B992-20C5B6B78F92}
{6E2C1144-703C-4FF5-893D-B9C1A2C55C3B} = {A51CE5B6-294F-4D39-B32B-BF08DAF9B40B}
+ {D076273B-C5EA-47C7-923D-523E4C5EE30D} = {681C45FB-103C-48BC-B992-20C5B6B78F92}
{7E1C3999-1982-476D-9307-12B30737B41E} = {681C45FB-103C-48BC-B992-20C5B6B78F92}
EndGlobalSection
EndGlobal
diff --git a/tests/React.Tests/Core/ReactEnvironmentTest.cs b/tests/React.Tests/Core/ReactEnvironmentTest.cs
index 5829d21d2..4017051ba 100644
--- a/tests/React.Tests/Core/ReactEnvironmentTest.cs
+++ b/tests/React.Tests/Core/ReactEnvironmentTest.cs
@@ -123,7 +123,21 @@ public void ReturnsEngineToPool()
mocks.EngineFactory.Verify(x => x.GetEngine(), Times.AtLeast(2));
}
- private class Mocks
+ [Fact]
+ public void CreatesIReactComponent()
+ {
+ var mocks = new Mocks();
+ var environment = mocks.CreateReactEnvironment();
+
+ var component = new Mock();
+
+ environment.CreateComponent(component.Object);
+
+ // A single nameless component was successfully added!
+ Assert.Equal(";\r\n", environment.GetInitJavaScript());
+ }
+
+ public class Mocks
{
public Mock Engine { get; private set; }
public Mock EngineFactory { get; private set; }
@@ -155,6 +169,17 @@ public ReactEnvironment CreateReactEnvironment()
FileCacheHash.Object
);
}
+
+ public Mock CreateMockedReactEnvironment()
+ {
+ return new Mock(
+ EngineFactory.Object,
+ Config.Object,
+ Cache.Object,
+ FileSystem.Object,
+ FileCacheHash.Object
+ );
+ }
}
}
}
diff --git a/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs b/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs
index d14d0f9e1..4f456e9a3 100644
--- a/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs
+++ b/tests/React.Tests/Owin/EntryAssemblyFileSystemTests.cs
@@ -14,7 +14,7 @@ namespace React.Tests.Owin
{
public class EntryAssemblyFileSystemTests
{
- [Theory]
+ [Theory]
[InlineData("C:\\", "~/", "C:\\")]
[InlineData("C:\\", "~/foo/bar.js", "C:\\foo\\bar.js")]
public void MapPath(string rootPath, string relativePath, string expected)
diff --git a/tests/React.Tests/React.Tests.csproj b/tests/React.Tests/React.Tests.csproj
index 662774500..8f21419d2 100644
--- a/tests/React.Tests/React.Tests.csproj
+++ b/tests/React.Tests/React.Tests.csproj
@@ -18,6 +18,7 @@
+
diff --git a/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs
new file mode 100644
index 000000000..7ccaf6230
--- /dev/null
+++ b/tests/React.Tests/Router/HtmlHelperExtensionsTest.cs
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+using Moq;
+using Xunit;
+using React.Exceptions;
+using React.Router;
+using React.Tests.Core;
+using System.Web;
+using JavaScriptEngineSwitcher.Core;
+using System.Web.Mvc;
+
+namespace React.Tests.Router
+{
+ public class HtmlHelperExtensionsTest
+ {
+ ///
+ /// Creates a mock and registers it with the IoC container
+ /// This is only required because can not be
+ /// injected :(
+ ///
+ private ReactEnvironmentTest.Mocks ConfigureMockReactEnvironment()
+ {
+ var mocks = new ReactEnvironmentTest.Mocks();
+
+ var environment = mocks.CreateReactEnvironment();
+ AssemblyRegistration.Container.Register(environment);
+ return mocks;
+ }
+
+ private Mock ConfigureMockEnvironment()
+ {
+ var environment = new Mock();
+ AssemblyRegistration.Container.Register(environment.Object);
+ return environment;
+ }
+
+ private Mock ConfigureMockConfiguration()
+ {
+ var config = new Mock();
+ AssemblyRegistration.Container.Register(config.Object);
+ return config;
+ }
+
+ ///
+ /// Mock an html helper with a mocked response object.
+ /// Used when testing for server response modification.
+ ///
+ class HtmlHelperMocks
+ {
+ public Mock htmlHelper;
+ public Mock httpResponse;
+
+ public HtmlHelperMocks()
+ {
+ var viewDataContainer = new Mock();
+ var viewContext = new Mock();
+ httpResponse = new Mock();
+ htmlHelper = new Mock(viewContext.Object, viewDataContainer.Object);
+ var httpContextBase = new Mock();
+ viewContext.Setup(x => x.HttpContext).Returns(httpContextBase.Object);
+ httpContextBase.Setup(x => x.Response).Returns(httpResponse.Object);
+ }
+ }
+
+ ///
+ /// Mocks alot of common functionality related to rendering a
+ /// React Router component.
+ ///
+ class ReactRouterMocks
+ {
+ public Mock config;
+ public Mock environment;
+ public Mock component;
+
+ public ReactRouterMocks(
+ Mock conf,
+ Mock env
+ )
+ {
+ config = conf;
+ environment = env;
+
+ component = new Mock(
+ environment.Object,
+ config.Object,
+ "ComponentName",
+ "",
+ "/"
+ );
+ var execResult = new Mock();
+
+ component.Setup(x => x.RenderRouterWithContext(It.IsAny(), It.IsAny()))
+ .Returns(execResult.Object);
+ environment.Setup(x => x.CreateComponent(
+ It.IsAny(),
+ It.IsAny()
+ )).Returns(component.Object);
+ environment.Setup(x => x.Execute("JSON.stringify(context);"))
+ .Returns("{ }");
+ }
+ }
+
+ [Fact]
+ public void EngineIsReturnedToPoolAfterRender()
+ {
+ var config = ConfigureMockConfiguration();
+ var environment = ConfigureMockEnvironment();
+ var routerMocks = new ReactRouterMocks(config, environment);
+ var htmlHelperMock = new HtmlHelperMocks();
+
+ environment.Verify(x => x.ReturnEngineToPool(), Times.Never);
+ var result = HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/",
+ htmlTag: "span",
+ clientOnly: true,
+ serverOnly: true
+ );
+ environment.Verify(x => x.ReturnEngineToPool(), Times.Once);
+ }
+
+ [Fact]
+ public void ReactWithClientOnlyTrueShouldCallRenderHtmlWithTrue()
+ {
+ var config = ConfigureMockConfiguration();
+
+ var htmlHelperMock = new HtmlHelperMocks();
+ var environment = ConfigureMockEnvironment();
+ var routerMocks = new ReactRouterMocks(config, environment);
+
+ var result = HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/",
+ htmlTag: "span",
+ clientOnly: true,
+ serverOnly: false
+ );
+ routerMocks.component.Verify(x => x.RenderRouterWithContext(It.Is(y => y == true), It.Is(z => z == false)), Times.Once);
+ }
+
+ [Fact]
+ public void ReactWithServerOnlyTrueShouldCallRenderHtmlWithTrue()
+ {
+ var config = ConfigureMockConfiguration();
+
+ var htmlHelperMock = new HtmlHelperMocks();
+ var environment = ConfigureMockEnvironment();
+ var routerMocks = new ReactRouterMocks(config, environment);
+
+ var result = HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/",
+ htmlTag: "span",
+ clientOnly: false,
+ serverOnly: true
+ );
+ routerMocks.component.Verify(x => x.RenderRouterWithContext(It.Is(y => y == false), It.Is(z => z == true)), Times.Once);
+ }
+
+ [Fact]
+ public void ShouldModifyStatusCode()
+ {
+ var mocks = ConfigureMockReactEnvironment();
+ ConfigureMockConfiguration();
+
+ mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);"))
+ .Returns("{ status: 200 }");
+
+ var htmlHelperMock = new HtmlHelperMocks();
+
+ HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/"
+ );
+ htmlHelperMock.httpResponse.VerifySet(x => x.StatusCode = 200);
+ }
+
+ [Fact]
+ public void ShouldRunCustomContextHandler()
+ {
+ var mocks = ConfigureMockReactEnvironment();
+ ConfigureMockConfiguration();
+
+ mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);"))
+ .Returns("{ status: 200 }");
+
+ var htmlHelperMock = new HtmlHelperMocks();
+
+ HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/",
+ contextHandler: (response, context) => response.StatusCode = context.status.Value
+ );
+ htmlHelperMock.httpResponse.VerifySet(x => x.StatusCode = 200);
+ }
+
+ [Fact]
+ public void ShouldRedirectPermanent()
+ {
+ var mocks = ConfigureMockReactEnvironment();
+ ConfigureMockConfiguration();
+
+ mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);"))
+ .Returns(@"{ status: 301, url: ""/foo"" }");
+
+ var htmlHelperMock = new HtmlHelperMocks();
+
+ HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/"
+ );
+ htmlHelperMock.httpResponse.Verify(x => x.RedirectPermanent(It.IsAny()));
+ }
+
+ [Fact]
+ public void ShouldFailRedirectWithNoUrl()
+ {
+ var mocks = ConfigureMockReactEnvironment();
+ ConfigureMockConfiguration();
+
+ mocks.Engine.Setup(x => x.Evaluate("JSON.stringify(context);"))
+ .Returns("{ status: 301 }");
+
+ var htmlHelperMock = new HtmlHelperMocks();
+
+ Assert.Throws(() =>
+
+ HtmlHelperExtensions.ReactRouterWithContext(
+ htmlHelper: htmlHelperMock.htmlHelper.Object,
+ componentName: "ComponentName",
+ props: new { },
+ path: "/"
+ )
+ );
+ }
+ }
+}
diff --git a/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs
new file mode 100644
index 000000000..98a486311
--- /dev/null
+++ b/tests/React.Tests/Router/ReactEnvironmentExtensionsTest.cs
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+using Moq;
+using Xunit;
+using React.Exceptions;
+using React.Router;
+using React.Tests.Core;
+
+namespace React.Tests.Router
+{
+ public class ReactEnvironmentExtensionsTest
+ {
+ [Fact]
+ public void EnvironmentShouldGetCalledClientOnly()
+ {
+ var environment = new Mock();
+ AssemblyRegistration.Container.Register(environment.Object);
+ var config = new Mock();
+ AssemblyRegistration.Container.Register(config.Object);
+
+ var component = ReactEnvironmentExtensions.CreateRouterComponent(
+ environment.Object,
+ "ComponentName",
+ new { },
+ "/",
+ null,
+ true
+ );
+
+ environment.Verify(x => x.CreateComponent(It.IsAny(), true));
+ }
+ }
+}
diff --git a/tests/React.Tests/Router/ReactRouterComponentTest.cs b/tests/React.Tests/Router/ReactRouterComponentTest.cs
new file mode 100644
index 000000000..59b90b06a
--- /dev/null
+++ b/tests/React.Tests/Router/ReactRouterComponentTest.cs
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2014-Present, Facebook, Inc.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+using Moq;
+using Xunit;
+using React.Exceptions;
+using React.Router;
+using React.Tests.Core;
+
+namespace React.Tests.Router
+{
+ public class ReactRouterComponentTest
+ {
+ [Fact]
+ public void RenderJavaScriptShouldNotIncludeContextOrPath()
+ {
+ var environment = new Mock();
+ var config = new Mock();
+
+ var component = new ReactRouterComponent(environment.Object, config.Object, "Foo", "container", "/bar")
+ {
+ Props = new { hello = "World" }
+ };
+ var result = component.RenderJavaScript();
+
+ Assert.Equal(
+ @"ReactDOM.render(React.createElement(Foo, {""hello"":""World""}), document.getElementById(""container""))",
+ result
+ );
+ }
+ }
+}