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

React Router Support #407

Merged
merged 12 commits into from
Jul 22, 2017
9 changes: 5 additions & 4 deletions build.proj
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ of patent rights can be found in the PATENTS file in the same directory.
<Project ToolsVersion="4.0" DefaultTargets="Build;Test;Package" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Major>3</Major>
<Minor>0</Minor>
<Build>1</Build>
<Minor>1</Minor>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry about bumping this, I'll bump it on release.

<Build>0</Build>
<Revision>0</Revision>
<DevNuGetServer>http://reactjs.net/packages/</DevNuGetServer>
<MSBuildCommunityTasksPath>$(MSBuildProjectDirectory)\tools\MSBuildTasks</MSBuildCommunityTasksPath>
Expand All @@ -26,8 +26,9 @@ of patent rights can be found in the PATENTS file in the same directory.
<PackageAssemblies Include="React.Core" />
<PackageAssemblies Include="React.MSBuild" />
<PackageAssemblies Include="React.Owin" />
<PackageAssemblies Include="React.Web" />
<PackageAssemblies Include="React.Web.Mvc4" />
<PackageAssemblies Include="React.Router" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation looks a bit off here, maybe it's using spaces instead of tabs?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, i went over the .cs files and tabified but i missed this file and some xml. fixed

<PackageAssemblies Include="React.Web" />
<PackageAssemblies Include="React.Web.Mvc4" />
<PackageAssemblies Include="System.Web.Optimization.React" />
</ItemGroup>

Expand Down
8 changes: 8 additions & 0 deletions src/React.Core/IReactEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ public interface IReactEnvironment
/// <returns>The component</returns>
IReactComponent CreateComponent<T>(string componentName, T props, string containerId = null, bool clientOnly = false);

/// <summary>
/// Adds the provided <see cref="IReactComponent"/> to the list of components to render client side.
/// </summary>
/// <param name="component">Component to add to client side render list</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <returns>The component</returns>
IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false);

/// <summary>
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
Expand Down
17 changes: 17 additions & 0 deletions src/React.Core/ReactEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,23 @@ public virtual IReactComponent CreateComponent<T>(string componentName, T props,
return component;
}

/// <summary>
/// Adds the provided <see cref="IReactComponent"/> to the list of components to render client side.
/// </summary>
/// <param name="component">Component to add to client side render list</param>
/// <param name="clientOnly">True if server-side rendering will be bypassed. Defaults to false.</param>
/// <returns>The component</returns>
public virtual IReactComponent CreateComponent(IReactComponent component, bool clientOnly = false)
{
if (!clientOnly)
{
EnsureUserScriptsLoaded();
}

_components.Add(component);
return component;
}

/// <summary>
/// Renders the JavaScript required to initialise all components client-side. This will
/// attach event handlers to the server-rendered HTML.
Expand Down
9 changes: 9 additions & 0 deletions src/React.Router/Content/Views/web.config.transform
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<configuration>
<system.web.webPages.razor>
<pages>
<namespaces>
<add namespace="React.Router" />
</namespaces>
</pages>
</system.web.webPages.razor>
</configuration>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The react router htmlhelper lives in this namespace. I seem to recall being unable to use it if I didn't either have this statement in my web.config or an explicit using statement in file.
You have a similar file in the mvc4 project.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, interesting.

28 changes: 28 additions & 0 deletions src/React.Router/ExecutionResult.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Contains the context object used during execution in addition to
/// the string result of rendering the React Router component.
/// </summary>
public class ExecutionResult
{
/// <summary>
/// String result of ReactDOMServer render of provided component.
/// </summary>
public string RenderResult { get; set; }

/// <summary>
/// Context object used during JS engine execution.
/// </summary>
public RoutingContext Context { get; set; }
}
}
167 changes: 167 additions & 0 deletions src/React.Router/HtmlHelperExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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
{
/// <summary>
/// Render a React StaticRouter Component with context.
/// </summary>
public static class HtmlHelperExtensions
{
/// <summary>
/// Gets the React environment
/// </summary>
private static IReactEnvironment Environment
{
get
{
try
{
return ReactEnvironment.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
);
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can probably just use ReactEnvironment.Current directly rather than wrapping it and rethrowing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that particular block is copied from react.aspnet\htmlhelperextensions.cs.
I imagined you had a reason for doing it there that would also apply here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, I wanted to show a more specific error message for if ReactJS.NET was misconfigured. I guess that's useful here too though! Maybe ReactEnvironment should have a getter that does that (like ReactEnvironment.GetCurrentOrThrow()) so that the code is not duplicated.


/// <summary>
/// Render a React StaticRouter Component with context object.
/// Can optionally be provided with a custom context handler to handle the various status codes.
///
/// </summary>
/// <param name="htmlHelper">MVC Razor <see cref="IHtmlHelper"/></param>
/// <param name="componentName">Name of React Static Router component. Expose component globally to ReactJS.NET</param>
/// <param name="props">Props to initialise the component with</param>
/// <param name="path">F.x. from Request.Path. Used by React Static Router to determine context and routing.</param>
/// <param name="Response">Used either by contextHandler or internally to modify the Response status code and redirect.</param>
/// <param name="contextHandler">Optional custom context handler, can be used instead of providing a Response object</param>
/// <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="containerClass">HTML class(es) to set on the container tag</param>
/// <returns><see cref="IHtmlString"/> containing the rendered markup for provided React Router component</returns>
public static IHtmlString ReactRouterWithContext<T>(
this IHtmlHelper htmlHelper,
string componentName,
T props,
string path = null,
HttpResponse Response = null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be response (lowercase r)

string htmlTag = null,
string containerId = null,
bool clientOnly = false,
bool serverOnly = false,
string containerClass = null,
Action<HttpResponse, RoutingContext> contextHandler = null
)
{
try
{
path = path ?? htmlHelper.ViewContext.HttpContext.Request.Path;
Response = Response ?? htmlHelper.ViewContext.HttpContext.Response;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you actually need the Response argument if it can come from the htmlHelper?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this was just intended as a convenience and i was hoping it might help for unit testing?

But I guess it might just be confusing, I'll remove it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah i'm mocking the response obj in the unit tests, should I be doing this a different way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was hoping it might help for unit testing

For unit testing, you should be able to mock the ViewContext property on the IHtmlHelper.


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
{
HandleRoutingContext(executionResult.Context, Response);
}
}

return new HtmlString(executionResult.RenderResult);
}
finally
{
Environment.ReturnEngineToPool();
}
}

private static void HandleRoutingContext(RoutingContext context, HttpResponse Response)
{
var statusCode = context.status.Value;

// 300-399
if (statusCode >= 300 && statusCode < 400)
{
if (!string.IsNullOrEmpty(context.url))
{
if (statusCode == 301)
{

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the blank line please

#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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels a bit strange for a HTML helper to set the status code, I can't think of a better way to do this though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, moved to helper class

}
}
}
}
7 changes: 7 additions & 0 deletions src/React.Router/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -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")]
55 changes: 55 additions & 0 deletions src/React.Router/React.Router.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>React Router support for ReactJS.NET.</Description>
<Copyright>Copyright 2014-Present Facebook, Inc</Copyright>
<AssemblyTitle>ReactJS.NET Router</AssemblyTitle>
<Authors>Daniel Lo Nigro, Gunnar Már Óttarsson</Authors>
<TargetFrameworks>net451;netstandard1.6</TargetFrameworks>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<AssemblyName>React.Router</AssemblyName>
<AssemblyOriginatorKeyFile>../key.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId>React.Router</PackageId>
<PackageTags>asp.net;mvc;asp;javascript;js;react;facebook;reactjs;babel</PackageTags>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add react router and router as tags too.

<PackageIconUrl>http://reactjs.net/img/logo_64.png</PackageIconUrl>
<PackageProjectUrl>http://reactjs.net/</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/reactjs/React.NET#licence</PackageLicenseUrl>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net451|AnyCPU'">
<DefineConstants>TRACE;DEBUG;ASPNETCORE;NET451</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\SharedAssemblyInfo.cs" />
<Compile Include="..\SharedAssemblyVersionInfo.cs" />
<Content Include="Content\**\*">
<Pack>true</Pack>
<PackagePath>content\</PackagePath>
</Content>
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard1.6' ">
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="1.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.ViewFeatures" Version="1.0.3" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'net451' ">
<Reference Include="System" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can remove this, System is always referenced by default.

<Reference Include="System.Web" />
<Reference Include="Microsoft.CSharp" />
<PackageReference Include="Microsoft.AspNet.Mvc" Version="4.0.20710" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\React.Core\React.Core.csproj" />
</ItemGroup>

<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

</Project>
55 changes: 55 additions & 0 deletions src/React.Router/ReactEnvironmentExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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
{
/// <summary>
/// <see cref="ReactEnvironment"/> extension for rendering a React Router Component with context
/// </summary>
public static class ReactEnvironmentExtensions
{
/// <summary>
/// Create a React Router Component with context and add it to the list of components to render client side,
/// if applicable.
/// </summary>
/// <typeparam name="T">Type of the props</typeparam>
/// <param name="env">React Environment</param>
/// <param name="componentName">Name of the component</param>
/// <param name="props">Props to use</param>
/// <param name="path">F.x. from Request.Path. Used by React Static Router to determine context and routing.</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>
/// <returns></returns>
public static ReactRouterComponent CreateRouterComponent<T>(
this IReactEnvironment env,
string componentName,
T props,
string path,
string containerId = null,
bool clientOnly = false
)
{
var config = AssemblyRegistration.Container.Resolve<IReactSiteConfiguration>();

var component = new ReactRouterComponent(
env,
config,
componentName,
containerId,
path
)
{
Props = props,
};

env.CreateComponent(component, clientOnly);
return component;
}
}
}
Loading