@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 ADDED
@@ -0,0 +1,8 @@
1
+ [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
2
+ charset = utf-8
3
+ indent_size = 4
4
+ indent_style = tab
5
+ insert_final_newline = true
6
+ trim_trailing_whitespace = true
7
+ end_of_line = lf
8
+ max_line_length = 100
package/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/prettierrc",
3
+ "semi": true,
4
+ "singleQuote": true,
5
+ "printWidth": 100
6
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "recommendations": [
3
+ "Vue.volar",
4
+ "dbaeumer.vscode-eslint",
5
+ "EditorConfig.EditorConfig",
6
+ "esbenp.prettier-vscode"
7
+ ]
8
+ }
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" />
@@ -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
@@ -0,0 +1,6 @@
1
+ import { Validator } from './ValidatorClass';
2
+ import { FieldValidator } from './FieldValidatorClass';
3
+ import FieldValidation from './FieldValidation.vue';
4
+ import * as rules from './rules';
5
+
6
+ export { Validator, FieldValidator, FieldValidation, rules };
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,11 @@
1
+ {
2
+ "files": [],
3
+ "references": [
4
+ {
5
+ "path": "./tsconfig.node.json"
6
+ },
7
+ {
8
+ "path": "./tsconfig.app.json"
9
+ }
10
+ ]
11
+ }
@@ -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
+ });