@splitty-test/validation 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +8 -0
- package/.gitattributes +1 -0
- package/.prettierrc.json +6 -0
- package/.vscode/extensions.json +8 -0
- package/README.md +39 -0
- package/env.d.ts +1 -0
- package/eslint.config.ts +22 -0
- package/lib/FieldValidation.vue +96 -0
- package/lib/FieldValidatorClass.ts +96 -0
- package/lib/ValidatorClass.ts +106 -0
- package/lib/index.ts +6 -0
- package/lib/rules/index.ts +0 -0
- package/lib/rules/match.ts +0 -0
- package/package.json +40 -0
- package/tsconfig.app.json +12 -0
- package/tsconfig.json +11 -0
- package/tsconfig.node.json +19 -0
- package/vite.config.ts +30 -0
package/.editorconfig
ADDED
package/.gitattributes
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* text=auto eol=lf
|
package/.prettierrc.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# validation
|
|
2
|
+
|
|
3
|
+
This template should help get you started developing with Vue 3 in Vite.
|
|
4
|
+
|
|
5
|
+
## Recommended IDE Setup
|
|
6
|
+
|
|
7
|
+
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
|
8
|
+
|
|
9
|
+
## Type Support for `.vue` Imports in TS
|
|
10
|
+
|
|
11
|
+
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
|
12
|
+
|
|
13
|
+
## Customize configuration
|
|
14
|
+
|
|
15
|
+
See [Vite Configuration Reference](https://vite.dev/config/).
|
|
16
|
+
|
|
17
|
+
## Project Setup
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npm install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Compile and Hot-Reload for Development
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm run dev
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Type-Check, Compile and Minify for Production
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
npm run build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Lint with [ESLint](https://eslint.org/)
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm run lint
|
|
39
|
+
```
|
package/env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
package/eslint.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { globalIgnores } from 'eslint/config'
|
|
2
|
+
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
|
3
|
+
import pluginVue from 'eslint-plugin-vue'
|
|
4
|
+
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
|
5
|
+
|
|
6
|
+
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
|
7
|
+
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
|
8
|
+
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
|
9
|
+
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
|
10
|
+
|
|
11
|
+
export default defineConfigWithVueTs(
|
|
12
|
+
{
|
|
13
|
+
name: 'app/files-to-lint',
|
|
14
|
+
files: ['**/*.{ts,mts,tsx,vue}'],
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']),
|
|
18
|
+
|
|
19
|
+
pluginVue.configs['flat/essential'],
|
|
20
|
+
vueTsConfigs.recommended,
|
|
21
|
+
skipFormatting,
|
|
22
|
+
)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div ref="root" :class="{ invalid: error }">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
<div v-if="!hideError" class="validation-error">{{ error }}</div>
|
|
5
|
+
</div>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { defineComponent, nextTick, type PropType } from 'vue';
|
|
10
|
+
import { Validator } from './ValidatorClass';
|
|
11
|
+
import { remove } from 'lodash-es';
|
|
12
|
+
import type { ValidationRule } from './FieldValidatorClass';
|
|
13
|
+
|
|
14
|
+
export default defineComponent({
|
|
15
|
+
name: 'FieldValidation',
|
|
16
|
+
props: {
|
|
17
|
+
name: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true,
|
|
20
|
+
},
|
|
21
|
+
hideError: Boolean,
|
|
22
|
+
modelValue: {
|
|
23
|
+
type: null,
|
|
24
|
+
},
|
|
25
|
+
modelModifiers: {
|
|
26
|
+
default: () => ({}),
|
|
27
|
+
},
|
|
28
|
+
rules: {
|
|
29
|
+
type: Array as PropType<ValidationRule[]>,
|
|
30
|
+
default() {
|
|
31
|
+
return [];
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
validator: {
|
|
35
|
+
type: Validator,
|
|
36
|
+
required: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
data() {
|
|
40
|
+
return {
|
|
41
|
+
form_fields: [] as HTMLElement[],
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
computed: {
|
|
45
|
+
error() {
|
|
46
|
+
if (this.validator.fields[this.name]?.errors.length) {
|
|
47
|
+
return this.validator.fields[this.name].errors[0];
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
mounted() {
|
|
53
|
+
// Add the field to the validator
|
|
54
|
+
if (!this.validator.fields[this.name]) {
|
|
55
|
+
let group;
|
|
56
|
+
if (Object.keys(this.modelModifiers).length === 1) {
|
|
57
|
+
group = Object.keys(this.modelModifiers)[0];
|
|
58
|
+
}
|
|
59
|
+
this.validator.addField(this.name, {
|
|
60
|
+
value: this.modelValue,
|
|
61
|
+
rules: this.rules,
|
|
62
|
+
group,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
nextTick(() => {
|
|
67
|
+
// Add control elements to the validation methods
|
|
68
|
+
const root = this.$refs.root as HTMLElement;
|
|
69
|
+
const form_fields = root.querySelectorAll<HTMLElement>('input, select, textarea');
|
|
70
|
+
this.form_fields = Array.from(form_fields);
|
|
71
|
+
this.form_fields.forEach((form_field) => {
|
|
72
|
+
if (form_field.getAttribute('has-validation') !== 'true') {
|
|
73
|
+
this.validator.fields[this.name].controls.push(form_field);
|
|
74
|
+
form_field.addEventListener('blur', (event: FocusEvent) => {
|
|
75
|
+
if (!root.contains(event.relatedTarget as Node)) {
|
|
76
|
+
this.validator.touched.value = true;
|
|
77
|
+
if (this.validator.fields[this.name].group) {
|
|
78
|
+
const group_name = this.validator.fields[this.name]?.group;
|
|
79
|
+
this.validator.groups[group_name].touched.value = true;
|
|
80
|
+
}
|
|
81
|
+
this.validator.fields[this.name].touched.value = true;
|
|
82
|
+
this.validator.fields[this.name].validate();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
form_field.setAttribute('has-validation', 'true');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
beforeUnmount() {
|
|
91
|
+
this.form_fields.forEach((form_field) => {
|
|
92
|
+
remove(this.validator.fields[this.name].controls, form_field);
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isEqual, isPlainObject } from 'lodash-es';
|
|
2
|
+
import { asyncForEach } from 'modern-async';
|
|
3
|
+
import { reactive, ref, watch, type Ref } from 'vue';
|
|
4
|
+
import { type Validator } from './ValidatorClass';
|
|
5
|
+
|
|
6
|
+
export interface FieldValidatorConfig {
|
|
7
|
+
value: any;
|
|
8
|
+
rules: ValidationRule[];
|
|
9
|
+
group?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type ValidationRule = (value: any, ...args: any) => Promise<string | null> | (string | null);
|
|
13
|
+
|
|
14
|
+
export class FieldValidator {
|
|
15
|
+
controls: HTMLElement[];
|
|
16
|
+
dirty: Ref<boolean, boolean>;
|
|
17
|
+
errors: string[];
|
|
18
|
+
group?: string;
|
|
19
|
+
rules: ValidationRule[];
|
|
20
|
+
touched: Ref<boolean, boolean>;
|
|
21
|
+
validated: Ref<boolean, boolean>;
|
|
22
|
+
validator: Validator;
|
|
23
|
+
value: Ref<any, any>;
|
|
24
|
+
|
|
25
|
+
constructor(config: FieldValidatorConfig, validator: Validator) {
|
|
26
|
+
this.controls = reactive([]);
|
|
27
|
+
this.dirty = ref(false);
|
|
28
|
+
this.errors = reactive([]);
|
|
29
|
+
this.rules = reactive(config.rules);
|
|
30
|
+
this.touched = ref(false);
|
|
31
|
+
this.validated = ref(false);
|
|
32
|
+
this.validator = validator;
|
|
33
|
+
this.value = ref(config.value);
|
|
34
|
+
|
|
35
|
+
if (config.group) {
|
|
36
|
+
this.group = config.group;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Watch the value
|
|
40
|
+
watch(
|
|
41
|
+
this.value,
|
|
42
|
+
async (new_value, old_value) => {
|
|
43
|
+
if (!isEqual(new_value, old_value)) {
|
|
44
|
+
this.dirty.value = true;
|
|
45
|
+
this.validator.dirty.value = true;
|
|
46
|
+
if (this.group) {
|
|
47
|
+
this.validator.groups[this.group].dirty.value = true;
|
|
48
|
+
}
|
|
49
|
+
if (this.touched) {
|
|
50
|
+
await this.validate();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
deep: isPlainObject(this.value),
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get isValid() {
|
|
61
|
+
return this.errors.length === 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
reset() {
|
|
65
|
+
this.touched.value = false;
|
|
66
|
+
this.dirty.value = false;
|
|
67
|
+
this.validated.value = false;
|
|
68
|
+
this.errors = [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async validate() {
|
|
72
|
+
this.errors = [];
|
|
73
|
+
|
|
74
|
+
// Only validate if the controls are not disabled or hidden
|
|
75
|
+
let should_validate = false;
|
|
76
|
+
if (
|
|
77
|
+
this.controls.length > 0 &&
|
|
78
|
+
this.controls?.every((control_element) => {
|
|
79
|
+
return !control_element.getAttribute('disabled');
|
|
80
|
+
})
|
|
81
|
+
) {
|
|
82
|
+
should_validate = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (should_validate) {
|
|
86
|
+
await asyncForEach(this.rules, async (rule: ValidationRule) => {
|
|
87
|
+
const error = await rule(this.value);
|
|
88
|
+
if (error !== null) {
|
|
89
|
+
this.errors.push(error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return this.isValid;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { reactive, ref, type Ref } from 'vue';
|
|
2
|
+
import { forIn } from 'lodash-es';
|
|
3
|
+
import { FieldValidator, type FieldValidatorConfig } from './FieldValidatorClass';
|
|
4
|
+
import { asyncForEach } from 'modern-async';
|
|
5
|
+
|
|
6
|
+
export interface ValidatorConfig {
|
|
7
|
+
fields?: Record<string, FieldValidatorConfig>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ValidationGroup {
|
|
11
|
+
dirty: Ref<boolean, boolean>;
|
|
12
|
+
fields: Record<string, FieldValidator>;
|
|
13
|
+
touched: Ref<boolean, boolean>;
|
|
14
|
+
validated: Ref<boolean, boolean>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Validator {
|
|
18
|
+
dirty: Ref<boolean, boolean>;
|
|
19
|
+
errors: string[];
|
|
20
|
+
fields: Record<string, FieldValidator>;
|
|
21
|
+
groups: Record<string, ValidationGroup>;
|
|
22
|
+
touched: Ref<boolean, boolean>;
|
|
23
|
+
validated: Ref<boolean, boolean>;
|
|
24
|
+
|
|
25
|
+
constructor(config: ValidatorConfig = {}) {
|
|
26
|
+
this.dirty = ref(false);
|
|
27
|
+
this.errors = reactive([]);
|
|
28
|
+
this.fields = reactive({});
|
|
29
|
+
this.groups = reactive({});
|
|
30
|
+
this.touched = ref(false);
|
|
31
|
+
this.validated = ref(false);
|
|
32
|
+
|
|
33
|
+
if (config.fields) {
|
|
34
|
+
forIn(config.fields, (field_config, field_name) => {
|
|
35
|
+
this.addField(field_name, field_config);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isValid(group_name?: string) {
|
|
41
|
+
let field_names = Object.keys(this.fields);
|
|
42
|
+
if (group_name) {
|
|
43
|
+
field_names = Object.keys(this.groups[group_name].fields);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return field_names.every((field) => {
|
|
47
|
+
return this.fields[field].errors.length === 0;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
reset(group_name?: string) {
|
|
52
|
+
this.validated.value = false;
|
|
53
|
+
this.errors = [];
|
|
54
|
+
|
|
55
|
+
if (group_name) {
|
|
56
|
+
forIn(this.groups[group_name].fields, (field) => {
|
|
57
|
+
field.reset();
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
forIn(this.fields, (field) => {
|
|
61
|
+
field.reset();
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
addField(field_name: string, field_config: FieldValidatorConfig) {
|
|
67
|
+
const new_field = new FieldValidator(field_config, this);
|
|
68
|
+
this.fields[field_name] = new_field;
|
|
69
|
+
|
|
70
|
+
if (field_config.group) {
|
|
71
|
+
if (!this.groups[field_config.group]) {
|
|
72
|
+
this.groups[field_config.group] = {
|
|
73
|
+
dirty: ref(false),
|
|
74
|
+
fields: reactive({}),
|
|
75
|
+
touched: ref(false),
|
|
76
|
+
validated: ref(false),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
this.groups[field_config.group].fields[field_name] = new_field;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
removeField(field_name: string) {
|
|
84
|
+
if (this.fields[field_name].group) {
|
|
85
|
+
delete this.groups[this.fields[field_name].group!].fields[field_name];
|
|
86
|
+
}
|
|
87
|
+
delete this.fields[field_name];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async validate(group_name?: string) {
|
|
91
|
+
if (group_name && this.groups[group_name]) {
|
|
92
|
+
const group = this.groups[group_name];
|
|
93
|
+
|
|
94
|
+
await asyncForEach(Object.keys(group.fields), async (field) => {
|
|
95
|
+
await group.fields[field].validate();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
this.groups[group_name].validated.value = true;
|
|
99
|
+
} else {
|
|
100
|
+
await asyncForEach(Object.keys(this.fields), async (field) => {
|
|
101
|
+
await this.fields[field].validate();
|
|
102
|
+
});
|
|
103
|
+
this.validated.value = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
package/lib/index.ts
ADDED
|
File without changes
|
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@splitty-test/validation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"engines": {
|
|
6
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"build": "run-p type-check \"build-only {@}\" --",
|
|
11
|
+
"preview": "vite preview",
|
|
12
|
+
"build-only": "vite build",
|
|
13
|
+
"type-check": "vue-tsc --build",
|
|
14
|
+
"lint": "eslint . --fix",
|
|
15
|
+
"format": "prettier --write src/"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"lodash-es": "^4.17.21",
|
|
19
|
+
"modern-async": "^2.0.4",
|
|
20
|
+
"vue": "^3.5.18"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@tsconfig/node22": "^22.0.2",
|
|
24
|
+
"@types/lodash-es": "^4.17.12",
|
|
25
|
+
"@types/node": "^22.16.5",
|
|
26
|
+
"@vitejs/plugin-vue": "^6.0.1",
|
|
27
|
+
"@vue/eslint-config-prettier": "^10.2.0",
|
|
28
|
+
"@vue/eslint-config-typescript": "^14.6.0",
|
|
29
|
+
"@vue/tsconfig": "^0.7.0",
|
|
30
|
+
"eslint": "^9.31.0",
|
|
31
|
+
"eslint-plugin-vue": "~10.3.0",
|
|
32
|
+
"jiti": "^2.4.2",
|
|
33
|
+
"npm-run-all2": "^8.0.4",
|
|
34
|
+
"prettier": "3.6.2",
|
|
35
|
+
"typescript": "~5.8.0",
|
|
36
|
+
"vite": "^7.0.6",
|
|
37
|
+
"vite-plugin-vue-devtools": "^8.0.0",
|
|
38
|
+
"vue-tsc": "^3.0.4"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
|
3
|
+
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
|
4
|
+
"exclude": ["src/**/__tests__/*"],
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
7
|
+
|
|
8
|
+
"paths": {
|
|
9
|
+
"@/*": ["./src/*"]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@tsconfig/node22/tsconfig.json",
|
|
3
|
+
"include": [
|
|
4
|
+
"vite.config.*",
|
|
5
|
+
"vitest.config.*",
|
|
6
|
+
"cypress.config.*",
|
|
7
|
+
"nightwatch.conf.*",
|
|
8
|
+
"playwright.config.*",
|
|
9
|
+
"eslint.config.*"
|
|
10
|
+
],
|
|
11
|
+
"compilerOptions": {
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
14
|
+
|
|
15
|
+
"module": "ESNext",
|
|
16
|
+
"moduleResolution": "Bundler",
|
|
17
|
+
"types": ["node"]
|
|
18
|
+
}
|
|
19
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fileURLToPath, URL } from 'node:url';
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import vue from '@vitejs/plugin-vue';
|
|
4
|
+
import vueDevTools from 'vite-plugin-vue-devtools';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
|
|
7
|
+
// https://vite.dev/config/
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
build: {
|
|
10
|
+
lib: {
|
|
11
|
+
entry: resolve(__dirname, './lib/index.ts'),
|
|
12
|
+
name: 'Validation',
|
|
13
|
+
fileName: 'index',
|
|
14
|
+
},
|
|
15
|
+
rollupOptions: {
|
|
16
|
+
external: ['vue'],
|
|
17
|
+
output: {
|
|
18
|
+
globals: {
|
|
19
|
+
vue: 'Vue',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
plugins: [vue(), vueDevTools()],
|
|
25
|
+
resolve: {
|
|
26
|
+
alias: {
|
|
27
|
+
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
});
|