-A starting point for building universal/isomorphic React applications with ASP.NET Core 1, leveraging existing front-end approaches. Uses the [JavaScriptViewEngine](https://github.com/pauldotknopf/javascriptviewengine).
+A starting point for building universal/isomorphic React applications with ASP.NET Core 2, leveraging existing front-end approaches. Uses the [JavaScriptViewEngine](https://github.com/pauldotknopf/javascriptviewengine).
## Goals
1. **Minimize .NET's usage** - It's only usage should be for building REST endpoints (WebApi) and providing the initial state (pure POCO). No razor syntax *anywhere*.
2. **Isomorphic/universal rendering**
-3. **Client and server should render using the same source files (javascript)**
+3. **Client and server should render using the same source files (TypeScript)**
4. **Out-of-the-box login/register/manage functionality** - Use the branch ```empty-template``` if you wish to have a vanilla React application.
This approach is great for front-end developers because it gives them complete control to build their app as they like. No .NET crutches (bundling/razor). No opinions. No gotchas. Just another typical React client-side application, but with the initial state provided by ASP.NET for each URL.
@@ -40,7 +40,7 @@ After you have your new project generated, let's run that app!
```bash
cd src/ReactBoilerplate
npm install
-gulp
+gulp build
dotnet restore
# The following two lines are only for the 'master' branch, which has a database backend (user management).
# They are not needed when using 'empty-template'.
@@ -49,6 +49,21 @@ dotnet ef database update
dotnet run
```
+Additionally, other gulp actions are available to aid in development.
+
+Build bundles and server-side code in production mode:
+```bash
+gulp prod build
+```
+Build and watch both the bundles and the server-side code (in development mode), and run a browsersync proxy to serve both:
+```bash
+gulp start
+```
+
+Build both the bundles and the server-side code (in production mode), and execute "dotnet run" to serve both:
+```bash
+gulp prod start
+```
Some of the branches in this repo that are maintained:
* [```master```](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/master) - This is the main branch. It has all the stuff required to get you started, including membership, external logins (OAuth) and account management. This is the default branch used with the Yeoman generator.
@@ -56,8 +71,8 @@ Some of the branches in this repo that are maintained:
## The interesting parts
-- [client.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/client.js) and [server.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/server.js) - The entry point for the client-side/server-side applications.
-- [Html.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/helpers/Html.js) and [App.js](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/containers/App/App.js) - These files essentially represent the "React" version of MVC Razor's "_Layout.cshtml".
+- [client.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/client.tsx) and [server.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/server.tsx) - The entry point for the client-side/server-side applications.
+- [Html.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/helpers/Html.tsx) and [App.tsx](https://github.com/pauldotknopf/react-dot-net/blob/master/src/ReactBoilerplate/Scripts/containers/App/App.tsx) - These files essentially represent the "React" version of MVC Razor's "_Layout.cshtml".
- [Controllers](https://github.com/pauldotknopf/react-aspnet-boilerplate/tree/master/src/ReactBoilerplate/Controllers) - The endpoints for a each initial GET request, and each client-side network request.
## What is next?
diff --git a/src/ReactBoilerplate/.babelrc b/src/ReactBoilerplate/.babelrc
deleted file mode 100644
index 2fe3a45..0000000
--- a/src/ReactBoilerplate/.babelrc
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "presets": ["react", "es2015", "stage-0"],
- "plugins": []
-}
\ No newline at end of file
diff --git a/src/ReactBoilerplate/.bootstraprc b/src/ReactBoilerplate/.bootstraprc
index 962b9d5..592d2dc 100644
--- a/src/ReactBoilerplate/.bootstraprc
+++ b/src/ReactBoilerplate/.bootstraprc
@@ -1,11 +1,18 @@
-{
- "bootstrapVersion": 3,
+bootstrapVersion: 3
- "useFlexbox": true,
- "extractStyles": true,
- "styleLoaders": ["style", "css", "sass"],
+env:
+ development:
+ extractStyles: false
+ styleLoaders:
+ - style-loader
+ - css-loader
+ - sass-loader
+ production:
+ extractStyles: true
+ styleLoaders:
+ - css-loader
+ - sass-loader
- "styles": true,
+styles: true
- "scripts": false
-}
\ No newline at end of file
+scripts: false
\ No newline at end of file
diff --git a/src/ReactBoilerplate/.editorconfig b/src/ReactBoilerplate/.editorconfig
index bb1f190..097e288 100644
--- a/src/ReactBoilerplate/.editorconfig
+++ b/src/ReactBoilerplate/.editorconfig
@@ -5,5 +5,9 @@ root = true
indent_style = space
indent_size = 2
+[*.cs]
+indent_style = space
+indent_size = 4
+
[*.md]
trim_trailing_whitespace = false
diff --git a/src/ReactBoilerplate/.eslintrc b/src/ReactBoilerplate/.eslintrc
index fd71550..fdebf4b 100644
--- a/src/ReactBoilerplate/.eslintrc
+++ b/src/ReactBoilerplate/.eslintrc
@@ -4,35 +4,76 @@
"browser": true,
"node": true
},
- "parser": "babel-eslint",
+ "parser": "typescript-eslint-parser",
"plugins": [
"react",
- "import"
+ "import",
+ "typescript"
],
"rules": {
- "comma-dangle": 0, // not sure why airbnb turned this on. gross!
- "indent": [2, 2, {"SwitchCase": 1}],
- "react/prefer-stateless-function": 0,
- "react/prop-types": 0,
- "react/jsx-closing-bracket-location": 0,
- "no-console": 0,
- "prefer-template": 0,
- "max-len": 0,
- "no-underscore-dangle": [2, {"allow": ["__data"]}],
- "global-require": 0,
- "no-restricted-syntax": 0,
- "linebreak-style": 0,
- "react/jsx-filename-extension": 0,
- "import/imports-first": 0,
- "no-class-assign": 0
+ "comma-dangle": "off", // Not sure why airbnb turned this on. Gross!
+ "indent": ["error", 2, {"SwitchCase": 1}],
+ "react/prefer-stateless-function": "off",
+ "react/prop-types": "off",
+ "react/jsx-closing-bracket-location": "off",
+ "no-console": "off",
+ "prefer-template": "off",
+ "max-len": "off",
+ "no-underscore-dangle": ["error", {"allow": ["__data"]}],
+ "global-require": "off",
+ "no-restricted-syntax": "off",
+ "linebreak-style": "off",
+ "react/jsx-filename-extension": "off",
+ "arrow-parens": ["error", "always"],
+ "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
+ "jsx-a11y/anchor-is-valid": ["error", {
+ "specialLink": ["to"] // Allow "to" property in react-router-dom Link element
+ }],
+ "no-undef": "off", // TypeScript lint parser produces false positves; the compiler will error anyway
+ "no-unused-vars": "off", // TypeScript lint parser produces false positves; the compiler is set to error instead
+ "import/extensions": ["error", "ignorePackages", {
+ "js": "never",
+ "jsx": "never",
+ "ts": "never",
+ "tsx": "never"
+ }],
+ "react/sort-comp": ["error", { // Add type-annotations to the top
+ "order": [
+ "static-methods",
+ "type-annotations",
+ "instance-variables",
+ "lifecycle",
+ "/^on.+$/",
+ "getters",
+ "setters",
+ "/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/",
+ "instance-methods",
+ "everything-else",
+ "rendering"
+ ]
+ }],
+ "typescript/adjacent-overload-signatures": ["error"],
+ "typescript/class-name-casing": ["error"],
+ "typescript/explicit-member-accessibility": ["error"],
+ "typescript/interface-name-prefix": ["error", "never"],
+ "typescript/member-delimiter-style": ["error"],
+ "typescript/no-angle-bracket-type-assertion": ["error"],
+ "typescript/no-array-constructor": ["error"],
+ "typescript/no-empty-interface": ["error"],
+ "typescript/no-inferrable-types": ["error"],
+ "typescript/no-unused-vars": ["error"],
+ "typescript/no-use-before-define": ["error"],
+ "typescript/type-annotation-spacing": ["error"]
},
"settings": {
- "import/parser": "babel-eslint",
- "import/resolver": {
+ "import/parser": "typescript-eslint-parser",
+ "import/resolver": {
"node": {
- "moduleDirectory": ["node_modules", "Scripts"]
+ "moduleDirectory": ["node_modules", "Scripts"],
+ "extensions": [".js", ".jsx", ".ts", ".tsx"]
}
},
+ "import/extensions": [".js", ".jsx", ".ts", ".tsx"],
"no-underscore-dangle": {
"allow": ["__data"]
}
diff --git a/src/ReactBoilerplate/.stylelintrc b/src/ReactBoilerplate/.stylelintrc
new file mode 100644
index 0000000..77bc086
--- /dev/null
+++ b/src/ReactBoilerplate/.stylelintrc
@@ -0,0 +1,29 @@
+{
+ "extends": [
+ "stylelint-config-standard"
+ ],
+ "rules": {
+ 'selector-pseudo-class-no-unknown': [ true, {
+ ignorePseudoClasses: [
+ 'export',
+ 'import',
+ 'global',
+ 'local'
+ ],
+ }],
+ 'property-no-unknown': [ true, {
+ ignoreProperties: [
+ 'composes',
+ 'compose-with'
+ ],
+ }],
+ 'at-rule-no-unknown': [ true, {
+ ignoreAtRules: [
+ 'each',
+ 'extend',
+ 'include',
+ 'mixin',
+ ],
+ }]
+ }
+}
diff --git a/src/ReactBoilerplate/Scripts/client.js b/src/ReactBoilerplate/Scripts/client.js
deleted file mode 100644
index 704bc92..0000000
--- a/src/ReactBoilerplate/Scripts/client.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { Router, browserHistory } from 'react-router';
-import getRoutes from './routes';
-import { Provider } from 'react-redux';
-import configureStore from './redux/configureStore';
-import { syncHistoryWithStore } from 'react-router-redux';
-import ApiClient from './helpers/ApiClient';
-
-const client = new ApiClient();
-const store = configureStore(window.__data, browserHistory, client);
-const history = syncHistoryWithStore(browserHistory, store);
-
-ReactDOM.render(
-
-
- {getRoutes(store)}
-
- ,
- document.getElementById('content')
-);
diff --git a/src/ReactBoilerplate/Scripts/client.tsx b/src/ReactBoilerplate/Scripts/client.tsx
new file mode 100644
index 0000000..0217c3e
--- /dev/null
+++ b/src/ReactBoilerplate/Scripts/client.tsx
@@ -0,0 +1,36 @@
+import * as React from 'react';
+import * as ReactDOM from 'react-dom';
+import { Provider } from 'react-redux';
+import createHistory from 'history/createBrowserHistory';
+import { ConnectedRouter } from 'react-router-redux';
+import { AppContainer } from 'react-hot-loader';
+import configureStore from './redux/configureStore';
+import ApiClient from './helpers/ApiClient';
+
+// Need to import App directly from its file so that this module is dependent on it directly for HMR
+import App from './containers/App/App';
+
+const client = new ApiClient();
+const history = createHistory();
+const store = configureStore((window as any).__data, history, client);
+
+const render = () => {
+ ReactDOM.render(
+
+
+
+
+
+
+ ,
+ document.getElementById('content')
+ );
+};
+
+// Render the App inside the store and HMR AppContainer
+render();
+
+// Register to accept changes to the App
+if (module.hot) {
+ module.hot.accept('./containers/App/App', () => { render(); });
+}
diff --git a/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.js b/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.tsx
similarity index 66%
rename from src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.js
rename to src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.tsx
index 24281da..77c0e83 100644
--- a/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.js
+++ b/src/ReactBoilerplate/Scripts/components/ChangeEmailForm/ChangeEmailForm.tsx
@@ -1,19 +1,20 @@
-import React from 'react';
-import Form from 'components/Form';
+import * as React from 'react';
import { reduxForm } from 'redux-form';
-import { Input } from 'components';
-import { changeEmail } from 'redux/modules/manage';
+import { RootState } from '../../redux/reducer';
+import Form from '../Form';
+import { Input } from '../../components';
+import { changeEmail } from '../../redux/modules/manage/email';
-class ChangeEmailForm extends Form {
- constructor(props) {
+class ChangeEmailForm extends Form<{}, { success: boolean }> {
+ public constructor(props: any) {
super(props);
this.success = this.success.bind(this);
this.state = { success: false };
}
- success() {
+ public success() {
this.setState({ success: true });
}
- render() {
+ public render() {
const {
fields: { currentPassword, email, emailConfirm }
} = this.props;
@@ -27,7 +28,7 @@ class ChangeEmailForm extends Form {
An email has been sent to your email to confirm the change.
}
- {!success &&
+ {!success && currentPassword && email && emailConfirm &&