Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(RSS-ECOMM-2_01): implement client side validation for login form #112

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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