@goodie-forms/core 1.1.5-alpha → 1.2.0-alpha

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.
Files changed (40) hide show
  1. package/LICENSE +426 -426
  2. package/dist/field/FieldPath.d.ts +32 -0
  3. package/dist/field/FieldPath.d.ts.map +1 -0
  4. package/dist/field/FieldPathBuilder.d.ts +25 -0
  5. package/dist/field/FieldPathBuilder.d.ts.map +1 -0
  6. package/dist/field/Reconcile.d.ts +9 -0
  7. package/dist/field/Reconcile.d.ts.map +1 -0
  8. package/dist/form/FormController.d.ts +35 -39
  9. package/dist/form/FormController.d.ts.map +1 -1
  10. package/dist/form/FormField.d.ts +12 -10
  11. package/dist/form/FormField.d.ts.map +1 -1
  12. package/dist/form/NonullFormField.d.ts +3 -4
  13. package/dist/form/NonullFormField.d.ts.map +1 -1
  14. package/dist/index.d.ts +7 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +615 -881
  17. package/dist/index.js.map +1 -1
  18. package/dist/validation/CustomValidation.d.ts +10 -0
  19. package/dist/validation/CustomValidation.d.ts.map +1 -0
  20. package/package.json +1 -1
  21. package/src/field/FieldPath.ts +292 -0
  22. package/src/field/FieldPathBuilder.ts +132 -0
  23. package/src/field/Reconcile.ts +99 -0
  24. package/src/form/FormController.ts +342 -310
  25. package/src/form/FormField.ts +200 -202
  26. package/src/form/NonullFormField.ts +15 -21
  27. package/src/index.ts +12 -5
  28. package/src/types/DeepPartial.ts +7 -7
  29. package/src/types/Mixin.ts +2 -2
  30. package/src/utils/ensureImmerability.ts +30 -30
  31. package/src/utils/getId.ts +5 -5
  32. package/src/utils/removeBy.ts +11 -11
  33. package/src/{form → validation}/CustomValidation.ts +52 -52
  34. package/tsconfig.json +8 -7
  35. package/vite.config.ts +18 -18
  36. package/dist/form/CustomValidation.d.ts +0 -10
  37. package/dist/form/CustomValidation.d.ts.map +0 -1
  38. package/dist/form/Field.d.ts +0 -31
  39. package/dist/form/Field.d.ts.map +0 -1
  40. package/src/form/Field.ts +0 -334
@@ -1,202 +1,200 @@
1
- import { produce } from "immer";
2
- import { ensureImmerability } from "../utils/ensureImmerability";
3
- import { getId } from "../utils/getId";
4
- import { Field } from "./Field";
5
- import { FormController } from "./FormController";
6
-
7
- export class FormField<
8
- TShape extends object,
9
- TPath extends Field.Paths<TShape>
10
- > {
11
- public readonly id = getId();
12
-
13
- protected target?: HTMLElement;
14
-
15
- protected _isTouched = false;
16
- protected _isDirty = false;
17
-
18
- constructor(
19
- public readonly controller: FormController<TShape>,
20
- public readonly path: TPath,
21
- initialState?: {
22
- isTouched?: boolean;
23
- isDirty?: boolean;
24
- }
25
- ) {
26
- if (initialState?.isTouched) this._setTouched(true);
27
- if (initialState?.isDirty) this._setDirty(true);
28
- }
29
-
30
- get value(): Field.GetValue<TShape, TPath> | undefined {
31
- return Field.getValue<TShape, TPath>(
32
- this.controller._data as TShape,
33
- this.path
34
- );
35
- }
36
-
37
- get initialValue(): Field.GetValue<TShape, TPath> | undefined {
38
- return Field.getValue<TShape, TPath>(
39
- this.controller._initialData as TShape,
40
- this.path
41
- );
42
- }
43
-
44
- get boundElement() {
45
- return this.target;
46
- }
47
-
48
- get issues() {
49
- return this.controller._issues.filter(
50
- (issue) => Field.parsePath(issue.path ?? []) === this.path
51
- );
52
- }
53
-
54
- get isTouched() {
55
- return this._isTouched;
56
- }
57
-
58
- get isDirty() {
59
- return this._isDirty;
60
- }
61
-
62
- get isValid() {
63
- return this.issues.length === 0;
64
- }
65
-
66
- protected _setTouched(isTouched: boolean) {
67
- const changed = this._isTouched !== isTouched;
68
- this._isTouched = isTouched;
69
-
70
- if (changed) {
71
- const ascendantFields = this.controller.getAscendantFields(this.path);
72
- for (let i = ascendantFields.length - 1; i >= 0; i--) {
73
- const field = ascendantFields[i];
74
- if (field == null) continue;
75
- this.controller.events.emit("fieldTouchUpdated", field.path);
76
- }
77
- }
78
- }
79
-
80
- protected _setDirty(isDirty: boolean) {
81
- const changed = this._isDirty !== isDirty;
82
- this._isDirty = isDirty;
83
-
84
- if (changed) {
85
- const ascendantFields = this.controller.getAscendantFields(this.path);
86
- for (let i = ascendantFields.length - 1; i >= 0; i--) {
87
- const field = ascendantFields[i];
88
- if (field == null) continue;
89
- this.controller.events.emit("fieldDirtyUpdated", field.path);
90
- }
91
- }
92
- }
93
-
94
- bindElement(el: HTMLElement | undefined) {
95
- if (el != null) this.controller.events.emit("elementBound", this.path, el);
96
- else this.controller.events.emit("elementUnbound", this.path);
97
- this.target = el;
98
- }
99
-
100
- setValue(
101
- value: Field.GetValue<TShape, TPath>,
102
- opts?: Parameters<typeof this.modifyValue>[1]
103
- ) {
104
- return this.modifyValue(() => value, opts);
105
- }
106
-
107
- modifyValue(
108
- modifier: (
109
- currentValue: Field.GetValue<TShape, TPath> | undefined
110
- ) => Field.GetValue<TShape, TPath> | void,
111
- opts?: {
112
- shouldTouch?: boolean;
113
- shouldMarkDirty?: boolean;
114
- }
115
- ): void {
116
- if (opts?.shouldTouch == null || opts?.shouldTouch) {
117
- this.touch();
118
- }
119
-
120
- const ascendantFields = this.controller.getAscendantFields(this.path);
121
-
122
- const initialValues = ascendantFields.map((field) => field?.initialValue);
123
- initialValues.forEach((v) => ensureImmerability(v));
124
-
125
- const oldValues = ascendantFields.map((field) => field?.value);
126
- oldValues.forEach((v) => ensureImmerability(v));
127
-
128
- this.controller._data = produce(this.controller._data, (draft) => {
129
- Field.modifyValue(draft as TShape, this.path, (oldValue) => {
130
- return modifier(oldValue);
131
- });
132
- });
133
-
134
- const newValues = ascendantFields.map((field) => field?.value);
135
- newValues.forEach((v) => ensureImmerability(v));
136
-
137
- const compareCustom = (a: any, b: any) => {
138
- if (typeof a !== "object") return;
139
- if (typeof b !== "object") return;
140
- const ctorA = a.constructor;
141
- const ctorB = b.constructor;
142
- if (ctorA !== ctorB) return;
143
- return this.controller.equalityComparators?.[ctorA]?.(a, b);
144
- };
145
-
146
- const valueChanged = !Field.deepEqual(
147
- oldValues[oldValues.length - 1],
148
- newValues[newValues.length - 1],
149
- compareCustom
150
- );
151
-
152
- if (valueChanged) {
153
- for (let i = ascendantFields.length - 1; i >= 0; i--) {
154
- const field = ascendantFields[i];
155
- this.controller.events.emit(
156
- "valueChanged",
157
- field.path,
158
- newValues[i],
159
- oldValues[i]
160
- );
161
- }
162
- }
163
-
164
- if (opts?.shouldMarkDirty == null || opts?.shouldMarkDirty) {
165
- const gotDirty = !Field.deepEqual(
166
- initialValues[initialValues.length - 1],
167
- newValues[newValues.length - 1],
168
- compareCustom
169
- );
170
- this._setDirty(gotDirty);
171
- }
172
- }
173
-
174
- reset() {
175
- this._setTouched(false);
176
- this._setDirty(false);
177
- }
178
-
179
- touch() {
180
- this._setTouched(true);
181
- }
182
-
183
- markDirty() {
184
- this.touch();
185
- this._setDirty(true);
186
- }
187
-
188
- triggerValidation() {
189
- this.controller.validateField(this.path);
190
- }
191
-
192
- focus(opts?: { shouldTouch?: boolean }) {
193
- if (opts?.shouldTouch == null || opts.shouldTouch) {
194
- this.target?.addEventListener("focus", () => this.touch(), {
195
- once: true,
196
- });
197
- }
198
-
199
- this.target?.scrollIntoView();
200
- this.target?.focus();
201
- }
202
- }
1
+ import { produce } from "immer";
2
+ import { FieldPath } from "../field/FieldPath";
3
+ import { Reconsile } from "../field/Reconcile";
4
+ import { FormController } from "../form/FormController";
5
+ import { ensureImmerability } from "../utils/ensureImmerability";
6
+ import { getId } from "../utils/getId";
7
+
8
+ export class FormField<TOutput extends object, TValue> {
9
+ public readonly id = getId();
10
+
11
+ protected target?: HTMLElement;
12
+
13
+ protected _isTouched = false;
14
+ protected _isDirty = false;
15
+
16
+ constructor(
17
+ public readonly controller: FormController<TOutput>,
18
+ public readonly path: FieldPath.Segments,
19
+ initialState?: {
20
+ isTouched?: boolean;
21
+ isDirty?: boolean;
22
+ },
23
+ ) {
24
+ if (initialState?.isTouched) this._setTouched(true);
25
+ if (initialState?.isDirty) this._setDirty(true);
26
+ }
27
+
28
+ get canonicalPath() {
29
+ return FieldPath.toCanonicalPath(this.path);
30
+ }
31
+
32
+ get stringPath() {
33
+ return FieldPath.toStringPath(this.path);
34
+ }
35
+
36
+ get value(): TValue | undefined {
37
+ return FieldPath.getValue(this.controller._data, this.path);
38
+ }
39
+
40
+ get initialValue(): TValue | undefined {
41
+ return FieldPath.getValue(this.controller._initialData, this.path);
42
+ }
43
+
44
+ get boundElement() {
45
+ return this.target;
46
+ }
47
+
48
+ get issues() {
49
+ return this.controller._issues.filter((issue) =>
50
+ FieldPath.equals(FieldPath.normalize(issue.path), this.path),
51
+ );
52
+ }
53
+
54
+ get isTouched() {
55
+ return this._isTouched;
56
+ }
57
+
58
+ get isDirty() {
59
+ return this._isDirty;
60
+ }
61
+
62
+ get isValid() {
63
+ return this.issues.length === 0;
64
+ }
65
+
66
+ protected _setTouched(isTouched: boolean) {
67
+ const changed = this._isTouched !== isTouched;
68
+ this._isTouched = isTouched;
69
+
70
+ if (changed) {
71
+ const ascendantFields = this.controller.getAscendantFields(this.path);
72
+ for (let i = ascendantFields.length - 1; i >= 0; i--) {
73
+ const field = ascendantFields[i];
74
+ if (field == null) continue;
75
+ this.controller.events.emit("fieldTouchUpdated", field.path);
76
+ }
77
+ }
78
+ }
79
+
80
+ protected _setDirty(isDirty: boolean) {
81
+ const changed = this._isDirty !== isDirty;
82
+ this._isDirty = isDirty;
83
+
84
+ if (changed) {
85
+ const ascendantFields = this.controller.getAscendantFields(this.path);
86
+ for (let i = ascendantFields.length - 1; i >= 0; i--) {
87
+ const field = ascendantFields[i];
88
+ if (field == null) continue;
89
+ this.controller.events.emit("fieldDirtyUpdated", field.path);
90
+ }
91
+ }
92
+ }
93
+
94
+ bindElement(el: HTMLElement | undefined) {
95
+ this.target = el;
96
+ if (el != null) {
97
+ this.controller.events.emit("elementBound", this.path, el);
98
+ } else {
99
+ this.controller.events.emit("elementUnbound", this.path);
100
+ }
101
+ }
102
+
103
+ setValue(value: TValue, opts?: Parameters<typeof this.modifyValue>[1]) {
104
+ return this.modifyValue(() => value, opts);
105
+ }
106
+
107
+ modifyValue(
108
+ modifier: (currentValue: TValue | undefined) => TValue | void,
109
+ opts?: {
110
+ shouldTouch?: boolean;
111
+ shouldMarkDirty?: boolean;
112
+ },
113
+ ): void {
114
+ if (opts?.shouldTouch == null || opts?.shouldTouch) {
115
+ this.touch();
116
+ }
117
+
118
+ const ascendantFields = this.controller.getAscendantFields(this.path);
119
+
120
+ const initialValues = ascendantFields.map((field) => field?.initialValue);
121
+ initialValues.forEach((v) => ensureImmerability(v));
122
+
123
+ const oldValues = ascendantFields.map((field) => field?.value);
124
+ oldValues.forEach((v) => ensureImmerability(v));
125
+
126
+ this.controller._data = produce(this.controller._data, (draft) => {
127
+ FieldPath.modifyValue(draft as TOutput, this.path, (oldValue) => {
128
+ return modifier(oldValue);
129
+ });
130
+ });
131
+
132
+ const newValues = ascendantFields.map((field) => field?.value);
133
+ newValues.forEach((v) => ensureImmerability(v));
134
+
135
+ const compareCustom = (a: any, b: any) => {
136
+ if (typeof a !== "object") return;
137
+ if (typeof b !== "object") return;
138
+ const ctorA = a.constructor;
139
+ const ctorB = b.constructor;
140
+ if (ctorA !== ctorB) return;
141
+ return this.controller.equalityComparators?.[ctorA]?.(a, b);
142
+ };
143
+
144
+ const valueChanged = !Reconsile.deepEqual(
145
+ oldValues[oldValues.length - 1],
146
+ newValues[newValues.length - 1],
147
+ compareCustom,
148
+ );
149
+
150
+ if (valueChanged) {
151
+ for (let i = ascendantFields.length - 1; i >= 0; i--) {
152
+ const field = ascendantFields[i];
153
+ this.controller.events.emit(
154
+ "valueChanged",
155
+ field.path,
156
+ newValues[i],
157
+ oldValues[i],
158
+ );
159
+ }
160
+ }
161
+
162
+ if (opts?.shouldMarkDirty == null || opts?.shouldMarkDirty) {
163
+ const gotDirty = !Reconsile.deepEqual(
164
+ initialValues[initialValues.length - 1],
165
+ newValues[newValues.length - 1],
166
+ compareCustom,
167
+ );
168
+ this._setDirty(gotDirty);
169
+ }
170
+ }
171
+
172
+ reset() {
173
+ this._setTouched(false);
174
+ this._setDirty(false);
175
+ }
176
+
177
+ touch() {
178
+ this._setTouched(true);
179
+ }
180
+
181
+ markDirty() {
182
+ this.touch();
183
+ this._setDirty(true);
184
+ }
185
+
186
+ triggerValidation() {
187
+ this.controller.validateField(this.path);
188
+ }
189
+
190
+ focus(opts?: { shouldTouch?: boolean }) {
191
+ if (opts?.shouldTouch == null || opts.shouldTouch) {
192
+ this.target?.addEventListener("focus", () => this.touch(), {
193
+ once: true,
194
+ });
195
+ }
196
+
197
+ this.target?.scrollIntoView();
198
+ this.target?.focus();
199
+ }
200
+ }
@@ -1,21 +1,15 @@
1
- import { Field } from "../form/Field";
2
- import { FormField } from "../form/FormField";
3
- import { Mixin } from "../types/Mixin";
4
-
5
- export type NonnullFormField<
6
- TShape extends object,
7
- TPath extends Field.Paths<TShape>,
8
- > = Mixin<
9
- FormField<TShape, TPath>,
10
- {
11
- modifyValue: (
12
- modifier: (
13
- currentValue: Field.GetValue<TShape, TPath>,
14
- ) => Field.GetValue<TShape, TPath> | void,
15
- opts?: {
16
- shouldTouch?: boolean;
17
- shouldMarkDirty?: boolean;
18
- },
19
- ) => void;
20
- }
21
- >;
1
+ import { FormField } from "./FormField";
2
+ import { Mixin } from "../types/Mixin";
3
+
4
+ export type NonnullFormField<TOutput extends object, TValue> = Mixin<
5
+ FormField<TOutput, TValue>,
6
+ {
7
+ modifyValue: (
8
+ modifier: (currentValue: TValue) => TValue | void,
9
+ opts?: {
10
+ shouldTouch?: boolean;
11
+ shouldMarkDirty?: boolean;
12
+ },
13
+ ) => void;
14
+ }
15
+ >;
package/src/index.ts CHANGED
@@ -1,5 +1,12 @@
1
- export * from "./form/Field";
2
- export * from "./form/FormField";
3
- export * from "./form/NonullFormField";
4
- export * from "./form/FormController";
5
- export * from "./form/CustomValidation";
1
+ export * from "./field/FieldPath";
2
+ export * from "./field/FieldPathBuilder";
3
+ export * from "./field/Reconcile";
4
+
5
+ export * from "./form/FormController";
6
+ export * from "./form/FormField";
7
+ export * from "./form/NonullFormField";
8
+
9
+ export * from "./validation/CustomValidation";
10
+
11
+ export * from "./types/DeepPartial";
12
+ export * from "./types/Mixin";
@@ -1,7 +1,7 @@
1
- export type DeepPartial<T> = {
2
- [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
3
- } & {
4
- [K in keyof T as T[K] extends (...args: any[]) => any
5
- ? never
6
- : K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
7
- };
1
+ export type DeepPartial<T> = {
2
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
3
+ } & {
4
+ [K in keyof T as T[K] extends (...args: any[]) => any
5
+ ? never
6
+ : K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
7
+ };
@@ -1,2 +1,2 @@
1
- export type Mixin<T extends object, U extends Partial<T>> = Omit<T, keyof U> &
2
- U;
1
+ export type Mixin<T extends object, U extends Partial<T>> = Omit<T, keyof U> &
2
+ U;
@@ -1,30 +1,30 @@
1
- import { immerable } from "immer";
2
-
3
- export function ensureImmerability(value: any) {
4
- if (typeof value !== "object" || value === null) return;
5
-
6
- // Skip plain objects
7
- const proto = Object.getPrototypeOf(value);
8
- if (proto === Object.prototype || proto === null) return;
9
-
10
- const ctor = proto.constructor;
11
- if (typeof ctor !== "function") return;
12
-
13
- // Skip known built-ins
14
- if (
15
- value instanceof Date ||
16
- value instanceof RegExp ||
17
- value instanceof Map ||
18
- value instanceof Set ||
19
- value instanceof WeakMap ||
20
- value instanceof WeakSet ||
21
- ArrayBuffer.isView(value)
22
- ) {
23
- return;
24
- }
25
-
26
- if (ctor[immerable] === true) return;
27
-
28
- // Define non-enumerable immerable flag
29
- ctor[immerable] = true;
30
- }
1
+ import { immerable } from "immer";
2
+
3
+ export function ensureImmerability(value: any) {
4
+ if (typeof value !== "object" || value === null) return;
5
+
6
+ // Skip plain objects
7
+ const proto = Object.getPrototypeOf(value);
8
+ if (proto === Object.prototype || proto === null) return;
9
+
10
+ const ctor = proto.constructor;
11
+ if (typeof ctor !== "function") return;
12
+
13
+ // Skip known built-ins
14
+ if (
15
+ value instanceof Date ||
16
+ value instanceof RegExp ||
17
+ value instanceof Map ||
18
+ value instanceof Set ||
19
+ value instanceof WeakMap ||
20
+ value instanceof WeakSet ||
21
+ ArrayBuffer.isView(value)
22
+ ) {
23
+ return;
24
+ }
25
+
26
+ if (ctor[immerable] === true) return;
27
+
28
+ // Define non-enumerable immerable flag
29
+ ctor[immerable] = true;
30
+ }
@@ -1,5 +1,5 @@
1
- let id = 0;
2
-
3
- export function getId() {
4
- return id++;
5
- }
1
+ let id = 0;
2
+
3
+ export function getId() {
4
+ return id++;
5
+ }
@@ -1,11 +1,11 @@
1
- export function removeBy<T>(arr: T[], predicate: (item: T) => boolean) {
2
- let indices: number[] = [];
3
-
4
- for (let i = arr.length - 1; i >= 0; i--) {
5
- if (predicate(arr[i])) {
6
- indices.push(i);
7
- }
8
- }
9
-
10
- indices.forEach((i) => arr.splice(i, 1));
11
- }
1
+ export function removeBy<T>(arr: T[], predicate: (item: T) => boolean) {
2
+ let indices: number[] = [];
3
+
4
+ for (let i = arr.length - 1; i >= 0; i--) {
5
+ if (predicate(arr[i])) {
6
+ indices.push(i);
7
+ }
8
+ }
9
+
10
+ indices.forEach((i) => arr.splice(i, 1));
11
+ }