Skip to content
This repository was archived by the owner on Jun 27, 2023. It is now read-only.

Commit 505114e

Browse files
committed
feat(validation): async input validation has born
1 parent 91aac4c commit 505114e

File tree

5 files changed

+121
-55
lines changed

5 files changed

+121
-55
lines changed

dev/typescript/App.vue

+12-2
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
</template>
6363

6464
<script lang="ts">
65-
import { mockAsync } from '@/core/utils/helpers';
65+
import { mockAsync, mockAsyncValidator } from '@/core/utils/helpers';
6666
import { computed, defineComponent, onMounted, reactive, ref } from 'vue';
6767
import {
6868
TextField,
@@ -94,6 +94,16 @@ export default defineComponent({
9494
text: 'Email format is incorrect',
9595
};
9696
97+
const emailUniquenessValidator: FormValidator = {
98+
validator: value =>
99+
mockAsyncValidator(
100+
'isUnique',
101+
value === '[email protected]',
102+
2000,
103+
),
104+
text: 'Email must be unique',
105+
};
106+
97107
const passwordValidator: FormValidator = Validator({
98108
validator: pattern(
99109
'^(?=.*[a-z])(?=.*[A-Z])(?=.*)(?=.*[#$^+=!*()@%&]).{8,10}$',
@@ -129,7 +139,7 @@ export default defineComponent({
129139
}),
130140
email: EmailField({
131141
label: 'Email',
132-
validations: [emailValidator],
142+
validations: [emailValidator, emailUniquenessValidator],
133143
customClass: {
134144
active: true,
135145
'text-blue': true,

src/components/dynamic-form/DynamicForm.vue

+4-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,10 @@ export default defineComponent({
217217
function onValidate({ name, errors, valid }: ValidationEvent) {
218218
const updatedCtrl = findControlByName(name);
219219
if (updatedCtrl) {
220-
updatedCtrl.errors = errors;
220+
updatedCtrl.errors = removeEmpty({
221+
...updatedCtrl.errors,
222+
...errors,
223+
});
221224
updatedCtrl.valid = valid;
222225
}
223226
}

src/composables/use-validation.ts

+77-32
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,86 @@
11
/* eslint-disable */
22

3-
import { computed } from 'vue';
3+
import { isPromise, removeEmpty } from '@/core/utils/helpers';
4+
import { computed, ref } from 'vue';
45

56
export function useInputValidation(props: any, emit: any) {
6-
const isRequired = computed(() => {
7-
return props.control.validations.some(validation => validation.type === 'required')
8-
});
7+
const isPendingValidation = ref(false);
8+
const isRequired = computed(() => {
9+
return props.control.validations.some(
10+
validation => validation.type === 'required',
11+
);
12+
});
13+
14+
const requiresValidation = computed(() => {
15+
return props.control.validations.length > 0;
16+
});
17+
18+
async function validate(): Promise<void> {
19+
if (
20+
(props.control.touched || props.control.dirty) &&
21+
requiresValidation.value
22+
) {
23+
let errors = {};
24+
const syncValidations = [];
25+
const asyncValidations = [];
26+
props.control.validations.forEach(element => {
27+
const validation = element.validator(props.control.value);
28+
if (validation.constructor.name === 'Promise') {
29+
asyncValidations.push({
30+
validation: element.validator,
31+
text: element.text,
32+
});
33+
} else {
34+
syncValidations.push({ validation, text: element.text });
35+
}
36+
});
937

10-
const requiresValidation = computed(() => {
11-
return props.control.validations.length > 0;
12-
});
38+
console.log({
39+
sync: syncValidations,
40+
async: asyncValidations,
41+
});
42+
if(asyncValidations.length > 0) {
43+
isPendingValidation.value = true;
1344

14-
function validate(): void {
15-
if ((props.control.touched || props.control.dirty) && requiresValidation.value) {
16-
const validation = props.control.validations.reduce((prev, curr) => {
17-
const val =
18-
typeof curr.validator === 'function'
19-
? curr.validator(props.control.value)
45+
Promise.all(
46+
asyncValidations.map(async ({ validation, text }) => ({
47+
validation: await validation(props.control.value),
48+
text,
49+
})),
50+
).then(errorsArr => {
51+
errorsArr.forEach(({ validation, text }) => {
52+
const [key, value] = Object.entries(validation)[0];
53+
errors[key] = value
54+
? {
55+
value,
56+
text,
57+
}
58+
: null;
59+
});
60+
isPendingValidation.value = false;
61+
emit('validate', {
62+
name: props.control.name,
63+
errors,
64+
valid: Object.keys(removeEmpty(errors)).length === 0,
65+
});
66+
});
67+
}
68+
syncValidations.forEach(({ validation, text }) => {
69+
if (validation) {
70+
const [key, value] = Object.entries(validation)[0];
71+
errors[key] = value
72+
? {
73+
value,
74+
text,
75+
}
2076
: null;
21-
if (val !== null) {
22-
const [key, value] = Object.entries(val)[0];
23-
const obj = {};
24-
obj[key] = {
25-
value,
26-
text: curr.text,
27-
};
28-
return {
29-
...prev,
30-
...obj,
31-
};
3277
}
33-
return {
34-
...prev,
35-
};
36-
}, {});
78+
});
79+
3780
emit('validate', {
3881
name: props.control.name,
39-
errors: validation,
40-
valid: Object.keys(validation).length === 0,
82+
errors,
83+
valid: Object.keys(removeEmpty(errors)).length === 0,
4184
});
4285
}
4386
}
@@ -46,19 +89,21 @@ export function useInputValidation(props: any, emit: any) {
4689
return [
4790
{
4891
'form-control--success':
92+
!isPendingValidation.value &&
4993
requiresValidation.value &&
94+
props.control.errors &&
5095
props.control.valid &&
5196
props.control.dirty &&
5297
props.control.touched,
5398
},
5499
{
55-
'form-control--error': !props.control.valid,
100+
'form-control--error': !isPendingValidation.value && !props.control.valid,
56101
},
57102
];
58-
59103
});
60104

61105
return {
106+
isPendingValidation,
62107
validate,
63108
getValidationClasses,
64109
isRequired,

src/core/utils/helpers.ts

+12
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const slugify = (text: string): string =>
1515
export const isArray = (a: any) => !!a && a.constructor === Array;
1616
export const isObject = (a: any) => !!a && a.constructor === Object;
1717
export const isEvent = (e: any) => !!e && e.constructor === Event;
18+
export const isPromise = (e: any) => !!e && e.constructor.name === Promise;
1819

1920
export const isEmpty = (entry: any) => {
2021
if (isArray(entry)) {
@@ -69,3 +70,14 @@ export const deepClone = (obj: any) => {
6970

7071
return clone;
7172
};
73+
74+
export const mockAsyncValidator = (validator, success, timeout) => {
75+
const validationResponse = {};
76+
validationResponse[validator] = success ? true: null;
77+
78+
return new Promise((resolve) => {
79+
setTimeout(() => {
80+
resolve(validationResponse);
81+
}, timeout);
82+
});
83+
};

src/core/utils/validators.ts

+16-20
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,21 @@ export const isEmptyInputValue = (value: ControlValue): boolean =>
66
const EMAIL_REGEXP = /^(?=.{1,254}$)(?=.{1,64}@)[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+(\.[-!#$%&'*+/0-9=?A-Z^_`a-z{|}~]+)*@[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
77
const URL_REGEXP = /^((?:(https?):\/\/)?((?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9][0-9]|[0-9])\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9][0-9]|[0-9])\.)(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9][0-9]|[0-9])\.)(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9][0-9]|[0-9]))|(?:(?:(?:\w+\.){1,2}[\w]{2,3})))(?::(\d+))?((?:\/[\w]+)*)(?:\/|(\/[\w]+\.[\w]{3,4})|(\?(?:([\w]+=[\w]+)&)*([\w]+=[\w]+))?|\?(?:(wsdl|wadl))))$/;
88

9-
export const required = (value: ControlValue): ValidationErrors | null =>
10-
isEmptyInputValue(value) ? { required: true } : null;
9+
export const required = (value: ControlValue): ValidationErrors => ({
10+
required: isEmptyInputValue(value) ? true : null,
11+
});
1112

12-
export const min = (min: number) => (
13-
value: number,
14-
): ValidationErrors | null => {
13+
export const min = (min: number) => (value: number): ValidationErrors => {
1514
if (isEmptyInputValue(value) || isEmptyInputValue(min)) {
16-
return null; // don't validate empty values to allow optional controls
15+
return { min: null }; // don't validate empty values to allow optional controls
1716
}
1817
const minValue = parseFloat(`${value}`);
19-
20-
return !isNaN(minValue) && minValue < min
21-
? { min: { min, actual: +minValue } }
22-
: null;
18+
return {
19+
min: !isNaN(minValue) && minValue < min ? { min, actual: +minValue } : null,
20+
};
2321
};
2422

25-
export const max = (max: number) => (
26-
value: number,
27-
): ValidationErrors | null => {
23+
export const max = (max: number) => (value: number): ValidationErrors => {
2824
if (isEmptyInputValue(value) || isEmptyInputValue(max)) {
2925
return null; // don't validate empty values to allow optional controls
3026
}
@@ -36,23 +32,23 @@ export const max = (max: number) => (
3632
: null;
3733
};
3834

39-
export const email = (value: string): ValidationErrors | null => {
35+
export const email = (value: string): ValidationErrors => {
4036
if (isEmptyInputValue(value)) {
41-
return null; // don't validate empty values to allow optional controls
37+
return { email: null }; // don't validate empty values to allow optional controls
4238
}
43-
return EMAIL_REGEXP.test(`${value}`) ? null : { email: true };
39+
return { email: EMAIL_REGEXP.test(`${value}`) ? null : true };
4440
};
4541

46-
export const url = (value: string): ValidationErrors | null => {
42+
export const url = (value: string): ValidationErrors => {
4743
if (isEmptyInputValue(value)) {
4844
return null; // don't validate empty values to allow optional controls
4945
}
50-
return URL_REGEXP.test(`${value}`) ? null : { email: true };
46+
return URL_REGEXP.test(`${value}`) ? null : { url: true };
5147
};
5248

5349
export const minLength = (minLength: number) => (
5450
value: number,
55-
): ValidationErrors | null => {
51+
): ValidationErrors => {
5652
if (isEmptyInputValue(value)) {
5753
return null; // don't validate empty values to allow optional controls
5854
}
@@ -64,7 +60,7 @@ export const minLength = (minLength: number) => (
6460

6561
export const maxLength = (maxLength: number) => (
6662
value: number,
67-
): ValidationErrors | null => {
63+
): ValidationErrors => {
6864
if (isEmptyInputValue(value)) {
6965
return null; // don't validate empty values to allow optional controls
7066
}

0 commit comments

Comments
 (0)