Skip to content

Commit 448d237

Browse files
committed
14 form validation wip
1 parent 3e4ddb2 commit 448d237

17 files changed

Lines changed: 566 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: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
# About Basefactor + Lemoncode
132+
133+
We are an innovating team of Javascript experts, passionate about turning your ideas into robust products.
134+
135+
[Basefactor, consultancy by Lemoncode](http://www.basefactor.com) provides consultancy and coaching services.
136+
137+
[Lemoncode](http://lemoncode.net/services/en/#en-home) provides training services.
138+
139+
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';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import * as React from "react";
2+
import Button from "@material-ui/core/Button";
3+
import Snackbar from "@material-ui/core/Snackbar";
4+
import IconButton from "@material-ui/core/IconButton";
5+
import CloseIcon from "@material-ui/icons/Close";
6+
import { withStyles } from "@material-ui/core";
7+
8+
interface Props {
9+
classes?: any;
10+
message: string;
11+
show: boolean;
12+
onClose: () => void;
13+
}
14+
15+
const styles = theme => ({
16+
close: {
17+
padding: theme.spacing.unit / 2
18+
}
19+
});
20+
21+
const NotificationComponentInner = (props: Props) => {
22+
const { classes, message, show, onClose } = props;
23+
24+
return (
25+
<Snackbar
26+
anchorOrigin={{
27+
vertical: "bottom",
28+
horizontal: "left"
29+
}}
30+
open={show}
31+
autoHideDuration={3000}
32+
onClose={onClose}
33+
ContentProps={{
34+
"aria-describedby": "message-id"
35+
}}
36+
message={<span id="message-id">{message}</span>}
37+
action={[
38+
<IconButton
39+
key="close"
40+
aria-label="Close"
41+
color="inherit"
42+
className={classes.close}
43+
onClick={onClose}
44+
>
45+
<CloseIcon />
46+
</IconButton>
47+
]}
48+
/>
49+
);
50+
};
51+
52+
export const NotificationComponent = withStyles(styles)(
53+
NotificationComponentInner
54+
);
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as React from "react";
2+
import TextField from "@material-ui/core/TextField";
3+
import Typography from "@material-ui/core/Typography/Typography";
4+
5+
interface Props {
6+
name: string;
7+
label: string;
8+
onChange: any;
9+
value: string;
10+
error?: string;
11+
type?: string;
12+
}
13+
14+
const defaultProps: Partial<Props> = {
15+
type: "text"
16+
};
17+
18+
const onTextFieldChange = (
19+
fieldId: string,
20+
onChange: (fieldId, value) => void
21+
) => e => {
22+
onChange(fieldId, e.target.value);
23+
};
24+
25+
export const TextFieldForm: React.StatelessComponent<Props> = props => {
26+
const { name, label, onChange, value, error, type } = props;
27+
return (
28+
<>
29+
<TextField
30+
label={label}
31+
margin="normal"
32+
value={value}
33+
type={type}
34+
onChange={onTextFieldChange(name, onChange)}
35+
/>
36+
<Typography variant="caption" color="error" gutterBottom>
37+
{props.error}
38+
</Typography>
39+
</>
40+
);
41+
};
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<title></title>
6+
</head>
7+
<body>
8+
<div class="well">
9+
<div id="root"></div>
10+
</div>
11+
</body>
12+
</html>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as React from "react";
2+
import * as ReactDOM from "react-dom";
3+
4+
import { App } from "./app";
5+
6+
ReactDOM.render(<App />, document.getElementById("root"));

0 commit comments

Comments
 (0)