Skip to content

Commit

Permalink
add validation
Browse files Browse the repository at this point in the history
  • Loading branch information
nelyaklyusa committed Jun 6, 2024
1 parent abd58a0 commit 077e1ca
Show file tree
Hide file tree
Showing 12 changed files with 921 additions and 0 deletions.
132 changes: 132 additions & 0 deletions src/components/common/FooterSubscribeForm/FooterSubscribeForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<script setup lang="ts">
import {
computed, nextTick, reactive, ref, watch,
} from 'vue';
import BaseButton from 'UiKit/components/BaseButton/BaseButton.vue';
import BaseFormInput from 'UiKit/components/BaseFormInput/BaseFormInput.vue';
import { JSONSchemaType } from 'ajv';
import { emailRule, errorMessageRule } from 'UiKit/helpers/validation/rules';
import { PrecompiledValidator } from 'UiKit/helpers/validation/PrecompiledValidator';
import BaseFormGroup from 'UiKit/components/BaseFormGroup/BaseFormGroup.vue';
import { isEmpty } from 'UiKit/helpers/general';
import { scrollToError } from 'UiKit/helpers/validation/general';
const emit = defineEmits(['submit']);
type FormModelSubscribe = {
email: number;
}
const schemaSubscribe = {
$schema: 'http://json-schema.org/draft-07/schema#',
definitions: {
PatchIndividualProfile: {
properties: {
email: emailRule,
},
type: 'object',
required: ['email'],
errorMessage: errorMessageRule,
},
},
$ref: '#/definitions/PatchIndividualProfile',
} as unknown as JSONSchemaType<FormModelSubscribe>;
const model = reactive({
} as FormModelSubscribe);
const validator = new PrecompiledValidator<FormModelSubscribe>(
schemaSubscribe,
);
const validation = ref<unknown>();
const isValid = computed(() => isEmpty(validation.value || {}));
const isDisabledButton = computed(() => (!isValid.value));
const onValidate = () => {
validation.value = validator.getFormValidationErrors(model);
};
const onSubmit = () => {
onValidate();
if (!isValid.value) {
void nextTick(() => scrollToError('FooterSubscribeForm'));
return;
}
emit('submit', model.email);
};
watch(() => model, () => {
if (!isValid.value) onValidate();
}, { deep: true });
</script>

<template>
<div class="FooterSubscribeForm footer-subscribe-form">
<div class="footer-subscribe-form__title is--h6__title">
Receive latest news:
</div>
<form
class="form-submit"
@submit.prevent="onSubmit"
>
<div class="field-group form-control">
<BaseFormGroup
v-slot="baseFormGroupProps"
:model="model"
:validation="validation"
:schema-front="schemaSubscribe"
path="email"
class="footer-subscribe-form__group"
>
<BaseFormInput
:is-error="baseFormGroupProps.isFieldError"
:model-value="model.email"
placeholder="Email Address"
name="email"
text
type="email"
size="large"
@update:model-value="model.email = $event"
/>
</BaseFormGroup>
<BaseButton
size="large"
:disabled="isDisabledButton"
:uppercase="false"
>
Subscribe
</BaseButton>
<span class="form-control-error" />
</div>
<div class="general-form-error" />
</form>
</div>
</template>


<style lang="scss">
.footer-subscribe-form {
$root: &;
&__group {
width: 100%;
}
&__form {
width: 100%;
}
&__title {
margin-bottom: 8px;
}
.field-group {
position: relative;
width: 100%;
display: flex;
align-items: start;
margin-bottom: 0;
gap: 4px;
}
}
</style>
2 changes: 2 additions & 0 deletions src/components/common/FooterSubscribeForm/index.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import 'UiKit/styles/_colors.sass'
@import 'UiKit/styles/_global.sass'
73 changes: 73 additions & 0 deletions src/helpers/general.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@

export function isEmpty(obj: object) {
// eslint-disable-next-line
for (const prop of Object.keys(obj)) {
return false;
}

return true;
}

export function formatPhoneNumber(phoneNumber: string | undefined): string | undefined {
if (!phoneNumber) return undefined;
// Remove all non-digit characters from the input string
let cleaned: string = phoneNumber.replace(/\D/g, '');
// Extract the country code (if present)
let countryCode: string = '';
if (cleaned.length > 10) {
countryCode = `+${cleaned.slice(0, cleaned.length - 10)} `;
// Remove country code from the cleaned string
cleaned = cleaned.slice(-10);
}
// Extract the area code and the rest of the number
const areaCode: string = cleaned.slice(0, 3);
const middlePart: string = cleaned.slice(3, 6);
const lastPart: string = cleaned.slice(6);

// Format the phone number parts into the desired format
const formattedPhoneNumber: string = `${countryCode}(${areaCode}) ${middlePart}-${lastPart}`;

return formattedPhoneNumber;
}


export function booleanFormatToString(value: boolean | undefined) {
if (value === undefined) return undefined;
if (value) return 'Yes';
return 'No';
}

export function checkObjectAndDeleteNotRequiredFields(
defaultParameters: string[],
requiredFields: string[],
obj: object,
) {
return Object.keys(obj).reduce((acc, key) => {
if (defaultParameters.includes(key)) {
acc[key] = obj[key]; // Set the new value for 'type'
} else if (requiredFields.includes(obj[key])) {
acc[key] = obj[key];
} else {
// If property value is not in requiredEmployment.value, delete the property
delete acc[key];
}
return acc;
}, {});
}

export function urlize(input: string): string {
// Convert the input string to lowercase and replace spaces with hyphens
let urlFriendlyString = input.toLowerCase().replace(/\s+/g, '-');

// Remove any characters that are not alphanumeric or hyphens
// eslint-disable-next-line
urlFriendlyString = urlFriendlyString.replace(/[^a-z0-9\-]/g, '');

// Remove any consecutive hyphens
urlFriendlyString = urlFriendlyString.replace(/-{2,}/g, '-');

// Trim leading and trailing hyphens
urlFriendlyString = urlFriendlyString.replace(/^-+|-+$/g, '');

return urlFriendlyString;
}
112 changes: 112 additions & 0 deletions src/helpers/validation/ErrorSchemaBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import cloneDeep from 'lodash/cloneDeep';
import get from 'lodash/get';
import set from 'lodash/set';

import { ErrorSchema } from './types';
import { ERRORS_KEY } from './constants';

/** The `ErrorSchemaBuilder<T>` is used to build an `ErrorSchema<T>` since the definition of the `ErrorSchema` type is
* designed for reading information rather than writing it. Use this class to add, replace or clear errors in an error
* schema by using either dotted path or an array of path names. Once you are done building the `ErrorSchema`, you can
* get the result and/or reset all the errors back to an initial set and start again.
*/
export default class ErrorSchemaBuilder<T = any> {
/** The error schema being built
*
* @private
*/
private errorSchema: ErrorSchema<T> = {};

/** Construct an `ErrorSchemaBuilder` with an optional initial set of errors in an `ErrorSchema`.
*
* @param [initialSchema] - The optional set of initial errors, that will be cloned into the class
*/
constructor(initialSchema?: ErrorSchema<T>) {
this.resetAllErrors(initialSchema);
}

/** Returns the `ErrorSchema` that has been updated by the methods of the `ErrorSchemaBuilder`
*/
public get ErrorSchema() {
return this.errorSchema;
}

/** Will get an existing `ErrorSchema` at the specified `pathOfError` or create and return one.
*
* @param [pathOfError] - The optional path into the `ErrorSchema` at which to add the error(s)
* @returns - The error block for the given `pathOfError` or the root if not provided
* @private
*/
private getOrCreateErrorBlock(pathOfError?: string | string[]) {
const hasPath = (Array.isArray(pathOfError) && pathOfError.length > 0) || typeof pathOfError === 'string';
let errorBlock: ErrorSchema = hasPath ? get(this.errorSchema, pathOfError) : this.errorSchema;
if (!errorBlock && pathOfError) {
errorBlock = {};
set(this.errorSchema, pathOfError, errorBlock);
}
return errorBlock;
}

/** Resets all errors in the `ErrorSchemaBuilder` back to the `initialSchema` if provided, otherwise an empty set.
*
* @param [initialSchema] - The optional set of initial errors, that will be cloned into the class
* @returns - The `ErrorSchemaBuilder` object for chaining purposes
*/
public resetAllErrors(initialSchema?: ErrorSchema<T>) {
this.errorSchema = initialSchema ? cloneDeep(initialSchema) : {};
return this;
}

/** Adds the `errorOrList` to the list of errors in the `ErrorSchema` at either the root level or the location within
* the schema described by the `pathOfError`. For more information about how to specify the path see the
* [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md).
*
* @param errorOrList - The error or list of errors to add into the `ErrorSchema`
* @param [pathOfError] - The optional path into the `ErrorSchema` at which to add the error(s)
* @returns - The `ErrorSchemaBuilder` object for chaining purposes
*/
public addErrors(errorOrList: string | string[], pathOfError?: string | string[]) {
const errorBlock: ErrorSchema = this.getOrCreateErrorBlock(pathOfError);
let errorsList = get(errorBlock, ERRORS_KEY);
if (!Array.isArray(errorsList)) {
errorsList = [];
errorBlock[ERRORS_KEY] = errorsList;
}

if (Array.isArray(errorOrList)) {
errorsList.push(...errorOrList);
} else {
errorsList.push(errorOrList);
}
return this;
}

/** Sets/replaces the `errorOrList` as the error(s) in the `ErrorSchema` at either the root level or the location
* within the schema described by the `pathOfError`. For more information about how to specify the path see the
* [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md).
*
* @param errorOrList - The error or list of errors to set into the `ErrorSchema`
* @param [pathOfError] - The optional path into the `ErrorSchema` at which to set the error(s)
* @returns - The `ErrorSchemaBuilder` object for chaining purposes
*/
public setErrors(errorOrList: string | string[], pathOfError?: string | string[]) {
const errorBlock: ErrorSchema = this.getOrCreateErrorBlock(pathOfError);
// Effectively clone the array being given to prevent accidental outside manipulation of the given list
const listToAdd = Array.isArray(errorOrList) ? [...errorOrList] : [errorOrList];
set(errorBlock, ERRORS_KEY, listToAdd);
return this;
}

/** Clears the error(s) in the `ErrorSchema` at either the root level or the location within the schema described by
* the `pathOfError`. For more information about how to specify the path see the
* [eslint lodash plugin docs](https://github.com/wix/eslint-plugin-lodash/blob/master/docs/rules/path-style.md).
*
* @param [pathOfError] - The optional path into the `ErrorSchema` at which to clear the error(s)
* @returns - The `ErrorSchemaBuilder` object for chaining purposes
*/
public clearErrors(pathOfError?: string | string[]) {
const errorBlock: ErrorSchema = this.getOrCreateErrorBlock(pathOfError);
set(errorBlock, ERRORS_KEY, []);
return this;
}
}
48 changes: 48 additions & 0 deletions src/helpers/validation/PrecompiledValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@

import { ajv } from './ajv';
import { JSONSchemaType, ValidateFunction } from 'ajv';
import { processRawValidationErrors } from './processRawValidationErrors';
import merge from 'lodash/merge';
import { undefinedEmptyProp } from './general';

export class PrecompiledValidator<T> {
private readonly rootSchema: JSONSchemaType<T>;

private readonly compiledValidator: ValidateFunction<T>;

constructor(schema1: JSONSchemaType<T>, schema2?: JSONSchemaType<T>) {
let schema = schema1;
if (schema2) schema = merge(schema1, schema2);
this.rootSchema = schema;
this.compiledValidator = this.getCompiledValidator(schema);
}

private getRawErrors() {
const { errors } = this.compiledValidator;
this.compiledValidator.errors = null;

return errors;
}

// eslint-disable-next-line
private getCompiledValidator(schema: JSONSchemaType<T>) {
return ajv.compile(schema);
}

private getIsValid(formData: T) {
return this.compiledValidator(formData);
}

private getRawValidation(formData: T) {
this.getIsValid(formData);
return this.getRawErrors();
}

public getFormValidationErrors(formData: T) {
const rawErrors = this.getRawValidation(undefinedEmptyProp(formData));
// Need empty structure here
// Solve this if no error validation
if (!rawErrors) return {};
return processRawValidationErrors(rawErrors);
}
}
Loading

0 comments on commit 077e1ca

Please sign in to comment.