Skip to content

Commit

Permalink
feat(RSS-ECOMM-2_01): implement client side validation for login form (
Browse files Browse the repository at this point in the history
…#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
Kleostro and stardustmeg authored Apr 30, 2024
1 parent 12885ea commit 3a1a243
Show file tree
Hide file tree
Showing 13 changed files with 575 additions and 0 deletions.
76 changes: 76 additions & 0 deletions src/entities/InputField/model/InputFieldModel.ts
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;
115 changes: 115 additions & 0 deletions src/entities/InputField/view/InputFieldView.ts
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 src/features/InputFieldValidator/model/InputFieldValidatorModel.ts
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;
10 changes: 10 additions & 0 deletions src/pages/LoginPage/model/LoginPageModel.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import type { PageInterface } from '@/shared/types/interfaces.ts';

import LoginFormModel from '@/widgets/LoginForm/model/LoginFormModel.ts';

import LoginPageView from '../view/LoginPageView.ts';

class LoginPageModel implements PageInterface {
private loginForm = new LoginFormModel();

private view: LoginPageView;

constructor(parent: HTMLDivElement) {
this.view = new LoginPageView(parent);
this.init();
}

private init(): boolean {
this.getHTML().append(this.loginForm.getHTML());
return true;
}

public getHTML(): HTMLDivElement {
Expand Down
71 changes: 71 additions & 0 deletions src/shared/constants/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
3 changes: 3 additions & 0 deletions src/shared/img/svg/closeEye.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 3a1a243

Please sign in to comment.