+);
diff --git a/hooks/02_Properties/webpack.config.js b/hooks/02_Properties/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/02_Properties/webpack.config.js
+++ b/hooks/02_Properties/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/03_State/Readme.md b/hooks/03_State/Readme.md
index 79ca23ea..d56155d6 100644
--- a/hooks/03_State/Readme.md
+++ b/hooks/03_State/Readme.md
@@ -12,7 +12,7 @@ We take as a starting point the example _02 Properties_:
username (with default value "defaultUserName").
This _App_ component renders the _Hello_ component. At first we create a simple stateless
_App_ component.
-- Update _main.tsx_ file to include our _App_ component.
+- Update _index.tsx_ file to include our _App_ component.
- Change _App_ component to a stateful class component to hold the _userName_ state.
- Create a _NameEdit_ component to let the user change the value of username. This changes the _App_ state
using a function from _App_.
@@ -66,8 +66,8 @@ _./src/index.tsx_
```
- It's time to revisit _app.tsx_. We want to store the user's name and let the user updated it. We will use hooks to
- allow _App_ fucntional components to make use of state (this works in React 16.8.2 and above if you have to use
- older verions you have to use a class component, check the "old*classes_components" on the root of this repo for example).
+ allow _App_ functional components to make use of state (this works in React 16.8.2 and above if you have to use
+ older versions you have to use a class component, check the "old*classes_components" on the root of this repo for example).
We will add \_userName* to the state.
Let's move this component to a class stateful component and define a state including _userName_, and pass this value to the _Hello_ component.
@@ -134,7 +134,6 @@ _./src/app.tsx_
import * as React from "react";
import { HelloComponent } from "./hello";
import { NameEditComponent } from './nameEdit';
-import { NameEditComponent } from './nameEdit';
export const App = () => {
diff --git a/hooks/03_State/package.json b/hooks/03_State/package.json
index bfa0e4eb..264e71d2 100644
--- a/hooks/03_State/package.json
+++ b/hooks/03_State/package.json
@@ -15,27 +15,27 @@
"author": "Braulio Diez Botella",
"license": "MIT",
"devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/polyfill": "^7.2.5",
- "@babel/preset-env": "^7.3.1",
- "@types/react": "^16.8.3",
- "@types/react-dom": "^16.8.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.10.5",
+ "@babel/polyfill": "^7.10.4",
+ "@babel/preset-env": "^7.10.4",
+ "@types/react": "^16.9.43",
+ "@types/react-dom": "^16.9.8",
"awesome-typescript-loader": "^5.2.1",
- "babel-loader": "^8.0.5",
- "css-loader": "^2.1.0",
- "file-loader": "^3.0.1",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.5.0",
- "style-loader": "^0.23.1",
- "typescript": "^3.3.3",
- "url-loader": "^1.1.2",
- "webpack": "^4.29.3",
- "webpack-cli": "^3.2.3",
- "webpack-dev-server": "^3.1.14"
+ "babel-loader": "^8.1.0",
+ "css-loader": "^3.6.0",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.3.0",
+ "mini-css-extract-plugin": "^0.9.0",
+ "style-loader": "^1.2.1",
+ "typescript": "^3.9.7",
+ "url-loader": "^4.1.0",
+ "webpack": "^4.43.0",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0"
},
"dependencies": {
- "react": "^16.8.2",
- "react-dom": "^16.8.2"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/hooks/03_State/src/app.tsx b/hooks/03_State/src/app.tsx
index 00523d10..f7d14292 100644
--- a/hooks/03_State/src/app.tsx
+++ b/hooks/03_State/src/app.tsx
@@ -3,7 +3,7 @@ import { HelloComponent } from "./hello";
import { NameEditComponent } from "./nameEdit";
export const App = () => {
- const [name, setName] = React.useState("defaultUserName");
+ const [name, setName] = React.useState("initialName");
const setUsernameState = (event: React.ChangeEvent) => {
setName(event.target.value);
diff --git a/hooks/03_State/src/hello.tsx b/hooks/03_State/src/hello.tsx
index 33a0ab73..b6b7aca4 100644
--- a/hooks/03_State/src/hello.tsx
+++ b/hooks/03_State/src/hello.tsx
@@ -4,6 +4,6 @@ interface Props {
userName: string;
}
-export const HelloComponent = (props: Props) => {
+export const HelloComponent: React.FC = (props) => {
return
Hello user: {props.userName} !
;
};
diff --git a/hooks/03_State/src/nameEdit.tsx b/hooks/03_State/src/nameEdit.tsx
index f8691f70..02f7e36d 100644
--- a/hooks/03_State/src/nameEdit.tsx
+++ b/hooks/03_State/src/nameEdit.tsx
@@ -2,10 +2,10 @@ import * as React from "react";
interface Props {
userName: string;
- onChange: (e : React.ChangeEvent) => void;
+ onChange: (e: React.ChangeEvent) => void;
}
-export const NameEditComponent = (props: Props) => (
+export const NameEditComponent: React.FC = (props) => (
<>
diff --git a/hooks/03_State/webpack.config.js b/hooks/03_State/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/03_State/webpack.config.js
+++ b/hooks/03_State/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/04_Callback/Readme.md b/hooks/04_Callback/Readme.md
index 32246bd4..9612b559 100644
--- a/hooks/04_Callback/Readme.md
+++ b/hooks/04_Callback/Readme.md
@@ -1,6 +1,6 @@
# 04 Callback + State
-In this example we refactor the previous **03 State** example.
+In this example we will refactor the previous **03 State** example.
We will update the name property only when the user clicks a _change_ button, and we will simplify the prop callback signature.
@@ -14,7 +14,7 @@ Obviously, we take the example **03 State** as a starting point.
## Prerequisites
-Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0) if they are not already installed on your computer.
+Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0 or higher) if they are not already installed on your computer.
> Verify that you are running at least node v6.x.x and npm 3.x.x by running `node -v` and `npm -v` in a terminal/console window. Older versions may produce errors.
diff --git a/hooks/04_Callback/package.json b/hooks/04_Callback/package.json
index bfa0e4eb..264e71d2 100644
--- a/hooks/04_Callback/package.json
+++ b/hooks/04_Callback/package.json
@@ -15,27 +15,27 @@
"author": "Braulio Diez Botella",
"license": "MIT",
"devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/polyfill": "^7.2.5",
- "@babel/preset-env": "^7.3.1",
- "@types/react": "^16.8.3",
- "@types/react-dom": "^16.8.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.10.5",
+ "@babel/polyfill": "^7.10.4",
+ "@babel/preset-env": "^7.10.4",
+ "@types/react": "^16.9.43",
+ "@types/react-dom": "^16.9.8",
"awesome-typescript-loader": "^5.2.1",
- "babel-loader": "^8.0.5",
- "css-loader": "^2.1.0",
- "file-loader": "^3.0.1",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.5.0",
- "style-loader": "^0.23.1",
- "typescript": "^3.3.3",
- "url-loader": "^1.1.2",
- "webpack": "^4.29.3",
- "webpack-cli": "^3.2.3",
- "webpack-dev-server": "^3.1.14"
+ "babel-loader": "^8.1.0",
+ "css-loader": "^3.6.0",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.3.0",
+ "mini-css-extract-plugin": "^0.9.0",
+ "style-loader": "^1.2.1",
+ "typescript": "^3.9.7",
+ "url-loader": "^4.1.0",
+ "webpack": "^4.43.0",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0"
},
"dependencies": {
- "react": "^16.8.2",
- "react-dom": "^16.8.2"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/hooks/04_Callback/src/hello.tsx b/hooks/04_Callback/src/hello.tsx
index 33a0ab73..b6b7aca4 100644
--- a/hooks/04_Callback/src/hello.tsx
+++ b/hooks/04_Callback/src/hello.tsx
@@ -4,6 +4,6 @@ interface Props {
userName: string;
}
-export const HelloComponent = (props: Props) => {
+export const HelloComponent: React.FC = (props) => {
return
Hello user: {props.userName} !
;
};
diff --git a/hooks/04_Callback/src/nameEdit.tsx b/hooks/04_Callback/src/nameEdit.tsx
index 53edae95..8b1a5ca8 100644
--- a/hooks/04_Callback/src/nameEdit.tsx
+++ b/hooks/04_Callback/src/nameEdit.tsx
@@ -5,7 +5,7 @@ interface Props {
onNameUpdated: (newName: string) => any;
}
-export const NameEditComponent = (props: Props) => {
+export const NameEditComponent: React.FC = (props) => {
const [editingName, setEditingName] = React.useState(props.initialUserName);
const onChange = (e: React.ChangeEvent) => {
diff --git a/hooks/04_Callback/webpack.config.js b/hooks/04_Callback/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/04_Callback/webpack.config.js
+++ b/hooks/04_Callback/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/05_Refactor/Readme.md b/hooks/05_Refactor/Readme.md
index 2cb2c577..11b31907 100644
--- a/hooks/05_Refactor/Readme.md
+++ b/hooks/05_Refactor/Readme.md
@@ -62,7 +62,7 @@ We can think about two possible solutions:
- The child components remains as pure presentational.
- You have only one place where the updates is being done, and you have control over it, you could easily change behavior
- e.g. if the user has alreay started typing then name then ignore the ajax call.
+ e.g. if the user has already started typing the name then ignore the ajax call.
We will stick with the second approach in the rest of examples, but we encourage you to go through both approaches as
a learning excercise.
@@ -119,7 +119,7 @@ The first approach was fine if we come from a Java / C# mindset, but it has it d
what would happen if we want the editingName to be preserved if it has changed (async call
would have no effect then)? When there is a conflict updating the state between parent and child
a valid solution is to lift up that part of the state to the parent component, if we follow
-this approach we can endup having the core state isolated in container components (prior step
+this approach we can end up having the core state isolated in container components (prior step
to build redux on top of it).
Let's see how can we lift up this state:
@@ -163,6 +163,50 @@ export const App = () => {
};
```
+- Now let's jump into _NameEditComponent_ and update the contract and
+ implementation:
+
+```diff
+interface Props {
+ initialUserName: string;
++ editingName: string;
+- onNameUpdated: (newName: string) => any;
++ onNameUpdated: () => any;
++ onEditingNameUpdated: (newEditingName: string) => any;
+}
+```
+
+```diff
+export const NameEditComponent = (props: Props) => {
+- const [editingName, setEditingName] = React.useState(props.initialUserName);
+
+ const onChange = (e: React.ChangeEvent) => {
+- setEditingName(e.target.value);
++ props.onEditingNameUpdated(e.target.value);
+ };
+
+ const onNameSubmit = (event: any): any => {
+- props.onNameUpdated(editingName);
++ props.onNameUpdated();
+ };
+
+- if(props.initialUserName !== lastInitialName) {
+- setLastInitialName(props.initialUserName);
+- setEditingName(props.initialUserName);
+- }
+
+
+ return (
+ <>
+
+-
++
+
+ >
+ );
+};
+```
+
# About Basefactor + Lemoncode
We are an innovating team of Javascript experts, passionate about turning your ideas into robust products.
@@ -172,4 +216,3 @@ We are an innovating team of Javascript experts, passionate about turning your i
[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services.
For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend
-
diff --git a/hooks/05_Refactor/package.json b/hooks/05_Refactor/package.json
index bfa0e4eb..264e71d2 100644
--- a/hooks/05_Refactor/package.json
+++ b/hooks/05_Refactor/package.json
@@ -15,27 +15,27 @@
"author": "Braulio Diez Botella",
"license": "MIT",
"devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/polyfill": "^7.2.5",
- "@babel/preset-env": "^7.3.1",
- "@types/react": "^16.8.3",
- "@types/react-dom": "^16.8.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.10.5",
+ "@babel/polyfill": "^7.10.4",
+ "@babel/preset-env": "^7.10.4",
+ "@types/react": "^16.9.43",
+ "@types/react-dom": "^16.9.8",
"awesome-typescript-loader": "^5.2.1",
- "babel-loader": "^8.0.5",
- "css-loader": "^2.1.0",
- "file-loader": "^3.0.1",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.5.0",
- "style-loader": "^0.23.1",
- "typescript": "^3.3.3",
- "url-loader": "^1.1.2",
- "webpack": "^4.29.3",
- "webpack-cli": "^3.2.3",
- "webpack-dev-server": "^3.1.14"
+ "babel-loader": "^8.1.0",
+ "css-loader": "^3.6.0",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.3.0",
+ "mini-css-extract-plugin": "^0.9.0",
+ "style-loader": "^1.2.1",
+ "typescript": "^3.9.7",
+ "url-loader": "^4.1.0",
+ "webpack": "^4.43.0",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0"
},
"dependencies": {
- "react": "^16.8.2",
- "react-dom": "^16.8.2"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/hooks/05_Refactor/src/hello.tsx b/hooks/05_Refactor/src/hello.tsx
index 33a0ab73..b6b7aca4 100644
--- a/hooks/05_Refactor/src/hello.tsx
+++ b/hooks/05_Refactor/src/hello.tsx
@@ -4,6 +4,6 @@ interface Props {
userName: string;
}
-export const HelloComponent = (props: Props) => {
+export const HelloComponent: React.FC = (props) => {
return
Hello user: {props.userName} !
;
};
diff --git a/hooks/05_Refactor/src/nameEdit.tsx b/hooks/05_Refactor/src/nameEdit.tsx
index 57f53dd0..c2eebf31 100644
--- a/hooks/05_Refactor/src/nameEdit.tsx
+++ b/hooks/05_Refactor/src/nameEdit.tsx
@@ -7,7 +7,7 @@ interface Props {
onEditingNameUpdated: (newEditingName: string) => any;
}
-export const NameEditComponent = (props: Props) => {
+export const NameEditComponent: React.FC = (props) => {
const onChange = (e: React.ChangeEvent) => {
props.onEditingNameUpdated(e.target.value);
};
diff --git a/hooks/05_Refactor/webpack.config.js b/hooks/05_Refactor/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/05_Refactor/webpack.config.js
+++ b/hooks/05_Refactor/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/06_Enable/Readme.md b/hooks/06_Enable/Readme.md
index 01163edb..1b784daf 100644
--- a/hooks/06_Enable/Readme.md
+++ b/hooks/06_Enable/Readme.md
@@ -3,7 +3,7 @@
Let's continue with the update name sample, this time we want to disable the
"update" button when the input is empty or when the value hasn't changed.
-We will take a startup point sample _05 Refactor_
+We will take as startup point sample _05 Refactor_
## Summary steps:
@@ -22,8 +22,6 @@ We will take a startup point sample _05 Refactor_
npm install
```
-- Let's start by adding
-
- Let's start by adding a condition to disable the field whenever is empty.
_./src/nameEdit.tsx_
@@ -46,14 +44,14 @@ _./src/nameEdit.tsx_
the editingName is empty and when the editing name is the same as the finalname, how
could we do that? We can choose between two options here:
- - [Excercise] Add to the disabled option on more condition and cover the case where
+ - [Excercise] Add to the disabled option one more condition and cover the case where
the editingName is equal to the initialName.
- Create a generic disable property on the NameEditComponent and let the parent control
decide in which cases it should be disabled (thanks to [Victor Borrego](https://github.com/v-borrego) to point out this great solution).
-We will follow the second approach since is the one that can provide more flexiblity to the
-control (in a real project, choosing between one approach or the other depends on specfication details).
+We will follow the second approach since is the one that can provide more flexibility to the
+control (in a real project, choosing between one approach or the other depends on specification details).
We will expose a _disabled_ property in the _NameEdit_ component.
@@ -99,7 +97,7 @@ npm start
# Excercise
-Ideas to further implement this, let's convert our NamEdit component to a password
+Ideas to further implement this, let's convert our NameEdit component to a password
strength component:
- Extract the disabled condition to function.
diff --git a/hooks/06_Enable/package.json b/hooks/06_Enable/package.json
index bfa0e4eb..264e71d2 100644
--- a/hooks/06_Enable/package.json
+++ b/hooks/06_Enable/package.json
@@ -15,27 +15,27 @@
"author": "Braulio Diez Botella",
"license": "MIT",
"devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/polyfill": "^7.2.5",
- "@babel/preset-env": "^7.3.1",
- "@types/react": "^16.8.3",
- "@types/react-dom": "^16.8.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.10.5",
+ "@babel/polyfill": "^7.10.4",
+ "@babel/preset-env": "^7.10.4",
+ "@types/react": "^16.9.43",
+ "@types/react-dom": "^16.9.8",
"awesome-typescript-loader": "^5.2.1",
- "babel-loader": "^8.0.5",
- "css-loader": "^2.1.0",
- "file-loader": "^3.0.1",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.5.0",
- "style-loader": "^0.23.1",
- "typescript": "^3.3.3",
- "url-loader": "^1.1.2",
- "webpack": "^4.29.3",
- "webpack-cli": "^3.2.3",
- "webpack-dev-server": "^3.1.14"
+ "babel-loader": "^8.1.0",
+ "css-loader": "^3.6.0",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.3.0",
+ "mini-css-extract-plugin": "^0.9.0",
+ "style-loader": "^1.2.1",
+ "typescript": "^3.9.7",
+ "url-loader": "^4.1.0",
+ "webpack": "^4.43.0",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0"
},
"dependencies": {
- "react": "^16.8.2",
- "react-dom": "^16.8.2"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/hooks/06_Enable/src/hello.tsx b/hooks/06_Enable/src/hello.tsx
index 33a0ab73..b6b7aca4 100644
--- a/hooks/06_Enable/src/hello.tsx
+++ b/hooks/06_Enable/src/hello.tsx
@@ -4,6 +4,6 @@ interface Props {
userName: string;
}
-export const HelloComponent = (props: Props) => {
+export const HelloComponent: React.FC = (props) => {
return
Hello user: {props.userName} !
;
};
diff --git a/hooks/06_Enable/src/nameEdit.tsx b/hooks/06_Enable/src/nameEdit.tsx
index 646a4d26..7f9091e3 100644
--- a/hooks/06_Enable/src/nameEdit.tsx
+++ b/hooks/06_Enable/src/nameEdit.tsx
@@ -5,10 +5,10 @@ interface Props {
editingName: string;
onNameUpdated: () => any;
onEditingNameUpdated: (newEditingName: string) => any;
- disabled : boolean;
+ disabled: boolean;
}
-export const NameEditComponent = (props: Props) => {
+export const NameEditComponent: React.FC = (props) => {
const onChange = (e: React.ChangeEvent) => {
props.onEditingNameUpdated(e.target.value);
};
@@ -21,10 +21,9 @@ export const NameEditComponent = (props: Props) => {
<>
-
+
>
);
};
diff --git a/hooks/06_Enable/webpack.config.js b/hooks/06_Enable/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/06_Enable/webpack.config.js
+++ b/hooks/06_Enable/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/07_ColorPicker/Readme.md b/hooks/07_ColorPicker/Readme.md
index 18584b9f..2f0096e1 100644
--- a/hooks/07_ColorPicker/Readme.md
+++ b/hooks/07_ColorPicker/Readme.md
@@ -1,14 +1,13 @@
# 07 Colopicker
-In this sample we are going to implement a color picker component, it will allow us to choose
-between red / green / blue components and displaty the resulting color.
+In this sample we are going to implement a color picker component, it will allow us to choose between red / green / blue components and display the resulting color.
-We will start with a non optimal implementation and we will refactor it in sample 09.
+We will start with a non optimal implementation and we will refactor it in sample 08.
# Steps
- We will take as starting point sample _06 Enable_, copy the content from that project and
- exectue _npm install_
+ execute _npm install_
```bash
npm install
@@ -18,14 +17,14 @@ npm install
- We will create a components folder under _src_ (_src/components_).
-- Inside that components folder we will copy all the components that we have created (_nameEdit_
+- Inside that components folder we will copy all the components that we have created (_nameEdit.tsx_
and _hello.tsx_).
- We will create an _index_ file under _src/components_ and create a barrel (by doing this, later
on we can refactor the content of the components folder without affecting other external files in
the application that may import them).
-_./src/index.tsx_
+_./src/components/index.tsx_
```typescript
export * from "./hello";
@@ -47,7 +46,7 @@ export interface Color {
- Let's start by creating a _ColorBrowser_ component: this color will just display the
selected color (under the hood is just a div, by applying css styling we provide a width
- and height to that rectanble, and a background color).
+ and height to that rectangle, and a background color).
_./src/components/colorBrowser.tsx_
@@ -128,7 +127,7 @@ _./src/app.tsx_
npm start
```
-- We want add color picker editing capabilities, let's create a color picker component
+- We want to add color picker editing capabilities, let's create a color picker component
and add a single slider for one of the color values.
_./src/components/colorpicker.tsx_
@@ -252,7 +251,7 @@ export const ColorPicker = (props: Props) => (
+ )}
+ />
+ {props.color.blue}
-+
++
);
```
@@ -262,6 +261,7 @@ export const ColorPicker = (props: Props) => (
```bash
npm start
```
+
# About Basefactor + Lemoncode
We are an innovating team of Javascript experts, passionate about turning your ideas into robust products.
diff --git a/hooks/07_ColorPicker/package.json b/hooks/07_ColorPicker/package.json
index bfa0e4eb..264e71d2 100644
--- a/hooks/07_ColorPicker/package.json
+++ b/hooks/07_ColorPicker/package.json
@@ -15,27 +15,27 @@
"author": "Braulio Diez Botella",
"license": "MIT",
"devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/polyfill": "^7.2.5",
- "@babel/preset-env": "^7.3.1",
- "@types/react": "^16.8.3",
- "@types/react-dom": "^16.8.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.10.5",
+ "@babel/polyfill": "^7.10.4",
+ "@babel/preset-env": "^7.10.4",
+ "@types/react": "^16.9.43",
+ "@types/react-dom": "^16.9.8",
"awesome-typescript-loader": "^5.2.1",
- "babel-loader": "^8.0.5",
- "css-loader": "^2.1.0",
- "file-loader": "^3.0.1",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.5.0",
- "style-loader": "^0.23.1",
- "typescript": "^3.3.3",
- "url-loader": "^1.1.2",
- "webpack": "^4.29.3",
- "webpack-cli": "^3.2.3",
- "webpack-dev-server": "^3.1.14"
+ "babel-loader": "^8.1.0",
+ "css-loader": "^3.6.0",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.3.0",
+ "mini-css-extract-plugin": "^0.9.0",
+ "style-loader": "^1.2.1",
+ "typescript": "^3.9.7",
+ "url-loader": "^4.1.0",
+ "webpack": "^4.43.0",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0"
},
"dependencies": {
- "react": "^16.8.2",
- "react-dom": "^16.8.2"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/hooks/07_ColorPicker/src/components/colorBrowser.tsx b/hooks/07_ColorPicker/src/components/colorBrowser.tsx
index 7dcc3905..813746ce 100644
--- a/hooks/07_ColorPicker/src/components/colorBrowser.tsx
+++ b/hooks/07_ColorPicker/src/components/colorBrowser.tsx
@@ -5,13 +5,11 @@ interface Props {
color: Color;
}
-export const ColorBrowser = (props: Props) => {
+export const ColorBrowser: React.FC = (props) => {
const divStyle: React.CSSProperties = {
width: "11rem",
height: "7rem",
- backgroundColor: `rgb(${props.color.red},${props.color.green}, ${
- props.color.blue
- })`
+ backgroundColor: `rgb(${props.color.red},${props.color.green}, ${props.color.blue})`,
};
return ;
diff --git a/hooks/07_ColorPicker/src/components/colorPicker.tsx b/hooks/07_ColorPicker/src/components/colorPicker.tsx
index 91283505..23bdca3c 100644
--- a/hooks/07_ColorPicker/src/components/colorPicker.tsx
+++ b/hooks/07_ColorPicker/src/components/colorPicker.tsx
@@ -6,7 +6,7 @@ interface Props {
onColorUpdated: (color: Color) => void;
}
-export const ColorPicker = (props: Props) => (
+export const ColorPicker: React.FC = (props) => (
;
};
diff --git a/hooks/07_ColorPicker/src/components/nameEdit.tsx b/hooks/07_ColorPicker/src/components/nameEdit.tsx
index 646a4d26..5461073e 100644
--- a/hooks/07_ColorPicker/src/components/nameEdit.tsx
+++ b/hooks/07_ColorPicker/src/components/nameEdit.tsx
@@ -8,7 +8,7 @@ interface Props {
disabled : boolean;
}
-export const NameEditComponent = (props: Props) => {
+export const NameEditComponent: React.FC = (props) => {
const onChange = (e: React.ChangeEvent) => {
props.onEditingNameUpdated(e.target.value);
};
diff --git a/hooks/07_ColorPicker/webpack.config.js b/hooks/07_ColorPicker/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/07_ColorPicker/webpack.config.js
+++ b/hooks/07_ColorPicker/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/08_ColorPickerRefactor/Readme.md b/hooks/08_ColorPickerRefactor/Readme.md
index 6d404b2d..3597b75a 100644
--- a/hooks/08_ColorPickerRefactor/Readme.md
+++ b/hooks/08_ColorPickerRefactor/Readme.md
@@ -1,12 +1,12 @@
-# 09 Colorpicker Refactor
+# 08 Colorpicker Refactor
-In this example we are going to review the colorpicker component we have created and simplify it. Right now we have three slider controls with many details that make our HTML hard to read. Let's componentize this scenario.
+In this example we are going to review the colorPicker component we have created and simplify it. Right now we have three slider controls with many details that make our HTML hard to read. Let's componentize this scenario.
-We take _07 Colorpicker_ as reference.
+We take _07 ColorPicker_ as reference.
# Steps to reproduce this sample
-- Copy the content from _07 Colorpicker_ and execute _npm install_.
+- Copy the content from _07 ColorPicker_ and execute _npm install_.
```bash
npm install
@@ -37,9 +37,9 @@ _DO NOT COPY THIS_
- We will start our refactor by componentizing this piece of code,
let's append to the _colorPicker.tsx_ the following component
- (another approach could be to create a new file, that's a tetchty decisition,
+ (another approach could be to create a new file, that's a tetchy decision,
on one hand it makes sense to keep in the same file the slider since is quite
- related to the picker, on the other segregating in diferent files is another
+ related to the picker, on the other segregating in different files is another
valid approach).
_./src/components/colorPicker.tsx_
@@ -219,13 +219,13 @@ export const ColorPicker = (props: Props) => {
}
```
-- Now we got a great result !! we have enhanced code quality in our component.
+- Now we got a great result!! We have enhanced code quality in our component.
```bash
npm start
```
-- Could we go one step furhter refactoring? The answer is yes, could it be worth? That's
+- Could we go one step further refactoring? The answer is yes, could it be worth? That's
worth a discussion, sometimes is a good idea to keep on refactoring, and then rollback
one step, let's apply the following trick, let's replace our color picker with this code:
diff --git a/hooks/08_ColorPickerRefactor/package.json b/hooks/08_ColorPickerRefactor/package.json
index bfa0e4eb..264e71d2 100644
--- a/hooks/08_ColorPickerRefactor/package.json
+++ b/hooks/08_ColorPickerRefactor/package.json
@@ -15,27 +15,27 @@
"author": "Braulio Diez Botella",
"license": "MIT",
"devDependencies": {
- "@babel/cli": "^7.2.3",
- "@babel/core": "^7.2.2",
- "@babel/polyfill": "^7.2.5",
- "@babel/preset-env": "^7.3.1",
- "@types/react": "^16.8.3",
- "@types/react-dom": "^16.8.1",
+ "@babel/cli": "^7.10.5",
+ "@babel/core": "^7.10.5",
+ "@babel/polyfill": "^7.10.4",
+ "@babel/preset-env": "^7.10.4",
+ "@types/react": "^16.9.43",
+ "@types/react-dom": "^16.9.8",
"awesome-typescript-loader": "^5.2.1",
- "babel-loader": "^8.0.5",
- "css-loader": "^2.1.0",
- "file-loader": "^3.0.1",
- "html-webpack-plugin": "^3.2.0",
- "mini-css-extract-plugin": "^0.5.0",
- "style-loader": "^0.23.1",
- "typescript": "^3.3.3",
- "url-loader": "^1.1.2",
- "webpack": "^4.29.3",
- "webpack-cli": "^3.2.3",
- "webpack-dev-server": "^3.1.14"
+ "babel-loader": "^8.1.0",
+ "css-loader": "^3.6.0",
+ "file-loader": "^6.0.0",
+ "html-webpack-plugin": "^4.3.0",
+ "mini-css-extract-plugin": "^0.9.0",
+ "style-loader": "^1.2.1",
+ "typescript": "^3.9.7",
+ "url-loader": "^4.1.0",
+ "webpack": "^4.43.0",
+ "webpack-cli": "^3.3.12",
+ "webpack-dev-server": "^3.11.0"
},
"dependencies": {
- "react": "^16.8.2",
- "react-dom": "^16.8.2"
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1"
}
}
diff --git a/hooks/08_ColorPickerRefactor/src/components/colorBrowser.tsx b/hooks/08_ColorPickerRefactor/src/components/colorBrowser.tsx
index 7dcc3905..a28e438e 100644
--- a/hooks/08_ColorPickerRefactor/src/components/colorBrowser.tsx
+++ b/hooks/08_ColorPickerRefactor/src/components/colorBrowser.tsx
@@ -5,7 +5,7 @@ interface Props {
color: Color;
}
-export const ColorBrowser = (props: Props) => {
+export const ColorBrowser: React.FC = (props) => {
const divStyle: React.CSSProperties = {
width: "11rem",
height: "7rem",
diff --git a/hooks/08_ColorPickerRefactor/src/components/colorPicker.tsx b/hooks/08_ColorPickerRefactor/src/components/colorPicker.tsx
index ea449834..ff4cbcdc 100644
--- a/hooks/08_ColorPickerRefactor/src/components/colorPicker.tsx
+++ b/hooks/08_ColorPickerRefactor/src/components/colorPicker.tsx
@@ -6,15 +6,15 @@ interface Props {
onColorUpdated: (color: Color) => void;
}
-const updateColor = (props: Props, colorId: keyof Color) => value => {
+const updateColor = (props: Props, colorId: keyof Color) => (value: any) => {
// keyof Color ensures only 'red', 'blue' or 'green' can be passed in.
props.onColorUpdated({
...props.color, // this creates a clone of the current props.color object...
- [colorId]: value // ... which gets one of its properties (colorId) immediately replaced by a new value.
+ [colorId]: value, // ... which gets one of its properties (colorId) immediately replaced by a new value.
});
};
-export const ColorPicker = (props: Props) => (
+export const ColorPicker: React.FC = (props) => (
;
};
diff --git a/hooks/08_ColorPickerRefactor/src/components/nameEdit.tsx b/hooks/08_ColorPickerRefactor/src/components/nameEdit.tsx
index 646a4d26..7f9091e3 100644
--- a/hooks/08_ColorPickerRefactor/src/components/nameEdit.tsx
+++ b/hooks/08_ColorPickerRefactor/src/components/nameEdit.tsx
@@ -5,10 +5,10 @@ interface Props {
editingName: string;
onNameUpdated: () => any;
onEditingNameUpdated: (newEditingName: string) => any;
- disabled : boolean;
+ disabled: boolean;
}
-export const NameEditComponent = (props: Props) => {
+export const NameEditComponent: React.FC = (props) => {
const onChange = (e: React.ChangeEvent) => {
props.onEditingNameUpdated(e.target.value);
};
@@ -21,10 +21,9 @@ export const NameEditComponent = (props: Props) => {
<>
-
+
>
);
};
diff --git a/hooks/08_ColorPickerRefactor/webpack.config.js b/hooks/08_ColorPickerRefactor/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/08_ColorPickerRefactor/webpack.config.js
+++ b/hooks/08_ColorPickerRefactor/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/09_Sidebar/Readme.md b/hooks/09_Sidebar/Readme.md
index 3a5ec64f..e3dda427 100644
--- a/hooks/09_Sidebar/Readme.md
+++ b/hooks/09_Sidebar/Readme.md
@@ -12,7 +12,7 @@ implementation, then we will make it generic.
npm install
```
-- Create a file called _src/sidebar.css_ and add the following styles (http://www.w3schools.com/howto/howto_js_sidenav.asp):
+- Create a file called _src/components/sidebar.css_ and add the following styles (http://www.w3schools.com/howto/howto_js_sidenav.asp):
_./src/components/sidebar.css_
@@ -77,9 +77,9 @@ _./webpack.config.js_
```diff
{
test: /\.css$/,
-+ include: /node_modules/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
- },
++ include: /node_modules/,
+ use: [MiniCssExtractPlugin.loader, "css-loader"]
+ },
+ // Use CSS modules for custom stylesheets
+ {
+ test: /\.css$/,
@@ -89,9 +89,10 @@ _./webpack.config.js_
+ {
+ loader: 'css-loader',
+ options: {
-+ modules: true,
-+ localIdentName: '[name]__[local]___[hash:base64:5]',
-+ camelCase: true,
++ modules: {
++ localIdentName: "[name]__[local]___[hash:base64:5]",
++ },
++ localsConvention: "camelCase",
+ },
+ },
+ ]
@@ -100,10 +101,10 @@ _./webpack.config.js_
```
-- We are going to create now a sidebar component, _src/sidebar.tsx_. Right now we will create just
+- We are going to create now a sidebar component, _src/components/sidebar.tsx_. Right now we will create just
a rectangle and we will interact with the animation.
-We need to install node typings, since we are goin to make use of _require_ to import from
+We need to install node typings, since we are going to make use of _require_ to import from
the _css_.
```bash
@@ -161,7 +162,7 @@ _./src/app.tsx_
```diff
return (
<>
-+
++
```
@@ -212,7 +213,7 @@ export const SidebarComponent = (props: Props) =>
```
-- Let's make a quick test we will show always the side bar:
+- Let's make a quick test, we will show always the side bar:
_./src/app.tsx_
@@ -287,7 +288,7 @@ npm start
> good pratice (on each render the function will be recreated), let's refactor this in two
> steps:
-- First we will extract this logic to a function, we will call it _toggleSidebarVisbility_.
+- First we will extract this logic to a function, we will call it _toggleSidebarVisibility_.
- Then let's wrap visibility + toggleSidebarVisibility in a custom hook.
* So far so good, but what happens if we want to make this sidebar a reusable component? We could just show the frame but the content should be dynamic.
@@ -343,4 +344,3 @@ We are an innovating team of Javascript experts, passionate about turning your i
[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services.
For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend
-
diff --git a/hooks/09_Sidebar/Readme_es.md b/hooks/09_Sidebar/Readme_es.md
new file mode 100644
index 00000000..109add7b
--- /dev/null
+++ b/hooks/09_Sidebar/Readme_es.md
@@ -0,0 +1,342 @@
+# 09 Sidebar
+
+En este ejemplo vamos a añadir una barra lateral a nuestra aplicación, empezaremos con una implementación específica, y luego la haremos genérica.
+
+# Pasos a seguir
+
+- Tomaremos como punto de partida el ejemplo _08 ColorPickerRefactor_, copiamos el contenido de ese archivo y ejecutamos _npm install_.
+
+```bash
+npm install
+```
+
+- Cree un archivo llamado _src/components/sidebar.css_ y añada los siguientes estilos (http://www.w3schools.com/howto/howto_js_sidenav.asp):
+
+_./src/components/sidebar.css_
+
+```css
+/* The side navigation menu */
+.sidenav {
+ height: 100%; /* 100% Full-height */
+ width: 0; /* 0 width - change this with JavaScript */
+ position: fixed; /* Stay in place */
+ z-index: 1; /* Stay on top */
+ top: 0;
+ left: 0;
+ background-color: #808080; /* Gray*/
+ overflow-x: hidden; /* Disable horizontal scroll */
+ padding-top: 60px; /* Place content 60px from the top */
+ transition: 0.5s; /* 0.5 second transition effect to slide in the sidenav */
+}
+
+/* Position and style the close button (top right corner) */
+.sidenav .closebtn {
+ position: absolute;
+ top: 0;
+ right: 25px;
+ font-size: 36px;
+ margin-left: 50px;
+}
+
+/* Style page content - use this if you want to push the page content to the right when you open the side navigation */
+#main {
+ transition: margin-left 0.5s;
+ padding: 20px;
+}
+
+/* On smaller screens, where height is less than 450px, change the style of the sidenav (less padding and a smaller font size) */
+@media screen and (max-height: 450px) {
+ .sidenav {
+ padding-top: 15px;
+ }
+ .sidenav a {
+ font-size: 18px;
+ }
+}
+```
+
+- Vamos a usar módulos CSS, así que vamos a configurarlo.
+
+_./webpack.config.js_
+
+```diff
+ module.exports = {
+ context: path.join(basePath, "src"),
+ resolve: {
+- extensions: ['.js', '.ts', '.tsx']
++ extensions: ['.js', '.ts', '.tsx', '.css']
+ },
+```
+
+- Sólo usaremos módulos CSS para hojas de estilo personalizadas. No usaremos Módulos CSS para otros archivos CSS, como Bootstrap (carpeta node_modules).
+
+_./webpack.config.js_
+
+```diff
+ {
+ test: /\.css$/,
++ include: /node_modules/,
+ use: [MiniCssExtractPlugin.loader, "css-loader"]
+ },
++ // Use CSS modules for custom stylesheets
++ {
++ test: /\.css$/,
++ exclude: /node_modules/,
++ use: [
++ MiniCssExtractPlugin.loader,
++ {
++ loader: 'css-loader',
++ options: {
++ modules: true,
++ localIdentName: '[name]__[local]___[hash:base64:5]',
++ camelCase: true,
++ },
++ },
++ ]
++ },
++ // Do not use CSS modules in node_modules folder
+
+```
+
+- Vamos a crear el componente de la barra lateral, _src/components/sidebar.tsx_. Crearemos sólo
+ un rectángulo e interactuaremos con la animación.
+
+Necesitamos instalar los tipos para _node_, ya que usaremos _require_ a la hora de importar desde el _css_.
+
+```bash
+npm install @types/node --save-dev
+```
+
+_./src/components/sidebar.tsx_
+
+```jsx
+import * as React from "react";
+
+const classNames = require("./sidebar.css");
+
+export const SidebarComponent = () => (
+
+ Basic side bar, first steps
+
+);
+```
+
+- Añadimos este componente a nuestro barrel _index_
+
+_./src/components/index.ts_
+
+```diff
+export * from "./hello";
+export * from "./nameEdit";
+export * from "./colorBrowser";
+export * from "./colorPicker";
++ export * from "./sidebar";
+```
+
+- Vamos a añadir un _id_ al elemento _body_ de la página _src/index.html_
+
+_./src/index.html_
+
+```diff
+-
++
+```
+
+- Colocamos el componente añadiéndolo en `app.tsx`:
+
+_./src/app.tsx_
+
+```diff
+import * as React from "react";
+- import { HelloComponent, NameEditComponent, ColorBrowser, ColorPicker } from "./components";
++ import { HelloComponent, NameEditComponent, ColorBrowser, ColorPicker, SidebarComponent } from "./components";
+import { Color } from "./model/color";
+```
+
+_./src/app.tsx_
+
+```diff
+ return (
+ <>
++
+
+```
+
+- Comencemos con la parte interesante de esta implementación, agreguemos una opción para mostrar/ocultar la barra lateral _sidebar.tsx_.
+
+_./src/components/sidebar.tsx_
+
+```diff
+import * as React from 'react';
+
+const classNames = require('./sidebar.css');
+
++ interface Props {
++ isVisible: boolean;
++ }
+
+- export const SidebarComponent = () =>
++ export const SidebarComponent = (props: Props) =>
+
+ Basic sidebar, first steps
+
+```
+
+- Ahora vamos a añadir algo de lógica para mostrar / ocultar la barra lateral en caso de que se actualice dicha opción.
+
+
+_./src/sidebar.tsx_
+
+```diff
+import * as React from 'react';
+
+const classNames = require('./sidebar.css');
+
+interface Props {
+ isVisible: boolean;
+};
+
++ const divStyle = (props: Props): React.CSSProperties => ({
++ width: (props.isVisible) ? '23rem' : '0rem'
++ });
+
+export const SidebarComponent = (props: Props) =>
+-
++
+ Basic sidebar, first steps
+
+```
+
+- Hagamos una prueba rápida para mostrar siempre la barra lateral:
+
+_./src/app.tsx_
+
+```diff
+ return (
+ <>
+-
++
+
+```
+
+- Si arrancamos el proyecto veremos la barra lateral que hemos creado (un rectángulo gris).
+
+
+```bash
+npm start
+```
+_¿Qué pasa si no puedo ver la barra lateral?_ Compruebe que _webpack.config.js_ y sus estilos se han aplicado, es posible que tenga que iniciar de nuevo _webpack-dev-sever_ (relanzar _npm start_), compruebe con dev tools que está cargando los estilos CSS.
+
+- Ahora a nivel de aplicación podemos recordar la opción de visibilidad, y añadir un botón para alternar la visualización de la barra lateral.
+
+
+_./src/app.tsx_
+
+```diff
+export const App = () => {
+ const [name, setName] = React.useState("defaultUserName");
+ const [editingName, setEditingName] = React.useState("defaultUserName");
+ const [color, setColor] = React.useState({
+ red: 20,
+ green: 40,
+ blue: 180
+ });
++ const[isVisible, setVisible] = React.useState(false);
+```
+
+_./src/app.tsx_
+
+```diff
+ return (
+ <>
+-
++
+
+
+
+
++
++
++
+ >
+```
+
+- Iniciemos la aplicación para comprobar cómo se comporta:
+
+```bash
+npm start
+```
+
+> Ejercicio: la llamada en línea a la función dentro de _onClick_ no se
+> considera una buena práctica (en cada render se recreará la función),
+> vamos a refactorizarla en dos pasos:
+
+- Primero extraeremos esta lógica a una función, la llamaremos _toggleSidebarVisibility_.
+
+- Ahora envolvemos _visibility_ y _toggleSidebarVisibility_ en un _hook_ personalizado.
+
+* Hasta ahora todo va bien, pero ¿qué pasa si queremos que esta barra lateral sea un componente reutilizable? Podríamos simplemente mostrar el marco pero el contenido debe ser dinámico.
+
+* Comencemos por añadir algo de contenido al instanciar la barra lateral (_app.tsx_).
+
+
+_./src/app.tsx_
+
+```diff
+ <>
+-
++
++
-);
diff --git a/hooks/12_ReactRouter/src/pages/pageB.tsx b/hooks/12_ReactRouter/src/pages/pageB.tsx
index f21b6c70..39a86dd1 100644
--- a/hooks/12_ReactRouter/src/pages/pageB.tsx
+++ b/hooks/12_ReactRouter/src/pages/pageB.tsx
@@ -1,10 +1,9 @@
import * as React from "react";
import { Link } from "react-router-dom";
-export const PageB = () => (
+export const PageB = () =>
Hello from page B
Navigate to Page A
-);
diff --git a/hooks/12_ReactRouter/webpack.config.js b/hooks/12_ReactRouter/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/12_ReactRouter/webpack.config.js
+++ b/hooks/12_ReactRouter/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/hooks/13_LoginForm/Readme.md b/hooks/13_LoginForm/Readme.md
index 7ba9c386..5cef8683 100644
--- a/hooks/13_LoginForm/Readme.md
+++ b/hooks/13_LoginForm/Readme.md
@@ -2,9 +2,9 @@
In this sample we are going to implement a basic login page, that will redirect the user to another page whenever the login has completed successfully.
-We will attempt to create a realistic layout, in order to keep simplicity we will break it into subcomponents and perform some refactor in order to make the solution more maintenable.
+We will attempt to create a realistic layout, in order to keep simplicity we will break it into subcomponents and perform some refactor in order to make the solution more maintainable.
-We will take a startup point sample 12 ReactRouter:
+We will take a starting point the sample 12 ReactRouter:
## Steps
@@ -18,21 +18,21 @@ npm install
- Let's update as well the name of the component.
-_./src/pages/loginPage.tsx_
+_./src/pages/login.component.tsx_
-```javascript
+```diff
import * as React from "react";
import { Link } from "react-router-dom";
-export const LoginPage = () => {
- return (
+- export const PageA = () => (
++ export const LoginComponent: React.FC = (props) => {
-
Hello from login Page
+-
Hello from page A
++
Hello from login Page
Navigate to Page B
);
-};
```
- Let's update _app.tsx_ (routes, names and add a redirect from root to login page).
@@ -40,27 +40,29 @@ export const LoginPage = () => {
_./src/app.tsx_
```diff
-import * as React from 'react';
-import * as ReactDOM from 'react-dom';
-import { HashRouter, Switch, Route } from 'react-router-dom';
+import * as React from "react";
+import { HashRouter, Switch, Route } from "react-router-dom";
- import { PageA } from "./pages/pageA";
-+ import { LoginPage } from "./pages/loginPage";
++ import { LoginComponent } from "./pages/login.component";
import { PageB } from "./pages/pageB";
-ReactDOM.render(
-
-
--
-+
-
-
-
- ,
- document.getElementById('root')
-);
+export const App = () => {
+
+ return (
+ <>
+
+
+-
++
+
+
+
+ >
+ );
+};
```
-- Let's update as well the navigation from _pageB_ to _loginPage_, _pageB.tsx_.
+- Let's update as well the navigation from _pageB_ to _loginPage_.
_./src/pages/b/pageB.tsx_
@@ -80,7 +82,7 @@ export const PageB = () => {
}
```
-- Let's make a quick test and check that everyting is still working fine.
+- Let's make a quick test and check that everything is still working fine.
```
npm start
@@ -106,41 +108,43 @@ _./src/index.html_
```
-- Let's build a proper _login_ layout, _loginPage.tsx_. To build a nice layout, we will install _@material-ui/core_
+- Let's build a proper _login_ layout, _loginPage.tsx_. To build a nice layout, we will install _@material-ui/core_ and _@material-ui/icons_
```bash
-npm install @material-ui/core @material-ui/icons --save-dev
+npm install @material-ui/core @material-ui/icons --save
```
+- However, we must be careful with the compatibility of certain versions of _typescript_ with the new _hooks_ of _material-ui_. For this example, we can install _typescript@3.3.3_ version. You can get more information about this issue in the following link https://github.com/mui-org/material-ui/issues/14018
+
- Now we could create a login form it could look something like:
-_./src/pages/loginPage.tsx_
+_./src/pages/login.container.tsx_
```javascript
import * as React from "react";
-import { Link } from "react-router-dom";
import { withRouter, RouteComponentProps } from "react-router-dom";
-import { withStyles, createStyles, WithStyles } from "@material-ui/core/styles";
+import makeStyles from "@material-ui/styles/makeStyles";
+import createStyles from "@material-ui/styles/createStyles";
import Card from "@material-ui/core/Card";
import CardHeader from "@material-ui/core/CardHeader";
import CardContent from "@material-ui/core/CardContent";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
-import { FormHelperText } from "@material-ui/core";
-// https://material-ui.com/guides/typescript/
-const styles = theme =>
+// https://material-ui.com/styles/api/#makestyles-styles-options-hook
+const useStyles = makeStyles(theme =>
createStyles({
card: {
maxWidth: 400,
margin: "0 auto"
}
- });
+ })
+);
-interface Props extends RouteComponentProps, WithStyles {}
+interface Props extends RouteComponentProps {}
-const LoginPageInner = (props: Props) => {
- const { classes } = props;
+export const LoginContainer: React.FC = (props) => {
+ const classes = useStyles();
return (
@@ -163,10 +167,6 @@ const LoginPageInner = (props: Props) => {
);
};
-
-export const LoginPage = withStyles(styles)(
- withRouter < Props > LoginPageInner
-);
```
- This can be ok, but if we take a deeper look to this component, we could break down into two, one is the card itself the other the form dialog, so it should finally look like:
@@ -182,12 +182,12 @@ export const LoginPage = withStyles(styles)(
```
-- Let's create the loginformcomponent (append it to the loginPage file):
+- Let's create the LoginForm component (append it to the loginPage file):
-_./src/pages/loginPage.tsx_
+_./src/pages/login.component.tsx_
```javascript
-const LoginForm = props => {
+export const LoginComponent: React.FC = (props) => {
return (
{
justifyContent: "center"
}}
>
-
-
-
@@ -206,16 +216,16 @@ const LoginForm = props => {
};
```
-- And let's update the _loginPage.tsx_
+- And let's update the _login.container.tsx_
-_./src/pages/loginPage.tsx_
+_./src/pages/login.container.tsx_
```diff
return (
-+
++
-
- createStyles({
- card: {
- maxWidth: 400,
- margin: '0 auto',
- },
-});
+// https://material-ui.com/styles/api/#makestyles-styles-options-hook
+const useStyles = makeStyles(theme =>
+ createStyles({
+ card: {
+ maxWidth: 400,
+ margin: "0 auto"
+ }
+ })
+);
-interface Props extends RouteComponentProps, WithStyles {
-}
+interface Props extends RouteComponentProps {}
-const LoginPageInner = (props) => {
+export const LoginContainer: React.FC = (props) => {
const { classes } = props;
+ const onLogin = () => {
@@ -279,47 +290,38 @@ const LoginPageInner = (props) => {
)
}
-- export const LoginPage = withStyles(styles)(LoginPageInner);
-+ export const LoginPage = withStyles(styles)(withRouter((LoginPageInner)));
```
- Let's add the navigation on button clicked (later on we will check for user and pwd) _form.tsx_.
In order to do this we have used react-router 4 "withRouter" HoC (High order component), and pass it
down to the LoginForm component.
-_./src/pages/loginPage.tsx_
+_./src/pages/login.component.tsx_
```diff
+interface PropsForm {
+ onLogin : () => void;
+}
-
-+export const LoginForm = (props : PropsForm) => {
-- export const LoginForm = () => {
+-const LoginForm = props => {
++const LoginForm = (props : PropsForm) => {
+ const { onLogin } = props;
-
- return (
-
-
-
- );
-- }
-+})
+ return (
+
+
+
+-
++
+ Login
+
+
+ );
+ };
```
- Let's give a quick try.
@@ -330,9 +332,9 @@ npm start
Ok, we can navigate whenever we click on the login page.
-- Let's keep on progressing, now is time to collect the username and password info, and check if password is valid or not.
+- Let's keep on progressing, now it is time to collect the username and password info, and check if password is valid or not.
-- Let's define an entity for the loginInfo let's create the following path and file
+- Let's define an entity for the loginInfo. Let's create the following path and file
_src/model/login.ts_
@@ -348,7 +350,7 @@ export const createEmptyLogin = (): LoginEntity => ({
});
```
-- Let's add login validation fake restApi: create a folder _src/api_. and a file called _login.ts_
+- Let's add login validation fake restApi: create a folder _src/api_ and a file called _login.ts_
_./src/api/login.ts_
@@ -356,62 +358,59 @@ _./src/api/login.ts_
import { LoginEntity } from "../model/login";
// Just a fake loginAPI
-export const isValidLogin = (loginInfo: LoginEntity): boolean =>
- loginInfo.login === "admin" && loginInfo.password === "test";
+export const isValidLogin = (loginInfo: LoginEntity): Promise =>
+ new Promise((resolve) => {
+ setTimeout(() => {
+ // mock call
+ resolve(loginInfo.login === "admin" && loginInfo.password === "test");
+ }, 500);
+ });
```
- Let's add the _api_ integration, plus navigation on login succeeded:
- First let's create a login state and add the api integration.
-_./src/pages/loginPage.tsx_
+_./src/pages/login.container.tsx_
```diff
+ import { LoginEntity, createEmptyLogin } from '../model/login';
+ import { isValidLogin } from '../api/login';
```
-_./src/pages/loginPage.tsx_
+_./src/pages/login.container.tsx_
```diff
const LoginPageInner = (props: Props) => {
-+ const [loginInfo, setLoginInfo] = React.useState(createEmptyLogin());
++ const [loginInfo, setLoginInfo] = React.useState(
++ createEmptyLogin()
++ );
const { classes } = props;
- const onLogin = () => {
-+ if(isValidLogin(loginInfo)) {
- props.history.push("/pageB");
-+ }
+ const loginSucceeded = (isValid: boolean) => {
+ if (isValid) {
+ history.push("/pageB");
+ } else {
+ setShowAlert(true);
+ }
+ };
+
+ const handleLogin = (login: LoginEntity) => {
+ isValidLogin(login).then(loginSucceeded);
};
```
- Now let's read the data from the textfields components (user and password).
-_./src/pages/loginPage.tsx_
+_./src/pages/login.container.tsx_
```diff
- const onLogin = () => {
- if (isValidLogin(loginInfo)) {
- props.history.push("/pageB");
- }
- };
-
-+ const onUpdateLoginField = (name, value) => {
-+ setLoginInfo({
-+ ...loginInfo,
-+ [name]: value,
-+ })
-+ }
-
return (
-
++
);
@@ -419,23 +418,25 @@ _./src/pages/loginPage.tsx_
- And update _LoginForm_ props and textField onChange.
-_./src/pages/loginPage.tsx_
+_./src/pages/login.component.tsx_
```diff
interface PropsForm {
- onLogin: () => void;
-+ onUpdateField: (string, any) => void;
-+ loginInfo : LoginEntity;
++ onLogin: (login: LoginEntity) => void;
}
-const LoginForm = (props: PropsForm) => {
-- const { onLogin } = props;
-+ const { onLogin, onUpdateField, loginInfo } = props;
-
-+ // TODO: Enhacement move this outside the stateless component discuss why is a good idea
+export const LoginComponent: React.FC = (props) => {
++ const [loginInfo, setLoginInfo] = React.useState(
++ const { onLogin } = props;
++ createEmptyLogin()
++ );
++ const classes = useFormStyles();
+ const onTexFieldChange = (fieldId) => (e) => {
-+ onUpdateField(fieldId, e.target.value);
-+ }
++ setLoginInfo({
++ ...loginInfo,
++ [fieldId]: e.target.value,
++ });
++ };
return (
- );
-};
diff --git a/hooks/15_Context/src/pages/loginPage.validation.ts b/hooks/15_Context/src/pages/loginPage.validation.ts
deleted file mode 100644
index ae379b5d..00000000
--- a/hooks/15_Context/src/pages/loginPage.validation.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import {
- createFormValidation,
- ValidationConstraints,
- Validators
-} from "lc-form-validation";
-
-const loginFormValidationConstraints: ValidationConstraints = {
- fields: {
- login: [{ validator: Validators.required }],
- password: [{ validator: Validators.required }]
- }
-};
-
-export const loginFormValidation = createFormValidation(
- loginFormValidationConstraints
-);
diff --git a/hooks/15_Context/src/pages/loginPage.viewmodel.ts b/hooks/15_Context/src/pages/loginPage.viewmodel.ts
deleted file mode 100644
index 4b3cf5b4..00000000
--- a/hooks/15_Context/src/pages/loginPage.viewmodel.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { FieldValidationResult } from "lc-form-validation";
-
-export interface LoginFormErrors {
- login: FieldValidationResult;
- password: FieldValidationResult;
-}
-
-export const createDefaultLoginFormErrors = (): LoginFormErrors => ({
- login: new FieldValidationResult(),
- password: new FieldValidationResult()
-});
diff --git a/hooks/15_Context/src/pages/pageB.tsx b/hooks/15_Context/src/pages/pageB.tsx
index af5c08a3..ccf75980 100644
--- a/hooks/15_Context/src/pages/pageB.tsx
+++ b/hooks/15_Context/src/pages/pageB.tsx
@@ -2,7 +2,7 @@ import * as React from "react";
import { Link } from "react-router-dom";
import { SessionContext } from "../common";
-export const PageB = () => {
+export const PageB: React.FC = () => {
const loginContext = React.useContext(SessionContext);
return (
diff --git a/hooks/15_Context/webpack.config.js b/hooks/15_Context/webpack.config.js
index 31ab3dba..32eff849 100644
--- a/hooks/15_Context/webpack.config.js
+++ b/hooks/15_Context/webpack.config.js
@@ -1,19 +1,19 @@
-var HtmlWebpackPlugin = require("html-webpack-plugin");
-var MiniCssExtractPlugin = require("mini-css-extract-plugin");
-var webpack = require("webpack");
-var path = require("path");
+const HtmlWebpackPlugin = require("html-webpack-plugin");
+const MiniCssExtractPlugin = require("mini-css-extract-plugin");
+const webpack = require("webpack");
+const path = require("path");
-var basePath = __dirname;
+const basePath = __dirname;
module.exports = {
context: path.join(basePath, "src"),
resolve: {
- extensions: [".js", ".ts", ".tsx"]
+ extensions: [".js", ".ts", ".tsx"],
},
entry: ["@babel/polyfill", "./index.tsx"],
output: {
path: path.join(basePath, "dist"),
- filename: "bundle.js"
+ filename: "bundle.js",
},
devtool: "source-map",
devServer: {
@@ -21,7 +21,7 @@ module.exports = {
inline: true, // Enable watch and live reload
host: "localhost",
port: 8080,
- stats: "errors-only"
+ stats: "errors-only",
},
module: {
rules: [
@@ -31,32 +31,33 @@ module.exports = {
loader: "awesome-typescript-loader",
options: {
useBabel: true,
- babelCore: "@babel/core" // needed for Babel v7
- }
+ babelCore: "@babel/core", // needed for Babel v7
+ },
},
{
test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, "css-loader"]
+ use: [MiniCssExtractPlugin.loader, "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
loader: "file-loader",
options: {
- name: "assets/img/[name].[ext]?[hash]"
- }
- }
- ]
+ name: "assets/img/[name].[ext]?[hash]",
+ esModule: false,
+ },
+ },
+ ],
},
plugins: [
//Generate index.html in /dist => https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: "index.html", //Name of file in ./dist/
template: "index.html", //Name of template in ./src
- hash: true
+ hash: true,
}),
new MiniCssExtractPlugin({
filename: "[name].css",
- chunkFilename: "[id].css"
- })
- ]
+ chunkFilename: "[id].css",
+ }),
+ ],
};
diff --git a/old_class_components_samples/00 BoilerPlate/package.json b/old_class_components_samples/00 BoilerPlate/package.json
index 321c90bd..cdee4b10 100644
--- a/old_class_components_samples/00 BoilerPlate/package.json
+++ b/old_class_components_samples/00 BoilerPlate/package.json
@@ -22,7 +22,7 @@
"typescript": "^2.8.3",
"url-loader": "^1.0.1",
"webpack": "^4.8.1",
- "webpack-cli": "^2.1.3",
+ "webpack-cli": "3.2.3",
"webpack-dev-server": "^3.1.4"
},
"dependencies": {
diff --git a/old_class_components_samples/00 BoilerPlate/readme.md b/old_class_components_samples/00 BoilerPlate/readme.md
index 7356ffb1..e0b08c4a 100644
--- a/old_class_components_samples/00 BoilerPlate/readme.md
+++ b/old_class_components_samples/00 BoilerPlate/readme.md
@@ -24,7 +24,7 @@ Summary steps:
# Prerequisites
-Install [Node.js and npm](https://nodejs.org/en/) (v8.9.4) if they are not already installed on your computer.
+Install at least [Node.js and npm](https://nodejs.org/en/) (v8.9.4) if they are not already installed on your computer.
> Verify that you are running at least node v8.x.x and npm 5.x.x by running `node -v` and `npm -v` in a terminal/console window. Older versions may produce errors.
@@ -143,29 +143,32 @@ _[./package.json](./package.json)_
{
"name": "reactboilerplate",
"version": "1.0.0",
- "description": "Sample working with React,TypeScript and Webpack",
+ "description": "sample working with React, Typescript and Webpack",
+ "main": "index.js",
"scripts": {
- "start": "webpack-dev-server",
- "build": "webpack"
- },
- "author": "Lemoncode",
- "license": "MIT",
- "dependencies": {
- "bootstrap": "^3.3.7",
- "jquery": "^3.2.1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "webpack-dev-server --mode development --inline --hot --open",
+ "build": "webpack --mode development"
},
+ "author": "",
+ "license": "ISC",
"devDependencies": {
- "awesome-typescript-loader": "^3.1.3",
- "babel-core": "^6.25.0",
- "babel-preset-env": "^1.5.2",
- "css-loader": "^0.28.4",
- "file-loader": "^0.11.2",
- "html-webpack-plugin": "^2.28.0",
- "style-loader": "^0.18.2",
- "typescript": "^2.3.4",
- "url-loader": "^0.5.9",
- "webpack": "^2.6.1",
- "webpack-dev-server": "^2.4.5"
+ "awesome-typescript-loader": "^5.0.0",
+ "babel-core": "^6.26.3",
+ "babel-preset-env": "^1.7.0",
+ "css-loader": "^0.28.11",
+ "file-loader": "^1.1.11",
+ "html-webpack-plugin": "^3.2.0",
+ "mini-css-extract-plugin": "^0.4.0",
+ "style-loader": "^0.21.0",
+ "typescript": "^2.8.3",
+ "url-loader": "^1.0.1",
+ "webpack": "^4.8.1",
+ "webpack-cli": "^2.1.3",
+ "webpack-dev-server": "^3.1.4"
+ },
+ "dependencies": {
+ "bootstrap": "^4.1.1"
}
}
@@ -198,8 +201,7 @@ _[./src/index.html](./src/index.html)_
```
-- Now it's time to create a basic **[./webpack.config.js](./webpack.config.js)** file, this configuration will
- include plumbing for:
+- Now it's time to create a basic **[./webpack.config.js](./webpack.config.js)** file, this configuration will include plumbing for:
- Launching a web dev server.
- Transpiling from TypeScript to JavaScript.
- Setup Twitter Bootstrap (including fonts, etc...).
@@ -278,6 +280,7 @@ module.exports = {
```
npm start
```
+ ## Note: If you have problems when running the app you should update webpack-cli to : "webpack-cli": "3.2.3"
# About Lemoncode
diff --git a/old_class_components_samples/02 Components/readme.md b/old_class_components_samples/02 Components/readme.md
index 3299f113..83619aa2 100644
--- a/old_class_components_samples/02 Components/readme.md
+++ b/old_class_components_samples/02 Components/readme.md
@@ -13,7 +13,7 @@ Summary steps:
## Prerequisites
-Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0) if they are not already
+Install [Node.js and npm](https://nodejs.org/en/) (v6.6.0or higher) if they are not already
installed on your computer.
> Verify that you are running at least node v6.x.x and npm 3.x.x by running `node -v` and `npm -v`
diff --git a/old_class_components_samples/02 Components/src/components/about.tsx b/old_class_components_samples/02 Components/src/components/about.tsx
index 1e333c7c..69ac59d4 100644
--- a/old_class_components_samples/02 Components/src/components/about.tsx
+++ b/old_class_components_samples/02 Components/src/components/about.tsx
@@ -11,7 +11,7 @@ export const About: React.StatelessComponent<{}> = () => {
This sample takes the sample "01 Hello react" as starting point.
-
+
We are adding react components: a main component that consumes a header and an about component.
@@ -20,7 +20,7 @@ export const About: React.StatelessComponent<{}> = () => {
@@ -121,6 +123,7 @@ export const Header: React.StatelessComponent<{}> = () => {
- Update components `index.ts` file:
### ./src/components/index.ts
+
```diff
export * from './header';
export * from './about';
@@ -133,11 +136,12 @@ export * from './about';
- Now, we are going to create the `AppRouter` component where we define routes:
### ./src/router.tsx
+
```javascript
- import * as React from 'react';
- import { Route, HashRouter, Switch } from 'react-router-dom';
- import { App } from './app';
- import { About, MembersPage } from './components';
+ import * as React from "react";
+ import { Route, HashRouter, Switch } from "react-router-dom";
+ import { App } from "./app";
+ import { About, MembersPage } from "./components";
export const AppRouter: React.StatelessComponent<{}> = () => {
return (
@@ -152,7 +156,7 @@ export * from './about';
);
- }
+ };
```
- The type of router that we are using is HashRouter because we are creating a static website.
@@ -163,6 +167,7 @@ export * from './about';
- Update `App`. We will remove the div enclosing `Header` because we have already added it in `AppRouter`:
### ./src/app.tsx
+
```diff
import * as React from 'react';
- import { Header, About } from './components';
@@ -183,6 +188,7 @@ import * as React from 'react';
- And finally, update main file:
### ./src/index.tsx
+
```diff
import * as React from 'react';
import * as ReactDOM from 'react-dom';
@@ -198,11 +204,11 @@ ReactDOM.render(
- Execute the example:
- ```bash
- $ npm start
- ```
+```bash
+$ npm start
+```
# About Lemoncode
We are a team of long-term experienced freelance developers, established as a group in 2010.
-We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us.
+We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us.
diff --git a/old_class_components_samples/03 Navigation/src/components/header.tsx b/old_class_components_samples/03 Navigation/src/components/header.tsx
index 3aeb58d5..bf64b826 100644
--- a/old_class_components_samples/03 Navigation/src/components/header.tsx
+++ b/old_class_components_samples/03 Navigation/src/components/header.tsx
@@ -1,29 +1,49 @@
-import * as React from 'react';
-import { Link } from 'react-router-dom';
+import * as React from "react";
+import { Link } from "react-router-dom";
export const Header: React.StatelessComponent<{}> = () => {
return (
+
);
-}
\ No newline at end of file
+};
diff --git a/old_class_components_samples/19 LoginForm/.babelrc b/old_class_components_samples/19 LoginForm/.babelrc
new file mode 100644
index 00000000..03dfd132
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/.babelrc
@@ -0,0 +1,10 @@
+{
+ "presets": [
+ [
+ "env",
+ {
+ "modules": false
+ }
+ ]
+ ]
+}
diff --git a/old_class_components_samples/19 LoginForm/package.json b/old_class_components_samples/19 LoginForm/package.json
new file mode 100644
index 00000000..e99aa28b
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/package.json
@@ -0,0 +1,42 @@
+{
+ "name": "react-typescript-samples",
+ "version": "1.0.0",
+ "description": "Sample working with React,TypeScript and Webpack)",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "start": "webpack-dev-server --mode development --inline --hot --open",
+ "build": "webpack --mode development"
+ },
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@types/react": "^16.4.7",
+ "@types/react-dom": "^16.0.6",
+ "@types/react-router-dom": "^4.3.0",
+ "@types/toastr": "^2.1.35",
+ "awesome-typescript-loader": "^5.2.0",
+ "babel-core": "^6.26.3",
+ "babel-preset-env": "^1.7.0",
+ "css-loader": "^1.0.0",
+ "file-loader": "^1.1.11",
+ "html-webpack-plugin": "^3.2.0",
+ "mini-css-extract-plugin": "^0.4.1",
+ "style-loader": "^0.21.0",
+ "url-loader": "^1.0.1",
+ "webpack": "^4.16.2",
+ "webpack-cli": "^3.1.0",
+ "webpack-dev-server": "^3.1.5"
+ },
+ "dependencies": {
+ "@material-ui/core": "^3.9.0",
+ "@material-ui/icons": "^3.0.2",
+ "bootstrap": "^4.1.2",
+ "lc-form-validation": "^1.0.0",
+ "react": "16.7.0-alpha.0",
+ "react-dom": "16.7.0-alpha.0",
+ "react-router-dom": "^4.2.2",
+ "toastr": "^2.1.4",
+ "typescript": "^3.0.1"
+ }
+}
diff --git a/old_class_components_samples/19 LoginForm/readme.md b/old_class_components_samples/19 LoginForm/readme.md
new file mode 100644
index 00000000..26f7f41c
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/readme.md
@@ -0,0 +1,703 @@
+# 19 LoginForm
+
+In this sample we are going to add a login page using Material-UI, a widely used set of UI components for React.
+
+We will take as startup point sample _18 Hooks_.
+
+## Prerequisites
+
+Install [Node.js and npm](https://nodejs.org/en/) if they are not already installed on your computer.
+
+> Verify that you are running at least node v6.x.x and npm 3.x.x by running `node -v` and `npm -v`
+in a terminal/console window. Older versions may produce errors.
+
+## Steps to build it
+
+- Copy the content of `18 Hooks` folder to an empty folder for the sample.
+
+- To ensure that we use exactly React version 16.7.0-alpha-0, let's set it like that in package.json:
+```diff
+- "react": "^16.7.0-alpha.0",
+- "react-dom": "^16.7.0-alpha.0",
++ "react": "16.7.0-alpha.0",
++ "react-dom": "16.7.0-alpha.0",
+```
+
+- Install dependencies:
+```
+npm install
+```
+
+- Then, we need to install Material-UI:
+```
+npm install @material-ui/core @material-ui/icons --save
+```
+
+- We are going to add a new component to show a login form. So, firstly, let's create a new folder under _./src/components_, named _login_. Secondly, we will add there a new file, _loginForm.tsx_, which will contain a form with two text fields and a button from Material-UI.
+
+- In order to perform the login in the parent component when the button is clicked, we add a callback as a property in _Props_ interface.
+
+- Also, we are going to apply styles using _withStyles_ function from Material-UI. Those styles will be defined in a new file, _loginForm.styles.ts_.
+
+_./src/components/login/loginForm.tsx_
+```
+import * as React from 'react';
+import TextField from '@material-ui/core/TextField';
+import Button from '@material-ui/core/Button';
+import { withStyles, WithStyles } from '@material-ui/core/styles';
+import styles from './loginForm.styles';
+
+interface Props extends WithStyles {
+ onLogin: () => void;
+}
+
+const LoginFormInner: React.StatelessComponent = (props: Props) => {
+
+ return (
+
+
+
+
+ Login
+
+
+ );
+}
+
+export const LoginForm = withStyles(styles)(LoginFormInner);
+```
+_./src/components/login/loginForm.styles.ts_
+```
+import { createStyles, Theme } from "@material-ui/core/styles";
+
+export default (theme: Theme) => createStyles({
+ '@global': {
+ 'body, html, #root': {
+ margin: 0,
+ padding: 0,
+ width: '100%',
+ }
+ },
+ container: {
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ }
+});
+```
+- Now, let's create the page component, named _LoginPage_, that will contain our recently created _LoginForm_. We will include the form in a _Card_ from Material-UI. Also, we need to add a function to perform the action related to the _onLogin_ event. In this case, we are going to show the _About_ page, which was the home page in previous samples.
+
+_./src/components/login/loginPage.tsx_
+
+```
+import * as React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { Card, CardHeader, CardContent } from '@material-ui/core';
+import { LoginForm } from './loginForm';
+
+interface Props extends RouteComponentProps {
+}
+
+export class Login extends React.Component {
+
+ constructor(props: Props) {
+ super(props);
+ }
+
+ private onLogin = () => {
+ this.props.history.push('/about');
+ }
+
+ public render() {
+ return (
+
+
+
+
+
+
+ );
+ }
+}
+```
+
+- Also, we are going to add barrels for the login component:
+
+_./src/components/login/index.ts_
+```
+export { Login } from './loginPage';
+```
+
+_./src/components/index.ts_
+```diff
++ export * from './login';
+```
+
+- Now, let's change the home route in _AppRouter_ component. We have to replace the about page by the login page:
+
+_./src/router.tsx_
+```diff
+- import { About, MembersPage, MemberPageContainer } from './components';
++ import { About, MembersPage, MemberPageContainer, Login } from './components';
+
+export const AppRouter: React.StatelessComponent<{}> = () => {
+ return (
+
+
+
+
+-
++
+
+
+
+
+
+
+
+ );
+}
+```
+
+- The next step is to modify the _App_ component because we are not going to show the _Header_ in the _LoginPage_. Therefore, we need to remove it from the _App_ component and, instead of adding it to _About_ and _Members_ pages, we are going to create layouts. The idea is that we will have one layout for the login page and another layout for _About_ and _Members_ pages, which will include the _Header_.
+
+- Let's create the layout folder and the first layout for the login page, using Material-UI:
+
+_./src/layout/centeredView.styles.ts_
+```
+import { createStyles, Theme } from "@material-ui/core/styles";
+
+export default (theme: Theme) => createStyles({
+ '@global': {
+ 'body, html, #root': {
+ margin: 0,
+ padding: 0,
+ width: '100%',
+ }
+ },
+ container: {
+ maxWidth: '400px',
+ margin: '0 auto',
+ }
+});
+```
+_./src/layout/centeredView.component.tsx_
+```
+import * as React from 'react';
+import { withStyles, WithStyles } from '@material-ui/core/styles';
+import styles from './centeredView.styles';
+
+interface Props extends WithStyles {
+}
+
+const CenteredViewInner: React.StatelessComponent = (props) => (
+
+ {props.children}
+
+);
+
+export const CenteredView = withStyles(styles)(CenteredViewInner);
+```
+
+- Now, let's do the same for _Members_ and _About_ pages. In this case, we are going to create a layout that includes the _Header_.
+
+_./src/layout/appView.styles.ts_
+```
+import { createStyles, Theme } from "@material-ui/core/styles";
+
+export default (theme: Theme) => createStyles({
+ '@global': {
+ 'body, html, #root': {
+ margin: 0,
+ padding: 0,
+ width: '100%',
+ }
+ },
+ container: {
+ }
+});
+```
+_./src/layout/appView.component.tsx_
+```
+import * as React from 'react';
+import { withStyles, WithStyles } from '@material-ui/core/styles';
+import styles from './appView.styles';
+import { Header } from '../components';
+
+interface Props extends WithStyles {
+}
+
+const AppViewInner: React.StatelessComponent = (props) => (
+
+
+ {props.children}
+
+);
+
+export const AppView = withStyles(styles)(AppViewInner);
+```
+
+- Let's add barrel:
+
+_./src/layout/index.ts_
+```
+export { CenteredView } from './centeredView.component';
+export { AppView } from './appView.component';
+```
+
+- Now, we can modify the _App_ component to remove the _Header_.
+
+_./src/app.tsx_
+
+```diff
+import * as React from 'react';
+- import { Header } from './components';
+
+export const App: React.StatelessComponent<{}> = (props) => {
+ return (
+-
++
+-
+ {props.children}
+
+ );
+}
+```
+
+- And let's use our layouts in the corresponding pages:
+
+_/.src/components/members/page.tsx_
+
+```diff
+//...
++ import { AppView } from '../../layout';
+
+//...
+ return (
++
+
+- This sample takes as starting point sample "08 ParamNavigation".
++ This sample takes as starting point sample "18 Hooks".
+
+
+
+- We are replacing class components by stateless components using Hooks.
++ We are adding a login form using Material-UI.
+
+
+
+
+
+
Highlights
+
+
+ The most interesting parts worth to take a look
+
+
+
+
+
+
+
Components:
+
+
+
+- components/members/page.tsx: Stateless component using Hooks.
++ components/login: New login page.
+
+
+
+
+- components/member/pageContainer.tsx: Stateless component using Hooks.
++ common/components/notification: New common component to show a notification.
+
+
+
+
+
+
+
++
+ );
+}
+```
+
+_./src/components/login/loginPage.tsx_
+
+```diff
+import * as React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { Card, CardHeader, CardContent } from '@material-ui/core';
+import { LoginForm } from './loginForm';
++ import { CenteredView } from '../../layout';
+
+interface Props extends RouteComponentProps {
+}
+
+export class Login extends React.Component {
+
+ constructor(props: Props) {
+ super(props);
+ }
+
+ private onLogin = () => {
+ this.props.history.push('/about');
+ }
+
+ public render() {
+ return (
++
+
+
+
+
+
+
++
+ );
+ }
+}
+```
+
+- At this point, we have the structure and design of the login page working. Now, it is time to add the logic to perfom the login event, so that it only shows the _About_ page if the credentials are correct.
+
+- Firstly, we are going to create a new _LoginEntity_ in our model.
+
+_./src/model/login.ts_
+```
+export interface LoginEntity {
+ login: string;
+ password: string;
+}
+
+export const createEmptyLogin = (): LoginEntity => ({
+ login: '',
+ password: '',
+});
+```
+
+_./src/model/index.ts_
+```diff
++ export * from './login';
+```
+
+- Secondly, we will modify _LoginForm_ to add a property with the currently typed credentials, named _loginInfo_, and a function to update the credentials when the user types into the login or password text fields. We are going to use them in _value_ and _onChange_ properties of the _TextField_ component.
+
+_./src/components/login/loginForm.tsx_
+```diff
+import * as React from 'react';
+import TextField from '@material-ui/core/TextField';
+import Button from '@material-ui/core/Button';
+import { withStyles, WithStyles } from '@material-ui/core/styles';
+import styles from './loginForm.styles';
++ import { LoginEntity } from '../../model';
+
+interface Props extends WithStyles {
+ onLogin: () => void;
++ onUpdateLoginField: (name: string, value: any) => void;
++ loginInfo: LoginEntity;
+}
+
+const LoginFormInner: React.StatelessComponent = (props: Props) => {
+
++ const onTextFieldChange = (fieldId) => (e) => {
++ props.onUpdateLoginField(fieldId, e.target.value);
++ }
+
+ return (
+
+
+
+
+ Login
+
+
+ );
+}
+
+export const LoginForm = withStyles(styles)(LoginFormInner);
+```
+
+- Thirdly, let's review _LoginPage_ component. We need to add _loginInfo_ to the state and update it accordingly. Also, it is necessary to check whether the credentials are correct or not. To do that, we will add a function named _isValidLogin_ to our fake API.
+
+_./src/components/login/loginPage.tsx_
+
+```diff
+import * as React from 'react';
+import { RouteComponentProps } from 'react-router-dom';
+import { Card, CardHeader, CardContent } from '@material-ui/core';
+import { LoginForm } from './loginForm';
+import { CenteredView } from '../../layout';
++ import { isValidLogin } from '../../api/login';
++ import { LoginEntity, createEmptyLogin } from '../../model';
+
+interface Props extends RouteComponentProps {
+}
+
++ interface State {
++ loginInfo: LoginEntity;
++ }
+
+- export class Login extends React.Component {
++ export class Login extends React.Component {
+
+ constructor(props: Props) {
+ super(props);
+
++ this.state = {
++ loginInfo: createEmptyLogin(),
++ };
+ }
+
+ private onLogin = () => {
++ if (isValidLogin(this.state.loginInfo)) {
+ this.props.history.push('/about');
++ }
+ }
+
++ private onUpdateLoginField = (name, value) => {
++ this.setState({
++ loginInfo: {
++ ...this.state.loginInfo,
++ [name]: value,
++ },
++ });
++ }
+
+ public render() {
+ return (
+
+
+
+
+
+
+
+
+ );
+ }
+}
+```
+
+- The new function to check if a login is valid in our API:
+
+_./src/api/login/login.ts_
+```
+import { LoginEntity } from "../../model";
+
+export const isValidLogin = (loginInfo: LoginEntity): boolean =>
+ (loginInfo.login === 'admin' && loginInfo.password === 'test');
+```
+
+- And its corresponding barrel:
+
+_./src/api/login/index.ts_
+```
+export { isValidLogin } from './login';
+```
+
+- Finally, let's add a notification to show the user when the credentials are incorrect. To do that, we are going to create a common notification component using Material-UI:
+
+_./src/common/components/notification/notification.tsx_
+```
+import * as React from 'react';
+import Button from '@material-ui/core/Button';
+import Snackbar from '@material-ui/core/Snackbar';
+import IconButton from '@material-ui/core/IconButton';
+import CloseIcon from '@material-ui/icons/Close';
+import { withStyles } from "@material-ui/core";
+
+interface Props {
+ classes?: any;
+ message: string;
+ show: boolean;
+ onClose: () => void;
+}
+
+const styles = theme => ({
+ close: {
+ padding: theme.spacing.unit / 2,
+ },
+});
+
+const NotificationComponentInner: React.StatelessComponent = (props: Props) => {
+ return (
+ {props.message}}
+ action={[
+
+
+ ,
+ ]}
+ />
+ );
+}
+
+export const NotificationComponent = withStyles(styles)(NotificationComponentInner);
+```
+
+- Let's add the barrel:
+
+_./src/common/components/notification/index.ts_
+```
+export { NotificationComponent } from './notification';
+```
+
+- Now, let's use our recently created _NotificationComponent_ in _LoginPage_.
+
+_./src/components/login/loginPage.tsx_
+```diff
+//...
++ import { NotificationComponent } from '../../common/components/notification';
+
+interface Props extends RouteComponentProps {
+}
+
+interface State {
+ loginInfo: LoginEntity;
++ showLoginFailedMsg: boolean;
+}
+
+export class Login extends React.Component {
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ loginInfo: createEmptyLogin(),
++ showLoginFailedMsg: false,
+ };
+ }
+
+ private onLogin = () => {
+ if (isValidLogin(this.state.loginInfo)) {
+ this.props.history.push('/about');
++ } else {
++ this.setState({
++ ...this.state,
++ showLoginFailedMsg: true,
++ });
+ }
+ }
+
+ private onUpdateLoginField = (name, value) => {
+ this.setState({
+ loginInfo: {
+ ...this.state.loginInfo,
+ [name]: value,
+ },
+ });
+ }
+
+ public render() {
+ return (
+
++ this.setState({ showLoginFailedMsg: false })}
++ />
+
+
+
+
+
+
+
+ );
+ }
+}
+```
+
+- It is time to see our sample running. Run the command `npm start` and open the browser at http://localhost:8080
+
+- More info about Material-UI:
+
+ https://material-ui.com/
+
+# About Lemoncode
+
+We are a team of long-term experienced freelance developers, established as a group in 2010.
+We specialize in Front End technologies and .NET. [Click here](http://lemoncode.net/services/en/#en-home) to get more info about us.
diff --git a/old_class_components_samples/19 LoginForm/src/api/login/index.ts b/old_class_components_samples/19 LoginForm/src/api/login/index.ts
new file mode 100644
index 00000000..8de5a4cf
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/src/api/login/index.ts
@@ -0,0 +1 @@
+export { isValidLogin } from './login';
diff --git a/old_class_components_samples/19 LoginForm/src/api/login/login.ts b/old_class_components_samples/19 LoginForm/src/api/login/login.ts
new file mode 100644
index 00000000..eccd4da3
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/src/api/login/login.ts
@@ -0,0 +1,4 @@
+import { LoginEntity } from "../../model";
+
+export const isValidLogin = (loginInfo: LoginEntity): boolean =>
+ (loginInfo.login === 'admin' && loginInfo.password === 'test');
diff --git a/old_class_components_samples/19 LoginForm/src/api/member/index.ts b/old_class_components_samples/19 LoginForm/src/api/member/index.ts
new file mode 100644
index 00000000..90b1edd5
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/src/api/member/index.ts
@@ -0,0 +1,77 @@
+import { MemberEntity } from '../../model';
+import { members } from './mockData';
+
+const baseURL = 'https://api.github.com/orgs/lemoncode';
+let mockMembers = members;
+
+const fetchMembers = (): Promise => {
+ return Promise.resolve(mockMembers);
+};
+
+const fetchMembersAsync = (): Promise => {
+ const membersURL = `${baseURL}/members`;
+
+ return fetch(membersURL)
+ .then((response) => (response.json()))
+ .then(mapToMembers);
+};
+
+const mapToMembers = (githubMembers: any[]): MemberEntity[] => {
+ return githubMembers.map(mapToMember);
+};
+
+const mapToMember = (githubMember): MemberEntity => {
+ return {
+ id: githubMember.id,
+ login: githubMember.login,
+ avatar_url: githubMember.avatar_url,
+ };
+};
+
+const saveMember = (member: MemberEntity): Promise => {
+ const index = mockMembers.findIndex(m => m.id === member.id);
+
+ index >= 0 ?
+ updateMember(member, index) :
+ insertMember(member);
+
+ return Promise.resolve(true);
+};
+
+const updateMember = (member: MemberEntity, index: number) => {
+ mockMembers = [
+ ...mockMembers.slice(0, index),
+ member,
+ ...mockMembers.slice(index + 1),
+ ];
+};
+
+const insertMember = (member: MemberEntity) => {
+ member.id = mockMembers.length;
+
+ mockMembers = [
+ ...mockMembers,
+ member,
+ ];
+};
+
+const fetchMemberById = (id: number): Promise => {
+ const index: number = mockMembers.findIndex(m => m.id === id);
+ const member: MemberEntity = index >= 0 ?
+ mockMembers[index]
+ :
+ {
+ id: -1,
+ login: '',
+ avatar_url: '',
+ };
+
+ return Promise.resolve(member);
+}
+
+export const memberAPI = {
+ fetchMembers,
+ fetchMembersAsync,
+ saveMember,
+ fetchMemberById,
+};
diff --git a/old_class_components_samples/19 LoginForm/src/api/member/mockData.ts b/old_class_components_samples/19 LoginForm/src/api/member/mockData.ts
new file mode 100644
index 00000000..8c7a6e20
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/src/api/member/mockData.ts
@@ -0,0 +1,15 @@
+import { MemberEntity } from '../../model';
+
+export const members: MemberEntity[] =
+ [
+ {
+ id: 1457912,
+ login: "brauliodiez",
+ avatar_url: "https://avatars.githubusercontent.com/u/1457912?v=3"
+ },
+ {
+ id: 4374977,
+ login: "Nasdan",
+ avatar_url: "https://avatars.githubusercontent.com/u/4374977?v=3"
+ }
+ ];
diff --git a/old_class_components_samples/19 LoginForm/src/app.tsx b/old_class_components_samples/19 LoginForm/src/app.tsx
new file mode 100644
index 00000000..ef957b42
--- /dev/null
+++ b/old_class_components_samples/19 LoginForm/src/app.tsx
@@ -0,0 +1,9 @@
+import * as React from 'react';
+
+export const App: React.StatelessComponent<{}> = (props) => {
+ return (
+