|
| 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 |
0 commit comments