-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(RSS-ECOMM-2_01): implement client side validation for login form (…
…#112) * feat: add InputField component * feat: add InputFieldValidator component * feat: add types for InputField component * feat: add LoginForm component * feat: add the necessary validators to check input fields * feat: add password display functionality * fix: changes from @stardustmeg Co-authored-by: Meg G. <[email protected]> * fix: changes from @stardustmeg Co-authored-by: Meg G. <[email protected]> --------- Co-authored-by: Meg G. <[email protected]>
- Loading branch information
1 parent
12885ea
commit 3a1a243
Showing
13 changed files
with
575 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
import type { InputFieldParams, InputFieldValidatorParams } from '@/shared/types/interfaces.ts'; | ||
|
||
import InputFieldValidatorModel from '@/features/InputFieldValidator/model/InputFieldValidatorModel.ts'; | ||
import { EVENT_NAMES, INPUT_TYPES } from '@/shared/constants/enums.ts'; | ||
|
||
import InputFieldView from '../view/InputFieldView.ts'; | ||
|
||
class InputFieldModel { | ||
private isValid = false; | ||
|
||
private validator: InputFieldValidatorModel | null = null; | ||
|
||
private view: InputFieldView; | ||
|
||
constructor(inputFieldParams: InputFieldParams, validParams: InputFieldValidatorParams | null) { | ||
this.view = new InputFieldView(inputFieldParams); | ||
|
||
if (validParams) { | ||
this.validator = new InputFieldValidatorModel(validParams, this.isValid); | ||
this.setInputHandler(); | ||
} | ||
|
||
this.setShowPasswordHandler(); | ||
} | ||
|
||
private inputHandler(): boolean { | ||
const errorField = this.view.getErrorField(); | ||
const errors = this.validator?.validate(this.view.getValue()); | ||
if (errors === true) { | ||
if (errorField) { | ||
errorField.textContent = ''; | ||
} | ||
this.isValid = true; | ||
} else { | ||
if (errorField && errors) { | ||
const [firstError] = errors; | ||
errorField.textContent = firstError; | ||
} | ||
this.isValid = false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private setInputHandler(): boolean { | ||
const input = this.view.getInput().getHTML(); | ||
input.addEventListener(EVENT_NAMES.INPUT, () => { | ||
this.inputHandler(); | ||
}); | ||
|
||
return true; | ||
} | ||
|
||
private setShowPasswordHandler(): boolean { | ||
const button = this.view.getShowPasswordButton().getHTML(); | ||
button.addEventListener(EVENT_NAMES.CLICK, () => this.showPasswordHandler()); | ||
return true; | ||
} | ||
|
||
private showPasswordHandler(): boolean { | ||
const input = this.view.getInput().getHTML(); | ||
input.type = input.type === INPUT_TYPES.PASSWORD ? INPUT_TYPES.TEXT : INPUT_TYPES.PASSWORD; | ||
this.view.switchPasswordButtonSVG(input.type); | ||
return true; | ||
} | ||
|
||
public getIsValid(): boolean { | ||
return this.isValid; | ||
} | ||
|
||
public getView(): InputFieldView { | ||
return this.view; | ||
} | ||
} | ||
|
||
export default InputFieldModel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import type { InputFieldParams, InputParams, LabelParams } from '@/shared/types/interfaces.ts'; | ||
|
||
import ButtonModel from '@/shared/Button/model/ButtonModel.ts'; | ||
import { INPUT_TYPES, SVG_DETAILS, TAG_NAMES } from '@/shared/constants/enums.ts'; | ||
import createBaseElement from '@/shared/utils/createBaseElement.ts'; | ||
import createSVGUse from '@/shared/utils/createSVGUse.ts'; | ||
|
||
import InputModel from '@/shared/Input/model/InputModel.ts'; | ||
|
||
class InputFieldView { | ||
private errorField: HTMLSpanElement | null = null; | ||
|
||
private input: InputModel; | ||
|
||
private inputField: HTMLLabelElement | InputModel; | ||
|
||
private label: HTMLLabelElement | null = null; | ||
|
||
private showPasswordButton: ButtonModel; | ||
|
||
constructor(params: InputFieldParams) { | ||
this.input = this.createInput(params.inputParams); | ||
this.showPasswordButton = this.createShowPasswordButton(); | ||
this.inputField = this.createHTML(params); | ||
} | ||
|
||
private createErrorField(): HTMLSpanElement { | ||
this.errorField = createBaseElement({ | ||
tag: TAG_NAMES.SPAN, | ||
}); | ||
|
||
return this.errorField; | ||
} | ||
|
||
private createHTML(params: InputFieldParams): HTMLLabelElement | InputModel { | ||
const { labelParams } = params; | ||
if (labelParams) { | ||
this.inputField = this.createLabel(labelParams); | ||
this.errorField = this.createErrorField(); | ||
this.label?.append(this.input.getHTML(), this.errorField); | ||
} else { | ||
this.inputField = this.input; | ||
} | ||
|
||
if (this.getInput().getHTML().type === INPUT_TYPES.PASSWORD) { | ||
this.label?.append(this.showPasswordButton.getHTML()); | ||
} | ||
|
||
return this.inputField; | ||
} | ||
|
||
private createInput(inputParams: InputParams): InputModel { | ||
const { autocomplete, id, placeholder, type } = inputParams; | ||
this.input = new InputModel({ | ||
autocomplete, | ||
id, | ||
placeholder: placeholder || '', | ||
type, | ||
}); | ||
|
||
return this.input; | ||
} | ||
|
||
private createLabel(labelParams: LabelParams): HTMLLabelElement { | ||
const { for: htmlFor, text } = labelParams; | ||
this.label = createBaseElement({ | ||
attributes: { | ||
for: htmlFor, | ||
}, | ||
innerContent: text || '', | ||
tag: TAG_NAMES.LABEL, | ||
}); | ||
|
||
return this.label; | ||
} | ||
|
||
private createShowPasswordButton(): ButtonModel { | ||
this.showPasswordButton = new ButtonModel({}); | ||
this.switchPasswordButtonSVG(INPUT_TYPES.PASSWORD); | ||
return this.showPasswordButton; | ||
} | ||
|
||
public getErrorField(): HTMLSpanElement | null { | ||
return this.errorField; | ||
} | ||
|
||
public getHTML(): HTMLLabelElement | InputModel { | ||
return this.inputField; | ||
} | ||
|
||
public getInput(): InputModel { | ||
return this.input; | ||
} | ||
|
||
public getShowPasswordButton(): ButtonModel { | ||
return this.showPasswordButton; | ||
} | ||
|
||
public getValue(): string { | ||
if (this.inputField instanceof InputModel) { | ||
return this.inputField.getValue(); | ||
} | ||
return this.input.getValue(); | ||
} | ||
|
||
public switchPasswordButtonSVG(type: string): SVGSVGElement { | ||
const svg = document.createElementNS(SVG_DETAILS.SVG_URL, TAG_NAMES.SVG); | ||
this.showPasswordButton.getHTML().innerHTML = ''; | ||
svg.append(createSVGUse(type === INPUT_TYPES.PASSWORD ? SVG_DETAILS.CLOSE_EYE : SVG_DETAILS.OPEN_EYE)); | ||
this.showPasswordButton.getHTML().append(svg); | ||
return svg; | ||
} | ||
} | ||
|
||
export default InputFieldView; |
Empty file.
103 changes: 103 additions & 0 deletions
103
src/features/InputFieldValidator/model/InputFieldValidatorModel.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import type { InputFieldValidatorParams } from '@/shared/types/interfaces'; | ||
|
||
class InputFieldValidatorModel { | ||
private isValid: boolean; | ||
|
||
private validParams; | ||
|
||
constructor(validParams: InputFieldValidatorParams, isValid: boolean) { | ||
this.validParams = validParams; | ||
this.isValid = isValid; | ||
} | ||
|
||
private checkMaxLength(value: string): boolean | string { | ||
if (this.validParams.maxLength && value.length > this.validParams.maxLength) { | ||
const errorMessage = `Max length should not exceed ${this.validParams.maxLength}`; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private checkMinLength(value: string): boolean | string { | ||
if (this.validParams.minLength && value.length < this.validParams.minLength) { | ||
const errorMessage = `Min length should be at least ${this.validParams.minLength}`; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private checkNotSpecialSymbols(value: string): boolean | string { | ||
if (this.validParams.notSpecialSymbols && !this.validParams.notSpecialSymbols.pattern.test(value)) { | ||
const errorMessage = this.validParams.notSpecialSymbols.message; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private checkRequired(value: string): boolean | string { | ||
if (this.validParams.required && value.trim() === '') { | ||
const errorMessage = 'Field is required'; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private checkRequiredSymbols(value: string): boolean | string { | ||
if (this.validParams.requiredSymbols && !this.validParams.requiredSymbols.pattern.test(value)) { | ||
const errorMessage = this.validParams.requiredSymbols.message; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private checkValidMail(value: string): boolean | string { | ||
if (this.validParams.validMail && !this.validParams.validMail.pattern.test(value)) { | ||
const errorMessage = this.validParams.validMail.message; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private checkWhitespace(value: string): boolean | string { | ||
if (this.validParams.notWhitespace && !this.validParams.notWhitespace.pattern.test(value) && value.trim() !== '') { | ||
const errorMessage = this.validParams.notWhitespace.message; | ||
return errorMessage; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
public validate(value: string): boolean | string[] { | ||
const errors = [ | ||
this.checkWhitespace(value), | ||
this.checkRequired(value), | ||
this.checkNotSpecialSymbols(value), | ||
this.checkMinLength(value), | ||
this.checkMaxLength(value), | ||
this.checkRequiredSymbols(value), | ||
this.checkValidMail(value), | ||
]; | ||
|
||
const errorMessages: string[] = []; | ||
errors.forEach((error) => { | ||
if (typeof error === 'string') { | ||
errorMessages.push(error); | ||
} | ||
}); | ||
|
||
if (errorMessages.length) { | ||
return errorMessages; | ||
} | ||
|
||
this.isValid = true; | ||
return this.isValid; | ||
} | ||
} | ||
|
||
export default InputFieldValidatorModel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -113,3 +113,74 @@ export const PAGES_IDS = { | |
export const MEDIATOR_EVENTS = { | ||
CHANGE_PAGE: 'changePage', | ||
} as const; | ||
|
||
export const LOGIN_FORM_EMAIL_FIELD_PARAMS = { | ||
inputParams: { | ||
autocomplete: 'off', | ||
id: 'email', | ||
placeholder: '[email protected]', | ||
type: 'text', | ||
}, | ||
labelParams: { | ||
for: 'email', | ||
text: 'Enter your email', | ||
}, | ||
} as const; | ||
|
||
export const LOGIN_FORM_PASSWORD_FIELD_PARAMS = { | ||
inputParams: { | ||
autocomplete: 'off', | ||
id: 'password', | ||
placeholder: '***********', | ||
type: 'password', | ||
}, | ||
labelParams: { | ||
for: 'password', | ||
text: 'Enter your password', | ||
}, | ||
} as const; | ||
|
||
export const LOGIN_FORM_INPUT_FIELD_PARAMS = [LOGIN_FORM_EMAIL_FIELD_PARAMS, LOGIN_FORM_PASSWORD_FIELD_PARAMS]; | ||
|
||
const LOGIN_FORM_EMAIL_FIELD_VALIDATE_PARAMS = { | ||
key: 'email', | ||
notWhitespace: { | ||
message: 'Email must not contain whitespaces', | ||
pattern: /^\S+$/, | ||
}, | ||
required: true, | ||
validMail: { | ||
message: 'Enter correct email ([email protected])', | ||
pattern: /^([a-z0-9_-]+\.)*[a-z0-9_-]+@[a-z0-9_-]+(\.[a-z0-9_-]+)*\.[a-z]{2,6}$/, | ||
}, | ||
} as const; | ||
|
||
const LOGIN_FORM_PASSWORD_FIELD_VALIDATE_PARAMS = { | ||
key: 'password', | ||
minLength: 8, | ||
notWhitespace: { | ||
message: 'Password must not contain whitespaces', | ||
pattern: /^\S+$/, | ||
}, | ||
required: true, | ||
requiredSymbols: { | ||
message: 'Password must contain English letters, at least 1 letter in upper and lower case and at least 1 number', | ||
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+/, | ||
}, | ||
} as const; | ||
|
||
export const LOGIN_FORM_INPUT_FIELD_VALIDATION_PARAMS = [ | ||
LOGIN_FORM_EMAIL_FIELD_VALIDATE_PARAMS, | ||
LOGIN_FORM_PASSWORD_FIELD_VALIDATE_PARAMS, | ||
]; | ||
|
||
export const FORM_SUBMIT_BUTTON_TEXT = { | ||
LOGIN: 'Login', | ||
REGISTRATION: 'Register', | ||
} as const; | ||
|
||
export const SVG_DETAILS = { | ||
CLOSE_EYE: 'closeEye', | ||
OPEN_EYE: 'openEye', | ||
SVG_URL: 'http://www.w3.org/2000/svg', | ||
} as const; |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Oops, something went wrong.