From c64366aca1641fe7df12b7c6c583882ae8995a3b Mon Sep 17 00:00:00 2001 From: Braulio Diez Botella Date: Mon, 18 Feb 2019 09:28:13 +0100 Subject: [PATCH 1/3] added basefactor lemon info bottom of readmes --- hooks/01_HelloReact/Readme.md | 11 ++++++++--- hooks/02_Properties/Readme.md | 11 +++++++++++ hooks/03_State/Readme.md | 11 +++++++++++ hooks/04_Callback/Readme.md | 11 +++++++++++ hooks/05_Refactor/Readme.md | 11 +++++++++++ hooks/06_Enable/Readme.md | 11 +++++++++++ hooks/07_ColorPicker/Readme.md | 9 +++++++++ hooks/08_ColorPickerRefactor/Readme.md | 11 +++++++++++ hooks/09_Sidebar/Readme.md | 11 +++++++++++ hooks/10_TableMock/Readme.md | 10 ++++++++++ hooks/11_TableAxios/Readme.md | 10 ++++++++++ 11 files changed, 114 insertions(+), 3 deletions(-) diff --git a/hooks/01_HelloReact/Readme.md b/hooks/01_HelloReact/Readme.md index b3c141e..e1e3f82 100644 --- a/hooks/01_HelloReact/Readme.md +++ b/hooks/01_HelloReact/Readme.md @@ -124,7 +124,12 @@ module.exports = { npm start ``` -# About Lemoncode +# About Basefactor + 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 are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/02_Properties/Readme.md b/hooks/02_Properties/Readme.md index aacd180..aee5f3f 100644 --- a/hooks/02_Properties/Readme.md +++ b/hooks/02_Properties/Readme.md @@ -66,3 +66,14 @@ _./src/index.tsx_ ```cmd npm start ``` + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/03_State/Readme.md b/hooks/03_State/Readme.md index 87188b5..f6cb139 100644 --- a/hooks/03_State/Readme.md +++ b/hooks/03_State/Readme.md @@ -161,3 +161,14 @@ Side note: mind the use of the fat arrow function. This avoids losing the contex ``` npm start ``` + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/04_Callback/Readme.md b/hooks/04_Callback/Readme.md index ff0b894..32246bd 100644 --- a/hooks/04_Callback/Readme.md +++ b/hooks/04_Callback/Readme.md @@ -110,3 +110,14 @@ Now we've got a clear event, strongly typed and simplified (as it is more straig Now, the greetings message only changes when the user clicks the change button. > What happens if we simulate an AJAX call? Let's place in the app's componentWillMount a timeout and set the name value in the timeout's callback. + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/Readme.md b/hooks/05_Refactor/Readme.md index 9b09cb9..2cb2c57 100644 --- a/hooks/05_Refactor/Readme.md +++ b/hooks/05_Refactor/Readme.md @@ -162,3 +162,14 @@ export const App = () => { ); }; ``` + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/06_Enable/Readme.md b/hooks/06_Enable/Readme.md index 51537aa..01163ed 100644 --- a/hooks/06_Enable/Readme.md +++ b/hooks/06_Enable/Readme.md @@ -110,3 +110,14 @@ strength component: - Create a password strenght indicator (you can do it just showing plain text in future samples we will teach you how to interact with CSS and you will be able to display a color bar indicating password strength). + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/07_ColorPicker/Readme.md b/hooks/07_ColorPicker/Readme.md index 04960c0..18584b9 100644 --- a/hooks/07_ColorPicker/Readme.md +++ b/hooks/07_ColorPicker/Readme.md @@ -262,3 +262,12 @@ 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. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/08_ColorPickerRefactor/Readme.md b/hooks/08_ColorPickerRefactor/Readme.md index 4f3f2a6..6d404b2 100644 --- a/hooks/08_ColorPickerRefactor/Readme.md +++ b/hooks/08_ColorPickerRefactor/Readme.md @@ -247,3 +247,14 @@ export const ColorPicker = (props: Props) => ( > Excercise: evaluate what this code does, is this code worth? what issues could you find > in the future? What would happend if we add new fields to the color entity? + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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.md b/hooks/09_Sidebar/Readme.md index 2bac673..3a5ec64 100644 --- a/hooks/09_Sidebar/Readme.md +++ b/hooks/09_Sidebar/Readme.md @@ -333,3 +333,14 @@ _./src/components/sidebar.tsx_ ``` npm start ``` + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/10_TableMock/Readme.md b/hooks/10_TableMock/Readme.md index 76b9064..b88a63c 100644 --- a/hooks/10_TableMock/Readme.md +++ b/hooks/10_TableMock/Readme.md @@ -304,3 +304,13 @@ _./src/components/memberTable.tsx_ > Excercise: we could go further with the refactoring, what about creating a > _TableHeaderComponent_ component and a _tableBodyComponent_ ?. + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/11_TableAxios/Readme.md b/hooks/11_TableAxios/Readme.md index 416f069..0c4496f 100644 --- a/hooks/11_TableAxios/Readme.md +++ b/hooks/11_TableAxios/Readme.md @@ -74,3 +74,13 @@ export const getMembersCollection = (): Promise => { ```bash npm start ``` + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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 From 2429afee31260f3f463f509114b433ef925cd865 Mon Sep 17 00:00:00 2001 From: Braulio Diez Botella Date: Mon, 18 Feb 2019 22:55:47 +0100 Subject: [PATCH 2/3] sample 12 router completed --- hooks/12_ReactRouter/.babelrc | 10 ++ hooks/12_ReactRouter/Readme.md | 132 +++++++++++++++++++++++ hooks/12_ReactRouter/package.json | 43 ++++++++ hooks/12_ReactRouter/src/app.tsx | 23 ++++ hooks/12_ReactRouter/src/index.html | 13 +++ hooks/12_ReactRouter/src/index.tsx | 6 ++ hooks/12_ReactRouter/src/pages/pageA.tsx | 10 ++ hooks/12_ReactRouter/src/pages/pageB.tsx | 10 ++ hooks/12_ReactRouter/tsconfig.json | 15 +++ hooks/12_ReactRouter/webpack.config.js | 62 +++++++++++ 10 files changed, 324 insertions(+) create mode 100644 hooks/12_ReactRouter/.babelrc create mode 100644 hooks/12_ReactRouter/Readme.md create mode 100644 hooks/12_ReactRouter/package.json create mode 100644 hooks/12_ReactRouter/src/app.tsx create mode 100644 hooks/12_ReactRouter/src/index.html create mode 100644 hooks/12_ReactRouter/src/index.tsx create mode 100644 hooks/12_ReactRouter/src/pages/pageA.tsx create mode 100644 hooks/12_ReactRouter/src/pages/pageB.tsx create mode 100644 hooks/12_ReactRouter/tsconfig.json create mode 100644 hooks/12_ReactRouter/webpack.config.js diff --git a/hooks/12_ReactRouter/.babelrc b/hooks/12_ReactRouter/.babelrc new file mode 100644 index 0000000..957cae3 --- /dev/null +++ b/hooks/12_ReactRouter/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/hooks/12_ReactRouter/Readme.md b/hooks/12_ReactRouter/Readme.md new file mode 100644 index 0000000..3cd8a12 --- /dev/null +++ b/hooks/12_ReactRouter/Readme.md @@ -0,0 +1,132 @@ +# 12 React Router + +n this sample we will start using React-Router (SPA navigation). + +We take as a starting point the example _03 State_: + +## Steps + +- Copy the content from _03 State_ and execute `npm install`. + +```bash +npm install +``` + +- Let's make some cleanup (remove _src/hello.tsx_ and _src/nameEdit.tsx_ files). + +- Let's create a component called _PageA_ as _src/pageA.tsx_: + +_./src/pages/pageA.tsx_ + +```jsx +import * as React from "react" + +export const PageA = () => +
+

Hello from page A

+
+``` + +- Let's create a component called _PageB_ as _src/pageB.tsx_: + +_./src/pages/pageB.tsx_ + +```jsx +import * as React from "react" + +export const PageB = () => +
+

Hello from page B

+
+``` + +- Let's install the dependencies [`react-router-dom`](https://github.com/ReactTraining/react-router) and typescript definitions for this. + +```bash +npm install react-router-dom --save +npm install @types/react-router-dom --save-dev +``` + +- Let's define the routing in _app.tsx_: + +_./src/app.tsx_ + +```diff +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +- import { App } from './app'; +- import { HelloComponent } from './hello'; ++ import { HashRouter, Switch, Route } from 'react-router-dom'; ++ import { PageA } from './pages/pageA'; ++ import { PageB } from './pages/pageB'; + +ReactDOM.render( +- +- ++ ++ ++ ++ ++ ++ , +document.getElementById('root') +); + +``` + +- It's time to check that we are following the right track: + +```bash +npm start +``` + +- Let's define a navigation from _[PageA.tsx](./src/pageA.tsx)_ to _[PageB.tsx](./src/pageB.tsx)_. + +_./src/pages/pageA.tsx_ + +```diff +import * as React from "react" ++ import { Link } from 'react-router-dom'; + +export const PageA = () => +
+

Hello from page A

++
++ Navigate to Page B +
+``` + +- Let's define a navigation from _[PageB.tsx](./src/pageB.tsx)_ to _[PageA.tsx](./src/pageA.tsx)_ + +_./src/pages/pageB.tsx_ + +```diff +import * as React from "react" ++ import { Link } from 'react-router-dom'; + +export const PageB = () => +
+

Hello from page B

++
++ Navigate to Page A +
+``` + + +- Let's run the app and check that the navigation links are working + +```bash +npm start +``` + + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/12_ReactRouter/package.json b/hooks/12_ReactRouter/package.json new file mode 100644 index 0000000..2ff4e9a --- /dev/null +++ b/hooks/12_ReactRouter/package.json @@ -0,0 +1,43 @@ +{ + "name": "react-typescript-by-sample", + "version": "1.0.0", + "description": "React Typescript examples", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --inline --hot --open", + "build": "webpack --mode development" + }, + "keywords": [ + "react", + "typescript", + "hooks" + ], + "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", + "@types/react-router-dom": "^4.3.1", + "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" + }, + "dependencies": { + "react": "^16.8.2", + "react-dom": "^16.8.2", + "react-router-dom": "^4.3.1" + } +} diff --git a/hooks/12_ReactRouter/src/app.tsx b/hooks/12_ReactRouter/src/app.tsx new file mode 100644 index 0000000..c5b284d --- /dev/null +++ b/hooks/12_ReactRouter/src/app.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { HashRouter, Switch, Route } from "react-router-dom"; +import { PageA } from "./pages/pageA"; +import { PageB } from "./pages/pageB"; + +export const App = () => { + const [name, setName] = React.useState("defaultUserName"); + + const setUsernameState = (event: React.ChangeEvent) => { + setName(event.target.value); + }; + + return ( + <> + + + + + + + + ); +}; diff --git a/hooks/12_ReactRouter/src/index.html b/hooks/12_ReactRouter/src/index.html new file mode 100644 index 0000000..cef0845 --- /dev/null +++ b/hooks/12_ReactRouter/src/index.html @@ -0,0 +1,13 @@ + + + + + + + +
+

Sample app

+
+
+ + diff --git a/hooks/12_ReactRouter/src/index.tsx b/hooks/12_ReactRouter/src/index.tsx new file mode 100644 index 0000000..26ed977 --- /dev/null +++ b/hooks/12_ReactRouter/src/index.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +import { App } from "./app"; + +ReactDOM.render(, document.getElementById("root")); diff --git a/hooks/12_ReactRouter/src/pages/pageA.tsx b/hooks/12_ReactRouter/src/pages/pageA.tsx new file mode 100644 index 0000000..376961d --- /dev/null +++ b/hooks/12_ReactRouter/src/pages/pageA.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; +import { Link } from "react-router-dom"; + +export const PageA = () => ( +
+

Hello from page A

+
+ Navigate to Page B +
+); diff --git a/hooks/12_ReactRouter/src/pages/pageB.tsx b/hooks/12_ReactRouter/src/pages/pageB.tsx new file mode 100644 index 0000000..f21b6c7 --- /dev/null +++ b/hooks/12_ReactRouter/src/pages/pageB.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; +import { Link } from "react-router-dom"; + +export const PageB = () => ( +
+

Hello from page B

+
+ Navigate to Page A +
+); diff --git a/hooks/12_ReactRouter/tsconfig.json b/hooks/12_ReactRouter/tsconfig.json new file mode 100644 index 0000000..f90a3f0 --- /dev/null +++ b/hooks/12_ReactRouter/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "moduleResolution": "node", + "declaration": false, + "noImplicitAny": false, + "jsx": "react", + "sourceMap": true, + "noLib": false, + "suppressImplicitAnyIndexErrors": true + }, + "compileOnSave": false, + "exclude": ["node_modules"] +} diff --git a/hooks/12_ReactRouter/webpack.config.js b/hooks/12_ReactRouter/webpack.config.js new file mode 100644 index 0000000..31ab3db --- /dev/null +++ b/hooks/12_ReactRouter/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); +var webpack = require("webpack"); +var path = require("path"); + +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".ts", ".tsx"] + }, + entry: ["@babel/polyfill", "./index.tsx"], + output: { + path: path.join(basePath, "dist"), + filename: "bundle.js" + }, + devtool: "source-map", + devServer: { + contentBase: "./dist", // Content base + inline: true, // Enable watch and live reload + host: "localhost", + port: 8080, + stats: "errors-only" + }, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: "awesome-typescript-loader", + options: { + useBabel: true, + babelCore: "@babel/core" // needed for Babel v7 + } + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + }, + { + test: /\.(png|jpg|gif|svg)$/, + loader: "file-loader", + options: { + name: "assets/img/[name].[ext]?[hash]" + } + } + ] + }, + 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 + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +}; From e334f0a0e05dc4c0d9704298ea526304d286071c Mon Sep 17 00:00:00 2001 From: Braulio Diez Botella Date: Mon, 18 Feb 2019 23:42:04 +0100 Subject: [PATCH 3/3] Sample 13 completed --- hooks/13_LoginForm/.babelrc | 10 + hooks/13_LoginForm/Readme.md | 600 ++++++++++++++++++ hooks/13_LoginForm/package.json | 45 ++ hooks/13_LoginForm/src/api/login.ts | 5 + hooks/13_LoginForm/src/app.tsx | 17 + hooks/13_LoginForm/src/common/index.ts | 1 + .../13_LoginForm/src/common/notification.tsx | 54 ++ hooks/13_LoginForm/src/index.html | 12 + hooks/13_LoginForm/src/index.tsx | 6 + hooks/13_LoginForm/src/model/login.ts | 9 + hooks/13_LoginForm/src/pages/loginPage.tsx | 111 ++++ hooks/13_LoginForm/src/pages/pageB.tsx | 10 + hooks/13_LoginForm/tsconfig.json | 15 + hooks/13_LoginForm/webpack.config.js | 62 ++ 14 files changed, 957 insertions(+) create mode 100644 hooks/13_LoginForm/.babelrc create mode 100644 hooks/13_LoginForm/Readme.md create mode 100644 hooks/13_LoginForm/package.json create mode 100644 hooks/13_LoginForm/src/api/login.ts create mode 100644 hooks/13_LoginForm/src/app.tsx create mode 100644 hooks/13_LoginForm/src/common/index.ts create mode 100644 hooks/13_LoginForm/src/common/notification.tsx create mode 100644 hooks/13_LoginForm/src/index.html create mode 100644 hooks/13_LoginForm/src/index.tsx create mode 100644 hooks/13_LoginForm/src/model/login.ts create mode 100644 hooks/13_LoginForm/src/pages/loginPage.tsx create mode 100644 hooks/13_LoginForm/src/pages/pageB.tsx create mode 100644 hooks/13_LoginForm/tsconfig.json create mode 100644 hooks/13_LoginForm/webpack.config.js diff --git a/hooks/13_LoginForm/.babelrc b/hooks/13_LoginForm/.babelrc new file mode 100644 index 0000000..957cae3 --- /dev/null +++ b/hooks/13_LoginForm/.babelrc @@ -0,0 +1,10 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "entry" + } + ] + ] +} diff --git a/hooks/13_LoginForm/Readme.md b/hooks/13_LoginForm/Readme.md new file mode 100644 index 0000000..7ba9c38 --- /dev/null +++ b/hooks/13_LoginForm/Readme.md @@ -0,0 +1,600 @@ +# 13 Login Form + +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 take a startup point sample 12 ReactRouter: + +## Steps + +- Copy the content from _12 ReactRouter_ and execute `npm install`. + +```bash +npm install +``` + +- Let's rename _pageA.tsx_ to _loginPage.tsx_. + +- Let's update as well the name of the component. + +_./src/pages/loginPage.tsx_ + +```javascript +import * as React from "react"; +import { Link } from "react-router-dom"; + +export const LoginPage = () => { + return ( +
+

Hello from login Page

+
+ Navigate to Page B +
+ ); +}; +``` + +- Let's update _app.tsx_ (routes, names and add a redirect from root to login page). + +_./src/app.tsx_ + +```diff +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { HashRouter, Switch, Route } from 'react-router-dom'; +- import { PageA } from "./pages/pageA"; ++ import { LoginPage } from "./pages/loginPage"; +import { PageB } from "./pages/pageB"; + +ReactDOM.render( + + +- ++ + + + + , + document.getElementById('root') +); +``` + +- Let's update as well the navigation from _pageB_ to _loginPage_, _pageB.tsx_. + +_./src/pages/b/pageB.tsx_ + +```diff +import * as React from "react" +import { Link } from 'react-router-dom'; + +export const PageB = () => { + return ( +
+

Hello from page B

+
+- Navigate to Page A ++ Navigate to Login +
+ ) +} +``` + +- Let's make a quick test and check that everyting is still working fine. + +``` +npm start +``` + +- Time to remove 'Sample app' text from the main _html_. + +_./src/index.html_ + +```diff + + + + + + + +
+-

Sample app

+
+
+ + +``` + +- Let's build a proper _login_ layout, _loginPage.tsx_. To build a nice layout, we will install _@material-ui/core_ + +```bash +npm install @material-ui/core @material-ui/icons --save-dev +``` + +- Now we could create a login form it could look something like: + +_./src/pages/loginPage.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 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 => + createStyles({ + card: { + maxWidth: 400, + margin: "0 auto" + } + }); + +interface Props extends RouteComponentProps, WithStyles {} + +const LoginPageInner = (props: Props) => { + const { classes } = props; + + return ( + + + +
+ + + +
+
+
+ ); +}; + +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: + +** Proposal ** + +```javascript + + + + + + +``` + +- Let's create the loginformcomponent (append it to the loginPage file): + +_./src/pages/loginPage.tsx_ + +```javascript +const LoginForm = props => { + return ( +
+ + + +
+ ); +}; +``` + +- And let's update the _loginPage.tsx_ + +_./src/pages/loginPage.tsx_ + +```diff + return ( + + + ++ +-
+- +- +- +-
+
+
+ ) +``` + +- Let's give a try and check how is it looking. + +```bash +npm start +``` + +- Le'ts add the navigation on button clicked, we will do it in two steps. + +- First we will expose a method to do that in the loginPage. + +_./src/pages/login/loginPage.tsx_ + +```diff +// ... + +// https://material-ui.com/guides/typescript/ +const styles = theme => createStyles({ + card: { + maxWidth: 400, + margin: '0 auto', + }, +}); + +interface Props extends RouteComponentProps, WithStyles { +} + +const LoginPageInner = (props) => { + const { classes } = props; + ++ const onLogin = () => { ++ props.history.push('/pageB'); ++ } + + return ( + + + +- ++ + + + ) +} + +- 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_ + +```diff ++interface PropsForm { ++ onLogin : () => void; ++} + ++export const LoginForm = (props : PropsForm) => { +- export const LoginForm = () => { ++ const { onLogin } = props; + + return ( +
+
+
+
+ +
+
+ +
+- +- +
+
+
+ ); +- } ++}) +``` + +- Let's give a quick try. + +```bash +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 define an entity for the loginInfo let's create the following path and file + +_src/model/login.ts_ + +```javascript +export interface LoginEntity { + login: string; + password: string; +} + +export const createEmptyLogin = (): LoginEntity => ({ + login: "", + password: "" +}); +``` + +- Let's add login validation fake restApi: create a folder _src/api_. and a file called _login.ts_ + +_./src/api/login.ts_ + +```javascript +import { LoginEntity } from "../model/login"; + +// Just a fake loginAPI +export const isValidLogin = (loginInfo: LoginEntity): boolean => + loginInfo.login === "admin" && loginInfo.password === "test"; +``` + +- 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_ + +```diff ++ import { LoginEntity, createEmptyLogin } from '../model/login'; ++ import { isValidLogin } from '../api/login'; +``` + +_./src/pages/loginPage.tsx_ + +```diff +const LoginPageInner = (props: Props) => { ++ const [loginInfo, setLoginInfo] = React.useState(createEmptyLogin()); + const { classes } = props; + + const onLogin = () => { ++ if(isValidLogin(loginInfo)) { + props.history.push("/pageB"); ++ } + }; + +``` + +- Now let's read the data from the textfields components (user and password). + +_./src/pages/loginPage.tsx_ + +```diff + const onLogin = () => { + if (isValidLogin(loginInfo)) { + props.history.push("/pageB"); + } + }; + ++ const onUpdateLoginField = (name, value) => { ++ setLoginInfo({ ++ ...loginInfo, ++ [name]: value, ++ }) ++ } + + return ( + + + + + + + ); +``` + +- And update _LoginForm_ props and textField onChange. + +_./src/pages/loginPage.tsx_ + +```diff +interface PropsForm { + onLogin: () => void; ++ onUpdateField: (string, any) => void; ++ loginInfo : LoginEntity; +} + +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 ++ const onTexFieldChange = (fieldId) => (e) => { ++ onUpdateField(fieldId, e.target.value); ++ } + + return ( +
+ + + +
+ ); +}; +``` + +- Let's display a notification when the login validation fails. + +- First we will create a simple notification component, base on _react material ui_ _snackbar_ + +_./src/common/notification.tsx_ + +```javascript +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 = (props: Props) => { + const { classes, message, show, onClose } = props; + + return ( + {message}} + action={[ + + + + ]} + /> + ); +}; + +export const NotificationComponent = withStyles(styles)( + NotificationComponentInner +); +``` + +- Let's expose this common component via an _index_ file. + +_./src/common/index.ts_ + +```javascript +export * from "./notification"; +``` + +- Now let's instantiate this in our _loginPage_ + +_./src/pages/loginPage.tsx_ + +```diff ++ import { NotificationComponent } from "../common"; + +// (...) + +const LoginPageInner = (props: Props) => { + const [loginInfo, setLoginInfo] = React.useState( + createEmptyLogin() + ); ++ const [showLoginFailedMsg, setShowLoginFailedMsg] = React.useState(false); + const { classes } = props; + + const onLogin = () => { + if (isValidLogin(loginInfo)) { + props.history.push("/pageB"); +- } ++ } else { ++ setShowLoginFailedMsg(true); ++ } + } + + const onUpdateLoginField = (name, value) => { + setLoginInfo({ + ...loginInfo, + [name]: value + }); + }; + + return ( ++ <> + + + + + + ++ setShowLoginFailedMsg(false)} ++ /> ++ + ); +}; +``` + +- Now we can give a try, enter a wrong combination of user and password, check that the snack is shown, then + enter the right combination (admin / test) and check that the application navigates to PageB. + +# About Basefactor + Lemoncode + +We are an innovating team of Javascript experts, passionate about turning your ideas into robust products. + +[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services. + +[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/13_LoginForm/package.json b/hooks/13_LoginForm/package.json new file mode 100644 index 0000000..e59398f --- /dev/null +++ b/hooks/13_LoginForm/package.json @@ -0,0 +1,45 @@ +{ + "name": "react-typescript-by-sample", + "version": "1.0.0", + "description": "React Typescript examples", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --mode development --inline --hot --open", + "build": "webpack --mode development" + }, + "keywords": [ + "react", + "typescript", + "hooks" + ], + "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", + "@material-ui/core": "^3.9.2", + "@material-ui/icons": "^3.0.2", + "@types/react": "^16.8.3", + "@types/react-dom": "^16.8.1", + "@types/react-router-dom": "^4.3.1", + "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" + }, + "dependencies": { + "react": "^16.8.2", + "react-dom": "^16.8.2", + "react-router-dom": "^4.3.1" + } +} diff --git a/hooks/13_LoginForm/src/api/login.ts b/hooks/13_LoginForm/src/api/login.ts new file mode 100644 index 0000000..1f7d4f3 --- /dev/null +++ b/hooks/13_LoginForm/src/api/login.ts @@ -0,0 +1,5 @@ +import {LoginEntity} from '../model/login'; + +// Just a fake loginAPI +export const isValidLogin = (loginInfo : LoginEntity) : boolean => + (loginInfo.login === 'admin' && loginInfo.password === 'test'); diff --git a/hooks/13_LoginForm/src/app.tsx b/hooks/13_LoginForm/src/app.tsx new file mode 100644 index 0000000..9c1b1a2 --- /dev/null +++ b/hooks/13_LoginForm/src/app.tsx @@ -0,0 +1,17 @@ +import * as React from "react"; +import { HashRouter, Switch, Route } from "react-router-dom"; +import { LoginPage } from "./pages/loginPage"; +import { PageB } from "./pages/pageB"; + +export const App = () => { + return ( + <> + + + + + + + + ); +}; diff --git a/hooks/13_LoginForm/src/common/index.ts b/hooks/13_LoginForm/src/common/index.ts new file mode 100644 index 0000000..d9b217c --- /dev/null +++ b/hooks/13_LoginForm/src/common/index.ts @@ -0,0 +1 @@ +export * from './notification'; diff --git a/hooks/13_LoginForm/src/common/notification.tsx b/hooks/13_LoginForm/src/common/notification.tsx new file mode 100644 index 0000000..bcca827 --- /dev/null +++ b/hooks/13_LoginForm/src/common/notification.tsx @@ -0,0 +1,54 @@ +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 = (props: Props) => { + const { classes, message, show, onClose } = props; + + return ( + {message}} + action={[ + + + + ]} + /> + ); +}; + +export const NotificationComponent = withStyles(styles)( + NotificationComponentInner +); diff --git a/hooks/13_LoginForm/src/index.html b/hooks/13_LoginForm/src/index.html new file mode 100644 index 0000000..2ad4810 --- /dev/null +++ b/hooks/13_LoginForm/src/index.html @@ -0,0 +1,12 @@ + + + + + + + +
+
+
+ + diff --git a/hooks/13_LoginForm/src/index.tsx b/hooks/13_LoginForm/src/index.tsx new file mode 100644 index 0000000..26ed977 --- /dev/null +++ b/hooks/13_LoginForm/src/index.tsx @@ -0,0 +1,6 @@ +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +import { App } from "./app"; + +ReactDOM.render(, document.getElementById("root")); diff --git a/hooks/13_LoginForm/src/model/login.ts b/hooks/13_LoginForm/src/model/login.ts new file mode 100644 index 0000000..32854b7 --- /dev/null +++ b/hooks/13_LoginForm/src/model/login.ts @@ -0,0 +1,9 @@ +export interface LoginEntity { + login: string; + password: string; +} + +export const createEmptyLogin = (): LoginEntity => ({ + login: "", + password: "" +}); diff --git a/hooks/13_LoginForm/src/pages/loginPage.tsx b/hooks/13_LoginForm/src/pages/loginPage.tsx new file mode 100644 index 0000000..0c08d45 --- /dev/null +++ b/hooks/13_LoginForm/src/pages/loginPage.tsx @@ -0,0 +1,111 @@ +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 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"; +import { LoginEntity, createEmptyLogin } from "../model/login"; +import { isValidLogin } from "../api/login"; +import { NotificationComponent } from "../common"; + +// https://material-ui.com/guides/typescript/ +const styles = theme => + createStyles({ + card: { + maxWidth: 400, + margin: "0 auto" + } + }); + +interface Props extends RouteComponentProps, WithStyles {} + +const LoginPageInner = (props: Props) => { + const [loginInfo, setLoginInfo] = React.useState( + createEmptyLogin() + ); + const [showLoginFailedMsg, setShowLoginFailedMsg] = React.useState(false); + const { classes } = props; + + const onLogin = () => { + if (isValidLogin(loginInfo)) { + props.history.push("/pageB"); + } else { + setShowLoginFailedMsg(true); + } + }; + + const onUpdateLoginField = (name, value) => { + setLoginInfo({ + ...loginInfo, + [name]: value + }); + }; + + return ( + <> + + + + + + + setShowLoginFailedMsg(false)} + /> + + ); +}; + +export const LoginPage = withStyles(styles)(withRouter(LoginPageInner)); + +interface PropsForm { + onLogin: () => void; + onUpdateField: (string, any) => void; + loginInfo: LoginEntity; +} + +const LoginForm = (props: PropsForm) => { + const { onLogin, onUpdateField, loginInfo } = props; + + // TODO: Enhacement move this outside the stateless component discuss why is a good idea + const onTexFieldChange = fieldId => e => { + onUpdateField(fieldId, e.target.value); + }; + + return ( +
+ + + +
+ ); +}; diff --git a/hooks/13_LoginForm/src/pages/pageB.tsx b/hooks/13_LoginForm/src/pages/pageB.tsx new file mode 100644 index 0000000..87ec471 --- /dev/null +++ b/hooks/13_LoginForm/src/pages/pageB.tsx @@ -0,0 +1,10 @@ +import * as React from "react"; +import { Link } from "react-router-dom"; + +export const PageB = () => ( +
+

Hello from page B

+
+ Navigate to Login +
+); diff --git a/hooks/13_LoginForm/tsconfig.json b/hooks/13_LoginForm/tsconfig.json new file mode 100644 index 0000000..f90a3f0 --- /dev/null +++ b/hooks/13_LoginForm/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "es6", + "moduleResolution": "node", + "declaration": false, + "noImplicitAny": false, + "jsx": "react", + "sourceMap": true, + "noLib": false, + "suppressImplicitAnyIndexErrors": true + }, + "compileOnSave": false, + "exclude": ["node_modules"] +} diff --git a/hooks/13_LoginForm/webpack.config.js b/hooks/13_LoginForm/webpack.config.js new file mode 100644 index 0000000..31ab3db --- /dev/null +++ b/hooks/13_LoginForm/webpack.config.js @@ -0,0 +1,62 @@ +var HtmlWebpackPlugin = require("html-webpack-plugin"); +var MiniCssExtractPlugin = require("mini-css-extract-plugin"); +var webpack = require("webpack"); +var path = require("path"); + +var basePath = __dirname; + +module.exports = { + context: path.join(basePath, "src"), + resolve: { + extensions: [".js", ".ts", ".tsx"] + }, + entry: ["@babel/polyfill", "./index.tsx"], + output: { + path: path.join(basePath, "dist"), + filename: "bundle.js" + }, + devtool: "source-map", + devServer: { + contentBase: "./dist", // Content base + inline: true, // Enable watch and live reload + host: "localhost", + port: 8080, + stats: "errors-only" + }, + module: { + rules: [ + { + test: /\.(ts|tsx)$/, + exclude: /node_modules/, + loader: "awesome-typescript-loader", + options: { + useBabel: true, + babelCore: "@babel/core" // needed for Babel v7 + } + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader"] + }, + { + test: /\.(png|jpg|gif|svg)$/, + loader: "file-loader", + options: { + name: "assets/img/[name].[ext]?[hash]" + } + } + ] + }, + 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 + }), + new MiniCssExtractPlugin({ + filename: "[name].css", + chunkFilename: "[id].css" + }) + ] +};