Merge pull request #508 from smartxworks/feat/validation-trait

feat: refactor the validation trait (#breaking-chagnes)
This commit is contained in:
tanbowensg 2022-07-22 11:29:43 +08:00 committed by GitHub
commit fa79e120a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 1315 additions and 102 deletions

View File

@ -139,10 +139,29 @@
{
"type": "core/v1/validation",
"properties": {
"value": "{{ phoneInput.value || \"\" }}",
"maxLength": 100,
"minLength": 0,
"rule": "phoneNumber"
"validators": [
{
"name": "phone",
"value": "{{ phoneInput.value || \"\" }}",
"rules": [
{
"type": "regex",
"regex": "^1[3456789]\\d{9}$",
"error": {
"message": "Please input the correct phone number."
}
},
{
"type": "length",
"minLength": 0,
"maxLength": 100,
"error": {
"message": "Please input the length between 0 and 100."
}
}
]
}
]
}
}
]

View File

@ -314,9 +314,22 @@
{
"type": "core/v1/validation",
"properties": {
"value": "{{ nameInput.value || \"\" }}",
"maxLength": 10,
"minLength": 2
"validators": [
{
"name": "name",
"value": "{{ nameInput.value || \"\" }}",
"rules": [
{
"type": "length",
"maxLength": 10,
"minLength": 2,
"error": {
"message": "Please input the length between 2 and 10."
}
}
]
}
]
}
}
]

View File

@ -0,0 +1,855 @@
{
"app": {
"version": "sunmao/v1",
"kind": "Application",
"metadata": {
"name": "some App"
},
"spec": {
"components": [
{
"id": "name_form",
"type": "arco/v1/formControl",
"properties": {
"label": {
"format": "plain",
"raw": "name"
},
"layout": "horizontal",
"required": true,
"hidden": false,
"extra": "",
"errorMsg": "{{name_form.validatedResult.name?.errors[0]?.message || '';}}",
"labelAlign": "left",
"colon": false,
"help": "",
"labelCol": {
"span": 3,
"offset": 0
},
"wrapperCol": {
"span": 21,
"offset": 0
}
},
"traits": [
{
"type": "core/v1/validation",
"properties": {
"validators": [
{
"name": "name",
"value": "{{name_input.value;}}",
"rules": [
{
"type": "required",
"validate": null,
"error": {
"message": "Please input the name."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
},
{
"type": "length",
"validate": null,
"error": {
"message": "The name is limited in length to between 1 and 10."
},
"minLength": 1,
"maxLength": 10,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
}
]
}
]
}
}
]
},
{
"id": "name_input",
"type": "arco/v1/input",
"properties": {
"allowClear": false,
"disabled": false,
"readOnly": false,
"defaultValue": "",
"updateWhenDefaultValueChanges": false,
"placeholder": "please input the name.",
"error": "{{name_form.validatedResult.name.isInvalid}}",
"size": "default"
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "name_form",
"slot": "content"
},
"ifCondition": true
}
},
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onChange",
"componentId": "name_form",
"method": {
"name": "validateFields",
"parameters": {
"names": "{{['name']}}"
}
}
},
{
"type": "onBlur",
"componentId": "name_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
},
{
"type": "onChange",
"componentId": "check_name",
"method": {
"name": "triggerFetch",
"parameters": {}
}
},
{
"type": "onBlur",
"componentId": "check_name",
"method": {
"name": "triggerFetch",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "email_form",
"type": "arco/v1/formControl",
"properties": {
"label": {
"format": "plain",
"raw": "email"
},
"layout": "horizontal",
"required": true,
"hidden": false,
"extra": "",
"errorMsg": "{{email_form.validatedResult.email?.errors[0]?.message;}}",
"labelAlign": "left",
"colon": false,
"help": "",
"labelCol": {
"span": 3,
"offset": 0
},
"wrapperCol": {
"span": 21,
"offset": 0
}
},
"traits": [
{
"type": "core/v1/validation",
"properties": {
"validators": [
{
"name": "email",
"value": "{{email_input.value;}}",
"rules": [
{
"type": "required",
"validate": null,
"error": {
"message": "Please input the email."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
},
{
"type": "email",
"validate": null,
"error": {
"message": "Please input the correct email."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
}
]
}
]
}
}
]
},
{
"id": "email_input",
"type": "arco/v1/input",
"properties": {
"allowClear": false,
"disabled": false,
"readOnly": false,
"defaultValue": "",
"updateWhenDefaultValueChanges": false,
"placeholder": "please input the email.",
"error": "{{email_form.validatedResult.email.isInvalid}}",
"size": "default"
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "email_form",
"slot": "content"
},
"ifCondition": true
}
},
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onChange",
"componentId": "email_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
},
{
"type": "onBlur",
"componentId": "email_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "url_form",
"type": "arco/v1/formControl",
"properties": {
"label": {
"format": "plain",
"raw": "URL"
},
"layout": "horizontal",
"required": true,
"hidden": false,
"extra": "",
"errorMsg": "{{url_form.validatedResult.url?.errors[0]?.message}}",
"labelAlign": "left",
"colon": false,
"help": "",
"labelCol": {
"span": 3,
"offset": 0
},
"wrapperCol": {
"span": 21,
"offset": 0
}
},
"traits": [
{
"type": "core/v1/validation",
"properties": {
"validators": [
{
"name": "url",
"value": "{{url_input.value}}",
"rules": [
{
"type": "required",
"validate": null,
"error": {
"message": "Please input the URL. "
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
},
{
"type": "url",
"validate": null,
"error": {
"message": "Please input the correct URL."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
}
]
}
]
}
}
]
},
{
"id": "url_input",
"type": "arco/v1/input",
"properties": {
"allowClear": false,
"disabled": false,
"readOnly": false,
"defaultValue": "",
"updateWhenDefaultValueChanges": false,
"placeholder": "please input the URL.",
"error": "{{url_form.validatedResult.url.isInvalid}}",
"size": "default"
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "url_form",
"slot": "content"
},
"ifCondition": true
}
},
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onChange",
"componentId": "url_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
},
{
"type": "onBlur",
"componentId": "url_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "ip_form",
"type": "arco/v1/formControl",
"properties": {
"label": {
"format": "plain",
"raw": "IP"
},
"layout": "horizontal",
"required": true,
"hidden": false,
"extra": "",
"errorMsg": "{{ip_form.validatedResult.ip?.errors[0]?.message}}",
"labelAlign": "left",
"colon": false,
"help": "",
"labelCol": {
"span": 3,
"offset": 0
},
"wrapperCol": {
"span": 21,
"offset": 0
}
},
"traits": [
{
"type": "core/v1/validation",
"properties": {
"validators": [
{
"name": "ip",
"value": "{{ip_input.value}}",
"rules": [
{
"type": "required",
"validate": null,
"error": {
"message": "Please input the IP."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
},
{
"type": "ipv4",
"validate": null,
"error": {
"message": "Please input the correct IP."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
}
]
}
]
}
}
]
},
{
"id": "ip_input",
"type": "arco/v1/input",
"properties": {
"allowClear": false,
"disabled": false,
"readOnly": false,
"defaultValue": "",
"updateWhenDefaultValueChanges": false,
"placeholder": "please input the IP.",
"error": "{{ip_form.validatedResult.ip.isInvalid}}",
"size": "default"
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "ip_form",
"slot": "content"
},
"ifCondition": true
}
},
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onChange",
"componentId": "ip_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
},
{
"type": "onBlur",
"componentId": "ip_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "phone_form",
"type": "arco/v1/formControl",
"properties": {
"label": {
"format": "plain",
"raw": "phone"
},
"layout": "horizontal",
"required": true,
"hidden": false,
"extra": "",
"errorMsg": "{{phone_form.validatedResult.phone.errors[0]?.message}}",
"labelAlign": "left",
"colon": false,
"help": "",
"labelCol": {
"span": 3,
"offset": 0
},
"wrapperCol": {
"span": 21,
"offset": 0
}
},
"traits": [
{
"type": "core/v1/validation",
"properties": {
"validators": [
{
"name": "phone",
"value": "{{phone_input.value}}",
"rules": [
{
"type": "required",
"validate": null,
"error": {
"message": "Please input the phone number."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
},
{
"type": "regex",
"validate": null,
"error": {
"message": "Please input the correct phone number."
},
"minLength": 0,
"maxLength": 0,
"list": [],
"min": 0,
"max": 0,
"regex": "^1[3456789]\\d{9}$",
"flags": "",
"customOptions": {}
}
]
}
]
}
}
]
},
{
"id": "phone_input",
"type": "arco/v1/input",
"properties": {
"allowClear": false,
"disabled": false,
"readOnly": false,
"defaultValue": "",
"updateWhenDefaultValueChanges": false,
"placeholder": "please input the phone number.",
"error": "{{phone_form.validatedResult.phone.isInvalid}}",
"size": "default"
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "phone_form",
"slot": "content"
},
"ifCondition": true
}
},
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onChange",
"componentId": "phone_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
},
{
"type": "onBlur",
"componentId": "phone_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "city_form",
"type": "arco/v1/formControl",
"properties": {
"label": {
"format": "plain",
"raw": "city"
},
"layout": "horizontal",
"required": false,
"hidden": false,
"extra": "",
"errorMsg": "{{city_form.validatedResult.city.errors[0]?.message}}",
"labelAlign": "left",
"colon": false,
"help": "",
"labelCol": {
"span": 3,
"offset": 0
},
"wrapperCol": {
"span": 21,
"offset": 0
}
},
"traits": [
{
"type": "core/v1/validation",
"properties": {
"validators": [
{
"name": "city",
"value": "{{city_select.value}}",
"rules": [
{
"type": "include",
"validate": null,
"error": {
"message": "Please select \"Beijing\"."
},
"minLength": 0,
"maxLength": 0,
"includeList": ["Beijing"],
"excludeList": [],
"min": 0,
"max": 0,
"regex": "",
"flags": "",
"customOptions": {}
}
]
}
]
}
}
]
},
{
"id": "city_select",
"type": "arco/v1/select",
"properties": {
"allowClear": false,
"multiple": false,
"allowCreate": false,
"bordered": true,
"defaultValue": "Beijing",
"disabled": false,
"labelInValue": false,
"loading": false,
"showSearch": false,
"unmountOnExit": false,
"options": [
{
"value": "Beijing",
"text": "Beijing"
},
{
"value": "London",
"text": "London"
},
{
"value": "NewYork",
"text": "NewYork"
}
],
"placeholder": "Select city",
"size": "default",
"error": false,
"updateWhenDefaultValueChanges": false
},
"traits": [
{
"type": "core/v1/slot",
"properties": {
"container": {
"id": "city_form",
"slot": "content"
},
"ifCondition": true
}
},
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onChange",
"componentId": "city_form",
"method": {
"name": "validateAllFields",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "clear_button",
"type": "arco/v1/button",
"properties": {
"type": "default",
"status": "default",
"long": false,
"size": "default",
"disabled": false,
"loading": false,
"shape": "square",
"text": "clear"
},
"traits": [
{
"type": "core/v1/event",
"properties": {
"handlers": [
{
"type": "onClick",
"componentId": "name_form",
"method": {
"name": "clearAllErrors",
"parameters": {}
}
},
{
"type": "onClick",
"componentId": "email_form",
"method": {
"name": "clearAllErrors",
"parameters": {}
}
},
{
"type": "onClick",
"componentId": "url_form",
"method": {
"name": "clearAllErrors",
"parameters": {}
}
},
{
"type": "onClick",
"componentId": "ip_form",
"method": {
"name": "clearErrors",
"parameters": {
"names": "{{['ip']}}"
}
}
},
{
"type": "onClick",
"componentId": "phone_form",
"method": {
"name": "clearErrors",
"parameters": {
"names": "{{['phone']}}"
}
}
},
{
"type": "onClick",
"componentId": "city_form",
"method": {
"name": "clearAllErrors",
"parameters": {}
}
}
]
}
}
]
},
{
"id": "check_name",
"type": "core/v1/dummy",
"properties": {},
"traits": [
{
"type": "core/v1/fetch",
"properties": {
"url": "",
"method": "get",
"lazy": false,
"disabled": false,
"headers": {},
"body": {},
"bodyType": "json",
"onComplete": [
{
"componentId": "name_form",
"method": {
"name": "setErrors",
"parameters": {
"errorsMap": "{{\n{\n name: [...name_form.validatedResult.name.errors, { message: 'The name is exist.' }]\n}\n}}"
}
}
}
],
"onError": []
}
}
]
}
]
}
}
}

View File

@ -21,10 +21,28 @@
{
"type": "core/v1/validation",
"properties": {
"value": "{{ emailInput.value || \"\" }}",
"maxLength": 20,
"minLength": 10,
"rule": "email"
"validators": [
{
"name": "email",
"value": "{{ emailInput.value || \"\" }}",
"rules": [
{
"type": "email",
"error": {
"message": "Please input the email."
}
},
{
"type": "length",
"maxLength": 20,
"minLength": 10,
"error": {
"message": "Please input the length between 10 and 20."
}
}
]
}
]
}
}
]
@ -34,7 +52,7 @@
"type": "core/v1/text",
"properties": {
"value": {
"raw": "{{ emailInput.validResult.errorMsg }}",
"raw": "{{ emailInput.validatedResult.email.errors[0]?.message }}",
"format": "plain"
}
},
@ -54,10 +72,29 @@
{
"type": "core/v1/validation",
"properties": {
"value": "{{ phoneInput.value || \"\" }}",
"maxLength": 100,
"minLength": 0,
"rule": "phoneNumber"
"validators": [
{
"name": "phone",
"value": "{{ phoneInput.value || \"\" }}",
"rules": [
{
"type": "regex",
"regex": "^1[3456789]\\d{9}$",
"error": {
"message": "Please input the correct phone number."
}
},
{
"type": "length",
"maxLength": 100,
"minLength": 0,
"error": {
"message": "Please input the length between 0 and 100."
}
}
]
}
]
}
}
]
@ -95,7 +132,7 @@
"parameters": "{{ `email${ emailInput.value } phone${ phoneInput.value }` }}"
},
"wait": {},
"disabled": "{{ emailInput.validResult.isInvalid || phoneInput.validResult.isInvalid }}"
"disabled": "{{ emailInput.validatedResult.email.isInvalid || phoneInput.validatedResult.phone.isInvalid }}"
}
]
}

View File

@ -264,9 +264,22 @@
{
"type": "core/v1/validation",
"properties": {
"value": "{{ nameInput.value || \"\" }}",
"maxLength": 10,
"minLength": 2
"validators": [
{
"name": "name",
"value": "{{ nameInput.value || \"\" }}",
"rules": [
{
"type": "length",
"maxLength": 10,
"minLength": 2,
"error": {
"message": "Please input the length between 2 and 10."
}
}
]
}
]
}
}
]

View File

@ -14,7 +14,9 @@
import { initSunmaoUI } from './src';
import { ChakraProvider } from '@chakra-ui/react';
import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib';
import { ArcoDesignLib } from '@sunmao-ui/arco-lib';
import examples from '@example.json';
import '@arco-design/web-react/dist/css/arco.css';
const selectEl = document.querySelector('select');
for (const example of examples) {
@ -29,7 +31,9 @@
const rootEl = document.querySelector('#root');
const render = example => {
ReactDOM.unmountComponentAtNode(rootEl);
const { App, registry } = initSunmaoUI({ libs: [sunmaoChakraUILib] });
const { App, registry } = initSunmaoUI({
libs: [sunmaoChakraUILib, ArcoDesignLib],
});
const { app, modules = [] } = example.value;
window.registry = registry;
modules.forEach(m => {

View File

@ -1,112 +1,384 @@
import { Static, Type } from '@sinclair/typebox';
import { isEqual } from 'lodash';
import { Type, Static, TSchema } from '@sinclair/typebox';
import { implementRuntimeTrait } from '../../utils/buildKit';
import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared';
import { CORE_VERSION, CoreTraitName, StringUnion } from '@sunmao-ui/shared';
type ValidationResult = Static<typeof ResultSpec>;
type ValidationRule = (text: string) => ValidationResult;
type ParseValidateOption<
T extends Record<string, TSchema>,
OptionalKeys extends keyof T = ''
> = {
[K in keyof T as K extends OptionalKeys ? never : K]: Static<T[K]>;
} & {
[K in OptionalKeys]?: Static<T[K]>;
};
type UnionToIntersection<TUnion> = (
TUnion extends unknown ? (params: TUnion) => unknown : never
) extends (params: infer Params) => unknown
? Params
: never;
const ResultSpec = Type.Object({
isInvalid: Type.Boolean(),
errorMsg: Type.String(),
});
const validateOptionMap = {
length: {
minLength: Type.Number({ title: 'Min length' }),
maxLength: Type.Number({ title: 'Max length' }),
},
include: {
includeList: Type.Array(Type.String(), { title: 'Include list' }),
},
exclude: {
excludeList: Type.Array(Type.String(), { title: 'Exclude List' }),
},
number: {
min: Type.Number({ title: 'Min' }),
max: Type.Number({ title: 'Max' }),
},
regex: {
regex: Type.String({ title: 'Regex', description: 'The regular expression string.' }),
flags: Type.String({
title: 'Flags',
description: 'The flags of the regular expression.',
}),
},
};
const validateFnMap = {
required(value: string) {
return value !== undefined && value !== null && value !== '';
},
length(
value: string,
{
minLength,
maxLength,
}: ParseValidateOption<typeof validateOptionMap['length'], 'minLength' | 'maxLength'>
) {
if (minLength !== undefined && value.length < minLength) {
return false;
}
export const ValidationTraitStateSpec = Type.Object({
validResult: ResultSpec,
if (maxLength !== undefined && value.length > maxLength) {
return false;
}
return true;
},
include(
value: string,
{ includeList }: ParseValidateOption<typeof validateOptionMap['include']>
) {
return includeList.includes(value);
},
exclude(
value: string,
{ excludeList }: ParseValidateOption<typeof validateOptionMap['exclude']>
) {
return !excludeList.includes(value);
},
regex(
value: string,
{ regex, flags }: ParseValidateOption<typeof validateOptionMap['regex'], 'flags'>
) {
return new RegExp(regex, flags).test(value);
},
number(
value: string | number,
{ min, max }: ParseValidateOption<typeof validateOptionMap['number'], 'min' | 'max'>
) {
const num = Number(value);
if (min !== undefined && num < min) {
return false;
}
if (max !== undefined && num > max) {
return false;
}
return true;
},
ipv4(value: string) {
return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
value
);
},
email(value: string) {
return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value);
},
url(value: string) {
return /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test(
value
);
},
};
type AllFields = UnionToIntersection<
typeof validateOptionMap[keyof typeof validateOptionMap]
>;
type AllFieldsKeys = keyof UnionToIntersection<AllFields>;
const ValidatorSpec = Type.Object({
name: Type.String({
title: 'Name',
description: 'The name is used for getting the validated result.',
}),
value: Type.Any({ title: 'Value', description: 'The value need to be validated.' }),
rules: Type.Array(
Type.Object(
{
type: StringUnion(
Object.keys(validateFnMap).concat(['custom']) as [
keyof typeof validateFnMap,
'custom'
],
{
type: 'Type',
description:
'The type of the rule. Setting it as `custom` to use custom validate function.',
}
),
validate:
Type.Any({
title: 'Validate',
description:
'The validate function for the rule. Return `false` means it is invalid.',
conditions: [
{
key: 'type',
value: 'custom',
},
],
}) ||
Type.Function(
[Type.String(), Type.Record(Type.String(), Type.Any())],
Type.Boolean(),
{
conditions: [
{
key: 'type',
value: 'custom',
},
],
}
),
error: Type.Object(
{
message: Type.String({
title: 'Message',
description: 'The message to display when the value is invalid.',
}),
},
{ title: 'Error' }
),
...(Object.keys(validateOptionMap) as [keyof typeof validateOptionMap]).reduce(
(result, key) => {
const option = validateOptionMap[key] as AllFields;
(Object.keys(option) as [AllFieldsKeys]).forEach(optionKey => {
if (result[optionKey]) {
// if the different validate functions have the same parameter
throw Error(
"[Validation Trait]: The different validate function has the same parameter, please change the parameter's name."
);
} else {
result[optionKey] = {
...option[optionKey],
conditions: [{ key: 'type', value: key }],
} as any;
}
});
return result;
},
{} as AllFields
),
customOptions: Type.Record(Type.String(), Type.Any(), {
title: 'Custom options',
description:
'The custom options would pass to the custom validate function as the second parameter.',
conditions: [
{
key: 'type',
value: 'custom',
},
],
}),
},
{
title: 'Rules',
}
),
{
title: 'Rules',
widget: 'core/v1/array',
widgetOptions: { displayedKeys: ['type'] },
}
),
});
export const ValidationTraitPropertiesSpec = Type.Object({
value: Type.String(),
rule: Type.Optional(Type.String()),
maxLength: Type.Optional(Type.Integer()),
minLength: Type.Optional(Type.Integer()),
validators: Type.Array(ValidatorSpec, {
title: 'Validators',
widget: 'core/v1/array',
widgetOptions: { displayedKeys: ['name'] },
}),
});
const ErrorSpec = Type.Object({
message: Type.String(),
});
const ValidatedResultSpec = Type.Record(
Type.String(),
Type.Object({
isInvalid: Type.Boolean(),
errors: Type.Array(ErrorSpec),
})
);
export const ValidationTraitStateSpec = Type.Object({
validatedResult: ValidatedResultSpec,
});
export default implementRuntimeTrait({
version: CORE_VERSION,
metadata: {
name: CoreTraitName.Validation,
description: 'validation trait',
description: 'A trait for the form validation.',
},
spec: {
properties: ValidationTraitPropertiesSpec,
state: ValidationTraitStateSpec,
methods: [],
methods: [
{
name: 'setErrors',
parameters: Type.Object({
errorsMap: Type.Record(Type.String(), Type.Array(ErrorSpec)),
}),
},
{
name: 'validateFields',
parameters: Type.Object({
names: Type.Array(Type.String()),
}),
},
{
name: 'validateAllFields',
parameters: Type.Object({}),
},
{
name: 'clearErrors',
parameters: Type.Object({ names: Type.Array(Type.String()) }),
},
{
name: 'clearAllErrors',
parameters: Type.Object({}),
},
],
},
})(() => {
const rules = new Map<string, ValidationRule>();
function addValidationRule(name: string, rule: ValidationRule) {
rules.set(name, rule);
}
addValidationRule('email', text => {
if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(text)) {
return {
isInvalid: false,
errorMsg: '',
};
} else {
return {
isInvalid: true,
errorMsg: 'Please enter valid email.',
};
}
});
addValidationRule('phoneNumber', text => {
if (/^1[3456789]\d{9}$/.test(text)) {
return {
isInvalid: false,
errorMsg: '',
};
} else {
return {
isInvalid: true,
errorMsg: 'Please enter valid phone number.',
};
}
});
const ValidationResultCache: Record<string, ValidationResult> = {};
const initialMap = new Map();
return props => {
const { value, minLength, maxLength, mergeState, componentId, rule } = props;
const { validators, componentId, subscribeMethods, mergeState } = props;
const validatorMap = validators.reduce((result, validator) => {
result[validator.name] = validator;
const result: ValidationResult = {
isInvalid: false,
errorMsg: '',
};
return result;
}, {} as Record<string, Static<typeof ValidatorSpec>>);
if (maxLength !== undefined && value.length > maxLength) {
result.isInvalid = true;
result.errorMsg = `Can not be longer than ${maxLength}.`;
} else if (minLength !== undefined && value.length < minLength) {
result.isInvalid = true;
result.errorMsg = `Can not be shorter than ${minLength}.`;
} else {
const rulesArr = rule ? rule.split(',') : [];
for (const ruleName of rulesArr) {
const validateFunc = rules.get(ruleName);
if (validateFunc) {
const { isInvalid, errorMsg } = validateFunc(value);
if (isInvalid) {
result.isInvalid = true;
result.errorMsg = errorMsg;
break;
}
}
}
function setErrors({
errorsMap,
}: {
errorsMap: Record<string, Static<typeof ErrorSpec>[]>;
}) {
const validatedResult = Object.keys(errorsMap).reduce(
(result: Static<typeof ValidatedResultSpec>, name) => {
result[name] = {
isInvalid: errorsMap[name].length !== 0,
errors: errorsMap[name],
};
return result;
},
{}
);
mergeState({
validatedResult,
});
}
function validateFields({ names }: { names: string[] }) {
const validatedResult = names
.map(name => {
const validator = validatorMap[name];
const { value, rules } = validator;
const errors = rules
.map(rule => {
const { type, error, validate, customOptions, ...options } = rule;
let isValid = true;
if (type === 'custom') {
isValid = validate(value, customOptions);
} else {
isValid = validateFnMap[type](value, options);
}
return isValid ? null : { message: error.message };
})
.filter((error): error is Static<typeof ErrorSpec> => error !== null);
return {
name,
isInvalid: errors.length !== 0,
errors,
};
})
.reduce((result: Static<typeof ValidatedResultSpec>, validatedResultItem) => {
result[validatedResultItem.name] = {
isInvalid: validatedResultItem.isInvalid,
errors: validatedResultItem.errors,
};
return result;
}, {});
mergeState({ validatedResult });
}
function validateAllFields() {
validateFields({ names: validators.map(({ name }) => name) });
}
function clearErrors({ names }: { names: string[] }) {
setErrors({
errorsMap: names.reduce((result: Record<string, []>, name) => {
result[name] = [];
return result;
}, {}),
});
}
function clearAllErrors() {
clearErrors({ names: validators.map(({ name }) => name) });
}
if (!isEqual(result, ValidationResultCache[componentId])) {
ValidationResultCache[componentId] = result;
mergeState({
validResult: result,
});
subscribeMethods({
setErrors,
validateFields,
validateAllFields,
clearErrors,
clearAllErrors,
});
if (initialMap.has(componentId) === false) {
clearAllErrors();
initialMap.set(componentId, true);
}
return {
props: null,
props: {
componentDidUnmount: [
() => {
initialMap.delete(componentId);
},
],
},
};
};
});