Skip to content

Commit 51f8b04

Browse files
authored
Merge pull request #162 from Lemoncode/hooks
Hooks
2 parents cc4e2e0 + 7332659 commit 51f8b04

17 files changed

Lines changed: 747 additions & 0 deletions

hooks/14_FormValidation/.babelrc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"presets": [
3+
[
4+
"@babel/preset-env",
5+
{
6+
"useBuiltIns": "entry"
7+
}
8+
]
9+
]
10+
}

hooks/14_FormValidation/Readme.md

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# 13 Login Form
2+
3+
Let's add validation support to this form.
4+
5+
For this we will use lc-form-validation library
6+
7+
Summary steps:
8+
9+
- Install lc-form-validation library.
10+
- Refactor input component to a common component and include error validation info.
11+
- Let's define the validation for the form.
12+
- Let's hook it.
13+
14+
## Steps
15+
16+
- Copy the content from _13 LoginForm_ and execute `npm install`.
17+
18+
```bash
19+
npm install
20+
```
21+
22+
- Let's install the _lc-form-validation-library_.
23+
24+
```bash
25+
npm install lc-form-validation
26+
```
27+
28+
- To avoid having too much repeated code let's move to common an input component, including it's
29+
label plus validation text.
30+
31+
_./common/textFieldForm.tsx_
32+
33+
```tsx
34+
import * as React from "react";
35+
import TextField from "@material-ui/core/TextField";
36+
import Typography from "@material-ui/core/Typography/Typography";
37+
38+
interface Props {
39+
name: string;
40+
label: string;
41+
onChange: any;
42+
value: string;
43+
error?: string;
44+
type?: string;
45+
}
46+
47+
const defaultProps: Partial<Props> = {
48+
type: "text"
49+
};
50+
51+
const onTextFieldChange = (
52+
fieldId: string,
53+
onChange: (fieldId, value) => void
54+
) => e => {
55+
onChange(fieldId, e.target.value);
56+
};
57+
58+
export const TextFieldForm: React.StatelessComponent<Props> = props => {
59+
const { name, label, onChange, value, error, type } = props;
60+
return (
61+
<>
62+
<TextField
63+
label={label}
64+
margin="normal"
65+
value={value}
66+
type={type}
67+
onChange={onTextFieldChange(name, onChange)}
68+
/>
69+
<Typography variant="caption" color="error" gutterBottom>
70+
{props.error}
71+
</Typography>
72+
</>
73+
);
74+
};
75+
```
76+
77+
- Let's add it to the common index file.
78+
79+
_./src/common/index.ts_
80+
81+
```diff
82+
export * from './notification';
83+
+ export * from './textFieldForm';
84+
```
85+
86+
- Now let's define a basic validation for the form, we want to ensure both fields are informed.
87+
88+
_./src/pages/loginPage.validation.ts_
89+
90+
```typescript
91+
import {
92+
createFormValidation,
93+
ValidationConstraints,
94+
Validators
95+
} from "lc-form-validation";
96+
97+
const loginFormValidationConstraints: ValidationConstraints = {
98+
fields: {
99+
login: [{ validator: Validators.required }],
100+
password: [{ validator: Validators.required }]
101+
}
102+
};
103+
104+
export const loginFormValidation = createFormValidation(
105+
loginFormValidationConstraints
106+
);
107+
```
108+
109+
- Let's create now a class to hold the dataFormErrors.
110+
111+
_./src/login/loginPage.viewmodel.ts_
112+
113+
```typescript
114+
import { FieldValidationResult } from "lc-form-validation";
115+
116+
export interface LoginFormErrors {
117+
login: FieldValidationResult;
118+
password: FieldValidationResult;
119+
}
120+
121+
export const createDefaultLoginFormErrors = (): LoginFormErrors => ({
122+
login: new FieldValidationResult(),
123+
password: new FieldValidationResult()
124+
});
125+
```
126+
127+
- Now let's go for the component side.
128+
129+
- First let's add the dataFormErrors to the state of the component.
130+
131+
_./src/pages/loginPage.tsx_
132+
133+
```diff
134+
import { isValidLogin } from "../api/login";
135+
import { NotificationComponent } from "../common";
136+
+ import {LoginFormErrors, createDefaultLoginFormErrors} from './loginPage.viewmodel';
137+
```
138+
139+
_./src/pages/loginPage.tsx_
140+
141+
```diff
142+
const LoginPageInner = (props: Props) => {
143+
const [loginInfo, setLoginInfo] = React.useState<LoginEntity>(
144+
createEmptyLogin()
145+
);
146+
+ const [loginFormErrors, setLoginFormErrors] = React.useState<LoginFormErrors>(createDefaultLoginFormErrors());
147+
const [showLoginFailedMsg, setShowLoginFailedMsg] = React.useState(false);
148+
```
149+
150+
- Let's fire the validation on viemodel update.
151+
152+
_./src/pages/loginPage.tsx_
153+
154+
```diff
155+
+ import { loginFormValidation } from "./loginPage.validation";
156+
```
157+
158+
_./src/pages/loginPage.tsx_
159+
160+
```diff
161+
const onUpdateLoginField = (name, value) => {
162+
setLoginInfo({
163+
...loginInfo,
164+
[name]: value
165+
});
166+
167+
+ loginFormValidation.validateField(loginInfo, name, value)
168+
+ .then((fieldValidationResult) => {
169+
170+
+ setLoginFormErrors({
171+
+ ...loginFormErrors,
172+
+ [name]: fieldValidationResult,
173+
+ });
174+
+ });
175+
}
176+
```
177+
178+
- We need to pass down dataFormErrors
179+
180+
_./src/pages/loginPage.tsx_
181+
182+
```diff
183+
<LoginForm
184+
onLogin={onLogin}
185+
onUpdateField={onUpdateLoginField}
186+
loginInfo={loginInfo}
187+
+ loginFormErrors={this.state.loginFormErrors}
188+
/>
189+
```
190+
191+
_./src/pages/loginPage.tsx_
192+
193+
```diff
194+
interface PropsForm {
195+
onLogin: () => void;
196+
onUpdateField: (string, any) => void;
197+
loginInfo: LoginEntity;
198+
+ loginFormErrors : LoginFormErrors;
199+
}
200+
```
201+
202+
- Let's replace the _TextFieldForm_ entries with the wrapper we have created (includes
203+
displaying validation errors).
204+
205+
_./src/pages/loginPage.tsx_
206+
207+
```diff
208+
+ import { TextFieldForm } from '../common';
209+
```
210+
211+
_./src/pages/loginPage.tsx_
212+
213+
```diff
214+
const LoginForm = (props: PropsForm) => {
215+
- const { onLogin, onUpdateField, loginInfo } = props;
216+
+ const { onLogin, onUpdateField, loginInfo, loginFormErrors } = props;
217+
```
218+
219+
```diff
220+
- <TextField
221+
+ <TextFieldForm
222+
label="Name"
223+
name="login"
224+
- margin="normal"
225+
value={loginInfo.login}
226+
- onChange={onTexFieldChange("login")}
227+
+ onChange={onUpdateField}
228+
+ error={loginFormErrors.login.errorMessage}
229+
/>
230+
- <TextField
231+
+ <TextFieldForm
232+
label="Password"
233+
name="password"
234+
type="password"
235+
- margin="normal"
236+
value={loginInfo.password}
237+
- onChange={onTexFieldChange("password")}
238+
+ onChange={onUpdateField}
239+
+ error={loginFormErrors.password.errorMessage}
240+
/>
241+
```
242+
243+
- let's give a try
244+
245+
```
246+
npm start
247+
```
248+
249+
- And let's add an alert (Excercise and a notification) when the user clicks and the form all the fields are valid.
250+
251+
_./src/pages/loginPage.tsx_
252+
253+
```diff
254+
const onLogin = () => {
255+
+ loginFormValidation.validateForm(loginInfo)
256+
+ .then((formValidationResult) => {
257+
+ if(formValidationResult.succeeded) {
258+
if (isValidLogin(loginInfo)) {
259+
props.history.push("/pageB");
260+
} else {
261+
setShowLoginFailedMsg(true);
262+
}
263+
+ } else {
264+
+ alert('error, review the fields');
265+
+ const updatedLoginFormErrors = {
266+
+ ...loginFormErrors,
267+
+ ...formValidationResult.fieldErrors,
268+
+ }
269+
+ setLoginFormErrors(updatedLoginFormErrors);
270+
+ }
271+
272+
273+
+ });
274+
};
275+
```
276+
277+
> Excercise, refactor this method following single abstraction level principle and single resposibility principle.
278+
279+
# About Basefactor + Lemoncode
280+
281+
We are an innovating team of Javascript experts, passionate about turning your ideas into robust products.
282+
283+
[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services.
284+
285+
[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services.
286+
287+
For the LATAM/Spanish audience we are running an Online Front End Master degree, more info: http://lemoncode.net/master-frontend
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"name": "react-typescript-by-sample",
3+
"version": "1.0.0",
4+
"description": "React Typescript examples",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "webpack-dev-server --mode development --inline --hot --open",
8+
"build": "webpack --mode development"
9+
},
10+
"keywords": [
11+
"react",
12+
"typescript",
13+
"hooks"
14+
],
15+
"author": "Braulio Diez Botella",
16+
"license": "MIT",
17+
"devDependencies": {
18+
"@babel/cli": "^7.2.3",
19+
"@babel/core": "^7.2.2",
20+
"@babel/polyfill": "^7.2.5",
21+
"@babel/preset-env": "^7.3.1",
22+
"@material-ui/core": "^3.9.2",
23+
"@material-ui/icons": "^3.0.2",
24+
"@types/react": "^16.8.3",
25+
"@types/react-dom": "^16.8.1",
26+
"@types/react-router-dom": "^4.3.1",
27+
"awesome-typescript-loader": "^5.2.1",
28+
"babel-loader": "^8.0.5",
29+
"css-loader": "^2.1.0",
30+
"file-loader": "^3.0.1",
31+
"html-webpack-plugin": "^3.2.0",
32+
"mini-css-extract-plugin": "^0.5.0",
33+
"style-loader": "^0.23.1",
34+
"typescript": "^3.3.3",
35+
"url-loader": "^1.1.2",
36+
"webpack": "^4.29.3",
37+
"webpack-cli": "^3.2.3",
38+
"webpack-dev-server": "^3.1.14"
39+
},
40+
"dependencies": {
41+
"lc-form-validation": "^2.0.0",
42+
"react": "^16.8.2",
43+
"react-dom": "^16.8.2",
44+
"react-router-dom": "^4.3.1"
45+
}
46+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import {LoginEntity} from '../model/login';
2+
3+
// Just a fake loginAPI
4+
export const isValidLogin = (loginInfo : LoginEntity) : boolean =>
5+
(loginInfo.login === 'admin' && loginInfo.password === 'test');
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as React from "react";
2+
import { HashRouter, Switch, Route } from "react-router-dom";
3+
import { LoginPage } from "./pages/loginPage";
4+
import { PageB } from "./pages/pageB";
5+
6+
export const App = () => {
7+
return (
8+
<>
9+
<HashRouter>
10+
<Switch>
11+
<Route exact={true} path="/" component={LoginPage} />
12+
<Route path="/pageB" component={PageB} />
13+
</Switch>
14+
</HashRouter>
15+
</>
16+
);
17+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './notification';
2+
export * from './textFieldForm';

0 commit comments

Comments
 (0)