diff --git a/templates/ReactSpa/ClientApp/boot-server.tsx b/templates/ReactSpa/ClientApp/boot-server.tsx deleted file mode 100644 index b433f2e..0000000 --- a/templates/ReactSpa/ClientApp/boot-server.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { renderToString } from 'react-dom/server'; -import { match, RouterContext } from 'react-router'; -import createMemoryHistory from 'history/lib/createMemoryHistory'; -import { routes } from './routes'; - -// The 'asp-prerender-module' tag helper invokes the following function when the React app is to -// be prerendered on the server. It runs asynchronously, and issues a callback with the React app's -// initial HTML and any other state variables. - -export default function (params: any): Promise<{ html: string }> { - return new Promise<{ html: string, globals: { [key: string]: any } }>((resolve, reject) => { - // Match the incoming request against the list of client-side routes, and reject if there was no match - match({ routes, location: params.location }, (error, redirectLocation, renderProps: any) => { - if (error) { - throw error; - } - - // Build an instance of the application and perform an initial render. - // This will cause any async tasks (e.g., data access) to begin. - const history = createMemoryHistory(params.url); - const app = ; - renderToString(app); - - // Once the tasks are done, we can perform the final render - params.domainTasks.then(() => { - resolve({ - html: renderToString(app), - globals: { /* Supply any other JSON-serializable data you want to make available on the client */ } - }); - }, reject); // Also propagate any errors back into the host application - }); - }); -} diff --git a/templates/ReactSpa/ClientApp/boot-client.tsx b/templates/ReactSpa/ClientApp/boot.tsx similarity index 100% rename from templates/ReactSpa/ClientApp/boot-client.tsx rename to templates/ReactSpa/ClientApp/boot.tsx diff --git a/templates/ReactSpa/ClientApp/components/About.tsx b/templates/ReactSpa/ClientApp/components/About.tsx deleted file mode 100644 index a178eaf..0000000 --- a/templates/ReactSpa/ClientApp/components/About.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from 'react'; - -export class About extends React.Component { - public render() { - return
-

About

- -

This is another component.

- -

It's here to demonstrate navigation.

-
; - } -} diff --git a/templates/ReactSpa/ClientApp/components/FetchData.tsx b/templates/ReactSpa/ClientApp/components/FetchData.tsx new file mode 100644 index 0000000..8bb439f --- /dev/null +++ b/templates/ReactSpa/ClientApp/components/FetchData.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; + +interface FetchDataExampleState { + forecasts: WeatherForecast[]; + loading: boolean; +} + +export class FetchData extends React.Component { + constructor() { + super(); + this.state = { forecasts: [], loading: true }; + + fetch('/api/SampleData/WeatherForecasts') + .then(response => response.json()) + .then((data: WeatherForecast[]) => { + this.setState({ forecasts: data, loading: false }); + }); + } + + public render() { + let contents = this.state.loading + ?

Loading...

+ : FetchData.renderForecastsTable(this.state.forecasts); + + return
+

Weather forecast

+

This component demonstrates fetching data from the server.

+ { contents } +

For more sophisticated applications, consider an architecture such as Redux or Flux for managing state. You can generate an ASP.NET Core application with React and Redux using dotnet new aspnet/spa/reactredux instead of using this template.

+
; + } + + private static renderForecastsTable(forecasts: WeatherForecast[]) { + return + + + + + + + + + + {forecasts.map(forecast => + + + + + + + )} + +
DateTemp. (C)Temp. (F)Summary
{ forecast.dateFormatted }{ forecast.temperatureC }{ forecast.temperatureF }{ forecast.summary }
; + } +} + +interface WeatherForecast { + dateFormatted: string; + temperatureC: number; + temperatureF: number; + summary: string; +} diff --git a/templates/ReactSpa/ClientApp/components/Home.tsx b/templates/ReactSpa/ClientApp/components/Home.tsx index a796e30..8722510 100644 --- a/templates/ReactSpa/ClientApp/components/Home.tsx +++ b/templates/ReactSpa/ClientApp/components/Home.tsx @@ -13,12 +13,16 @@ export class Home extends React.Component {

To help you get started, we've also set up:

    -
  • Client-side navigation. For example, click About then Back to return here.
  • -
  • Server-side prerendering. For optimal performance, your React application is first executed on the server. The resulting HTML and state is then transferred to the client to continue execution. This is also known as being an isomorphic or universal application.
  • +
  • Client-side navigation. For example, click Counter then Back to return here.
  • Webpack dev middleware. In development mode, there's no need to run the webpack build tool. Your client-side resources are dynamically built on demand. Updates are available as soon as you modify any file.
  • Hot module replacement. In development mode, you don't even need to reload the page after making most changes. Within seconds of saving changes to files, rebuilt CSS and React components will be injected directly into your running application, preserving its live state.
  • Efficient production builds. In production mode, development-time features are disabled, and the webpack build tool produces minified static CSS and JavaScript files.
+

Going further

+

+ For larger applications, or for server-side prerendering (i.e., for isomorphic or universal applications), you should consider using a Flux/Redux-like architecture. + You can generate an ASP.NET Core application with React and Redux using dotnet new aspnet/spa/reactredux instead of using this template. +

; } } diff --git a/templates/ReactSpa/ClientApp/components/NavMenu.tsx b/templates/ReactSpa/ClientApp/components/NavMenu.tsx index 406280d..e52849d 100644 --- a/templates/ReactSpa/ClientApp/components/NavMenu.tsx +++ b/templates/ReactSpa/ClientApp/components/NavMenu.tsx @@ -23,13 +23,13 @@ export class NavMenu extends React.Component {
  • - - About + + Counter
  • - - Counter + + Fetch data
  • diff --git a/templates/ReactSpa/ClientApp/routes.tsx b/templates/ReactSpa/ClientApp/routes.tsx index 319f109..306c9bd 100644 --- a/templates/ReactSpa/ClientApp/routes.tsx +++ b/templates/ReactSpa/ClientApp/routes.tsx @@ -2,11 +2,11 @@ import * as React from 'react'; import { Router, Route, HistoryBase } from 'react-router'; import { Layout } from './components/Layout'; import { Home } from './components/Home'; -import { About } from './components/About'; +import { FetchData } from './components/FetchData'; import { Counter } from './components/Counter'; export const routes = - + ; diff --git a/templates/ReactSpa/Controllers/SampleDataController.cs b/templates/ReactSpa/Controllers/SampleDataController.cs new file mode 100644 index 0000000..a809d45 --- /dev/null +++ b/templates/ReactSpa/Controllers/SampleDataController.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace WebApplicationBasic.Controllers +{ + [Route("api/[controller]")] + public class SampleDataController : Controller + { + private static string[] Summaries = new[] + { + "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" + }; + + [HttpGet, Route("[action]")] + public IEnumerable WeatherForecasts() + { + var rng = new Random(); + return Enumerable.Range(1, 5).Select(index => new WeatherForecast + { + DateFormatted = DateTime.Now.AddDays(index).ToString("d"), + TemperatureC = rng.Next(-20, 55), + Summary = Summaries[rng.Next(Summaries.Length)] + }); + } + + public class WeatherForecast + { + public string DateFormatted { get; set; } + public int TemperatureC { get; set; } + public string Summary { get; set; } + + public int TemperatureF + { + get + { + return 32 + (int)(this.TemperatureC / 0.5556); + } + } + } + } +} diff --git a/templates/ReactSpa/Startup.cs b/templates/ReactSpa/Startup.cs index 21a21dc..4576699 100755 --- a/templates/ReactSpa/Startup.cs +++ b/templates/ReactSpa/Startup.cs @@ -8,6 +8,7 @@ using Microsoft.AspNet.SpaServices.Webpack; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Serialization; namespace WebApplicationBasic { @@ -28,7 +29,10 @@ namespace WebApplicationBasic public void ConfigureServices(IServiceCollection services) { // Add framework services. - services.AddMvc(); + services.AddMvc().AddJsonOptions(options => + { + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/templates/ReactSpa/Views/Home/Index.cshtml b/templates/ReactSpa/Views/Home/Index.cshtml index 1bb189e..139ed8a 100644 --- a/templates/ReactSpa/Views/Home/Index.cshtml +++ b/templates/ReactSpa/Views/Home/Index.cshtml @@ -2,7 +2,7 @@ ViewData["Title"] = "Home Page"; } -
    Loading...
    +
    Loading...
    @section scripts { diff --git a/templates/ReactSpa/tsd.json b/templates/ReactSpa/tsd.json index 316a3c0..523893d 100644 --- a/templates/ReactSpa/tsd.json +++ b/templates/ReactSpa/tsd.json @@ -16,6 +16,9 @@ }, "react-router/history.d.ts": { "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" + }, + "whatwg-fetch/whatwg-fetch.d.ts": { + "commit": "dade4414712ce84e3c63393f1aae407e9e7e6af7" } } } diff --git a/templates/ReactSpa/typings/tsd.d.ts b/templates/ReactSpa/typings/tsd.d.ts index 17f2698..f54e1c6 100644 --- a/templates/ReactSpa/typings/tsd.d.ts +++ b/templates/ReactSpa/typings/tsd.d.ts @@ -3,3 +3,4 @@ /// /// /// +/// diff --git a/templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts b/templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts new file mode 100644 index 0000000..64dd904 --- /dev/null +++ b/templates/ReactSpa/typings/whatwg-fetch/whatwg-fetch.d.ts @@ -0,0 +1,85 @@ +// Type definitions for fetch API +// Project: https://github.com/github/fetch +// Definitions by: Ryan Graham +// Definitions: https://github.com/borisyankov/DefinitelyTyped + +declare class Request extends Body { + constructor(input: string|Request, init?:RequestInit); + method: string; + url: string; + headers: Headers; + context: string|RequestContext; + referrer: string; + mode: string|RequestMode; + credentials: string|RequestCredentials; + cache: string|RequestCache; +} + +interface RequestInit { + method?: string; + headers?: HeaderInit|{ [index: string]: string }; + body?: BodyInit; + mode?: string|RequestMode; + credentials?: string|RequestCredentials; + cache?: string|RequestCache; +} + +declare enum RequestContext { + "audio", "beacon", "cspreport", "download", "embed", "eventsource", "favicon", "fetch", + "font", "form", "frame", "hyperlink", "iframe", "image", "imageset", "import", + "internal", "location", "manifest", "object", "ping", "plugin", "prefetch", "script", + "serviceworker", "sharedworker", "subresource", "style", "track", "video", "worker", + "xmlhttprequest", "xslt" +} +declare enum RequestMode { "same-origin", "no-cors", "cors" } +declare enum RequestCredentials { "omit", "same-origin", "include" } +declare enum RequestCache { "default", "no-store", "reload", "no-cache", "force-cache", "only-if-cached" } + +declare class Headers { + append(name: string, value: string): void; + delete(name: string):void; + get(name: string): string; + getAll(name: string): Array; + has(name: string): boolean; + set(name: string, value: string): void; +} + +declare class Body { + bodyUsed: boolean; + arrayBuffer(): Promise; + blob(): Promise; + formData(): Promise; + json(): Promise; + json(): Promise; + text(): Promise; +} +declare class Response extends Body { + constructor(body?: BodyInit, init?: ResponseInit); + error(): Response; + redirect(url: string, status: number): Response; + type: string|ResponseType; + url: string; + status: number; + ok: boolean; + statusText: string; + headers: Headers; + clone(): Response; +} + +declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } + +interface ResponseInit { + status: number; + statusText?: string; + headers?: HeaderInit; +} + +declare type HeaderInit = Headers|Array; +declare type BodyInit = Blob|FormData|string; +declare type RequestInfo = Request|string; + +interface Window { + fetch(url: string|Request, init?: RequestInit): Promise; +} + +declare var fetch: typeof window.fetch; diff --git a/templates/ReactSpa/webpack.config.js b/templates/ReactSpa/webpack.config.js index 503fea9..44fbddd 100644 --- a/templates/ReactSpa/webpack.config.js +++ b/templates/ReactSpa/webpack.config.js @@ -17,7 +17,7 @@ module.exports = merge({ ] }, entry: { - main: ['./ClientApp/boot-client.tsx'], + main: ['./ClientApp/boot.tsx'], vendor: ['bootstrap', 'bootstrap/dist/css/bootstrap.css', 'style-loader', 'jquery'] }, output: {