@teamnovu/kit-vue-forms 0.1.14 → 0.1.16

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/src/utils/path.ts CHANGED
@@ -1,94 +1,129 @@
1
- import { computed, isRef, unref, type MaybeRef } from 'vue'
2
- import type { Paths, PickProps, SplitPath } from '../types/util'
3
- import type { ErrorBag, ValidationErrors } from '../types/validation'
1
+ import { computed, isRef, unref, type MaybeRef } from "vue";
2
+ import type { Paths, PickProps, SplitPath } from "../types/util";
3
+ import type { ErrorBag, ValidationErrors } from "../types/validation";
4
+ import type { FormField } from "../types/form";
4
5
 
5
6
  export function splitPath(path: string): string[] {
6
- if (path === '') {
7
- return []
7
+ if (path === "") {
8
+ return [];
8
9
  }
9
- return path.split(/\s*\.\s*/).filter(Boolean)
10
+ return path.split(/\s*\.\s*/).filter(Boolean);
10
11
  }
11
12
 
12
- export function getNestedValue<T, K extends Paths<T>>(obj: T, path: K | SplitPath<K>) {
13
- const splittedPath = Array.isArray(path) ? path : splitPath(path)
13
+ export function existsPath<T, K extends Paths<T>>(
14
+ obj: T,
15
+ path: K | SplitPath<K>,
16
+ ): boolean {
17
+ const splittedPath = Array.isArray(path) ? path : splitPath(path) as SplitPath<K>;
18
+ return !!getNestedValue(obj, splittedPath.slice(0, -1) as SplitPath<K>);
19
+ }
20
+
21
+ export function existsFieldPath<T, K extends Paths<T>>(field: FormField<T, K>) {
22
+ return existsPath(field.data.value, field.path.value)
23
+ }
24
+
25
+ export function getNestedValue<T, K extends Paths<T>>(
26
+ obj: T,
27
+ path: K | SplitPath<K>,
28
+ ) {
29
+ const splittedPath = Array.isArray(path) ? path : splitPath(path);
14
30
  return splittedPath.reduce(
15
31
  (current, key) => current?.[key],
16
32
  obj as Record<string, never>,
17
- ) as PickProps<T, K>
33
+ ) as PickProps<T, K>;
18
34
  }
19
35
 
20
- export function setNestedValue<T, K extends Paths<T>>(obj: MaybeRef<T>, path: K | SplitPath<K>, value: PickProps<T, K>): void {
21
- const keys = Array.isArray(path) ? path : splitPath(path)
36
+ export function setNestedValue<T, K extends Paths<T>>(
37
+ obj: MaybeRef<T>,
38
+ path: K | SplitPath<K>,
39
+ value: PickProps<T, K>,
40
+ ): void {
41
+ const keys = Array.isArray(path) ? path : splitPath(path);
22
42
 
23
- const lastKey = keys.at(-1)!
43
+ const lastKey = keys.at(-1)!;
24
44
 
25
45
  if (!lastKey) {
26
46
  if (!isRef(obj)) {
27
47
  // We cannot do anything here as we have nothing we can assign to
28
- return
48
+ return;
29
49
  }
30
50
 
31
- obj.value = value
51
+ obj.value = value;
32
52
  } else {
33
- const target = keys
34
- .slice(0, -1)
35
- .reduce(
36
- (current, key) => current?.[key],
37
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
- unref(obj) as Record<string, any>,
39
- )
40
-
41
- target[lastKey] = value
53
+ const target = keys.slice(0, -1).reduce(
54
+ (current, key) => {
55
+ if (current?.[key] === undefined) {
56
+ // Create the nested object if it doesn't exist
57
+ current[key] = {};
58
+ }
59
+ return current?.[key];
60
+ },
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
62
+ unref(obj) as Record<string, any>,
63
+ );
64
+
65
+ target[lastKey] = value;
42
66
  }
43
67
  }
44
68
 
45
- export const getLens = <T, K extends Paths<T>>(data: MaybeRef<T>, key: MaybeRef<K | SplitPath<K>>) => {
69
+ export const getLens = <T, K extends Paths<T>>(
70
+ data: MaybeRef<T>,
71
+ key: MaybeRef<K | SplitPath<K>>,
72
+ ) => {
46
73
  return computed({
47
74
  get() {
48
- return getNestedValue(unref(data), unref(key))
75
+ return getNestedValue(unref(data), unref(key));
49
76
  },
50
77
  set(value: PickProps<T, K>) {
51
- setNestedValue(data, unref(key), value)
78
+ setNestedValue(data, unref(key), value);
52
79
  },
53
- })
54
- }
80
+ });
81
+ };
55
82
 
56
- type JoinPath<Base extends string, Sub extends string> = `${Base}${Base extends '' ? '' : Sub extends '' ? '' : '.'}${Sub}`
57
- export function joinPath<Base extends string, Sub extends string>(basePath: Base, subPath: Sub): JoinPath<Base, Sub> {
83
+ type JoinPath<
84
+ Base extends string,
85
+ Sub extends string,
86
+ > = `${Base}${Base extends "" ? "" : Sub extends "" ? "" : "."}${Sub}`;
87
+ export function joinPath<Base extends string, Sub extends string>(
88
+ basePath: Base,
89
+ subPath: Sub,
90
+ ): JoinPath<Base, Sub> {
58
91
  if (!basePath && !subPath) {
59
- return '' as JoinPath<Base, Sub>
92
+ return "" as JoinPath<Base, Sub>;
60
93
  }
61
94
 
62
95
  if (!basePath && subPath) {
63
- return subPath as JoinPath<Base, Sub>
96
+ return subPath as JoinPath<Base, Sub>;
64
97
  }
65
98
 
66
99
  if (!subPath && basePath) {
67
- return basePath as JoinPath<Base, Sub>
100
+ return basePath as JoinPath<Base, Sub>;
68
101
  }
69
102
 
70
- return `${basePath}.${subPath}` as JoinPath<Base, Sub>
103
+ return `${basePath}.${subPath}` as JoinPath<Base, Sub>;
71
104
  }
72
105
 
73
106
  export function filterErrorsForPath(errors: ErrorBag, path: string): ErrorBag {
74
107
  // Handle empty path - return all errors
75
108
  if (!path) {
76
- return errors
109
+ return errors;
77
110
  }
78
111
 
79
- const pathPrefix = `${path}.`
80
- const filteredPropertyErrors: Record<string, ValidationErrors> = Object.fromEntries(
81
- Object.entries(errors.propertyErrors)
82
- .filter(([errorPath]) => {
83
- return errorPath.startsWith(pathPrefix)
84
- })
85
- .map(
86
- ([errorPath, errorMessages]) => [errorPath.slice(pathPrefix.length), errorMessages],
87
- ),
88
- )
112
+ const pathPrefix = `${path}.`;
113
+ const filteredPropertyErrors: Record<string, ValidationErrors> =
114
+ Object.fromEntries(
115
+ Object.entries(errors.propertyErrors)
116
+ .filter(([errorPath]) => {
117
+ return errorPath.startsWith(pathPrefix);
118
+ })
119
+ .map(([errorPath, errorMessages]) => [
120
+ errorPath.slice(pathPrefix.length),
121
+ errorMessages,
122
+ ]),
123
+ );
89
124
 
90
125
  return {
91
126
  general: errors.general, // Keep general errors
92
127
  propertyErrors: filteredPropertyErrors,
93
- }
128
+ };
94
129
  }
@@ -1031,6 +1031,23 @@ describe('Subform Implementation', () => {
1031
1031
  expect(result.isValid).toBe(false)
1032
1032
  expect(result.errors.general).toContain('Data is undefined')
1033
1033
  })
1034
+
1035
+ it('can create subforms on non-existent paths', async () => {
1036
+ const form = useForm<{
1037
+ test: string[]
1038
+ nonExistentPath?: { name: string }
1039
+ }>({
1040
+ initialData: { test: ['item1', 'item2'] },
1041
+ })
1042
+
1043
+ const subform = form.getSubForm('nonExistentPath')
1044
+
1045
+ const nameField = subform.defineField({ path: 'name' })
1046
+
1047
+ nameField.setData('Test Name')
1048
+
1049
+ expect(nameField.data.value).toEqual('Test Name')
1050
+ })
1034
1051
  })
1035
1052
 
1036
1053
  describe('Path Edge Cases', () => {
@@ -207,4 +207,21 @@ describe('useField', () => {
207
207
  expect(field.initialValue.value).toBe('bar')
208
208
  expect(field.data.value).toBe('modified')
209
209
  })
210
+
211
+ it('it should reset errors on a field on blur', () => {
212
+ const errors = ref(['Initial error'])
213
+
214
+ const field = useField({
215
+ initialValue: 'foo',
216
+ value: 'foo',
217
+ path: 'name',
218
+ errors,
219
+ })
220
+
221
+ expect(field.errors.value).toEqual(['Initial error'])
222
+
223
+ field.onBlur()
224
+
225
+ expect(field.errors.value).toEqual([])
226
+ })
210
227
  })