@goodie-forms/core 1.2.5-alpha → 1.3.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.
Files changed (44) hide show
  1. package/README.md +61 -0
  2. package/dist/field/FieldPath.d.ts +18 -9
  3. package/dist/field/FieldPath.d.ts.map +1 -1
  4. package/dist/field/FieldPathBuilder.d.ts +2 -2
  5. package/dist/field/FieldPathBuilder.d.ts.map +1 -1
  6. package/dist/field/Reconcile.d.ts +5 -2
  7. package/dist/field/Reconcile.d.ts.map +1 -1
  8. package/dist/form/FormController.d.ts +51 -20
  9. package/dist/form/FormController.d.ts.map +1 -1
  10. package/dist/form/FormField.d.ts +15 -10
  11. package/dist/form/FormField.d.ts.map +1 -1
  12. package/dist/index.d.ts +2 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +550 -505
  15. package/dist/index.js.map +1 -1
  16. package/dist/types/DeepHelpers.d.ts +11 -0
  17. package/dist/types/DeepHelpers.d.ts.map +1 -0
  18. package/dist/types/FormHelpers.d.ts +5 -0
  19. package/dist/types/FormHelpers.d.ts.map +1 -0
  20. package/dist/types/Suppliable.d.ts +3 -0
  21. package/dist/types/Suppliable.d.ts.map +1 -0
  22. package/dist/validation/CustomValidation.d.ts +1 -1
  23. package/package.json +23 -3
  24. package/src/field/FieldPath.spec.ts +204 -0
  25. package/src/field/FieldPath.ts +63 -59
  26. package/src/field/FieldPathBuilder.spec.ts +47 -0
  27. package/src/field/FieldPathBuilder.ts +15 -81
  28. package/src/field/Reconcile.ts +12 -7
  29. package/src/form/FormController.spec.ts +55 -0
  30. package/src/form/FormController.ts +152 -121
  31. package/src/form/FormField.ts +66 -30
  32. package/src/index.ts +2 -2
  33. package/src/types/DeepHelpers.ts +15 -0
  34. package/src/types/FormHelpers.ts +13 -0
  35. package/src/types/Suppliable.ts +7 -0
  36. package/src/validation/CustomValidation.ts +1 -1
  37. package/dist/form/NonullFormField.d.ts +0 -9
  38. package/dist/form/NonullFormField.d.ts.map +0 -1
  39. package/dist/types/DeepPartial.d.ts +0 -6
  40. package/dist/types/DeepPartial.d.ts.map +0 -1
  41. package/src/form/NonullFormField.ts +0 -15
  42. package/src/types/DeepPartial.ts +0 -7
  43. package/tsconfig.json +0 -8
  44. package/vite.config.ts +0 -18
@@ -1,7 +1,9 @@
1
- import { produce } from "immer";
1
+ import { Draft, produce } from "immer";
2
2
  import { FieldPath } from "../field/FieldPath";
3
- import { Reconsile } from "../field/Reconcile";
3
+ import { Reconcile } from "../field/Reconcile";
4
4
  import { FormController } from "../form/FormController";
5
+ import { DeepReadonly } from "../types/DeepHelpers";
6
+ import { Suppliable, supply } from "../types/Suppliable";
5
7
  import { ensureImmerability } from "../utils/ensureImmerability";
6
8
  import { getId } from "../utils/getId";
7
9
 
@@ -13,9 +15,11 @@ export class FormField<TOutput extends object, TValue> {
13
15
  protected _isTouched = false;
14
16
  protected _isDirty = false;
15
17
 
18
+ /** @internal register via `FormController::registerField` instead */
16
19
  constructor(
17
20
  public readonly controller: FormController<TOutput>,
18
21
  public readonly path: FieldPath.Segments,
22
+ public readonly defaultValue?: Suppliable<TValue>,
19
23
  initialState?: {
20
24
  isTouched?: boolean;
21
25
  isDirty?: boolean;
@@ -25,20 +29,16 @@ export class FormField<TOutput extends object, TValue> {
25
29
  if (initialState?.isDirty) this._setDirty(true);
26
30
  }
27
31
 
28
- get canonicalPath() {
29
- return FieldPath.toCanonicalPath(this.path);
30
- }
31
-
32
32
  get stringPath() {
33
33
  return FieldPath.toStringPath(this.path);
34
34
  }
35
35
 
36
- get value(): TValue | undefined {
37
- return FieldPath.getValue(this.controller._data, this.path);
36
+ get value(): DeepReadonly<TValue> | undefined {
37
+ return FieldPath.getValue(this.controller.data, this.path);
38
38
  }
39
39
 
40
- get initialValue(): TValue | undefined {
41
- return FieldPath.getValue(this.controller._initialData, this.path);
40
+ get initialValue(): DeepReadonly<TValue> | undefined {
41
+ return FieldPath.getValue(this.controller.initialData, this.path);
42
42
  }
43
43
 
44
44
  get boundElement() {
@@ -63,6 +63,16 @@ export class FormField<TOutput extends object, TValue> {
63
63
  return this.issues.length === 0;
64
64
  }
65
65
 
66
+ ensureDefault(opts?: Parameters<typeof this.modifyData>[1]) {
67
+ if (this.value == null && this.defaultValue != null) {
68
+ const defaultValue = supply(this.defaultValue);
69
+
70
+ if (defaultValue != null) {
71
+ this.setValue(defaultValue, opts);
72
+ }
73
+ }
74
+ }
75
+
66
76
  protected _setTouched(isTouched: boolean) {
67
77
  const changed = this._isTouched !== isTouched;
68
78
  this._isTouched = isTouched;
@@ -92,26 +102,33 @@ export class FormField<TOutput extends object, TValue> {
92
102
  }
93
103
 
94
104
  bindElement(el: HTMLElement | undefined) {
95
- this.target = el;
96
105
  if (el != null) {
106
+ this.target = el;
97
107
  this.controller.events.emit("elementBound", this.path, el);
98
108
  } else {
109
+ this.unbindElement();
110
+ }
111
+ }
112
+
113
+ unbindElement() {
114
+ if (this.target != null) {
115
+ this.target = undefined;
99
116
  this.controller.events.emit("elementUnbound", this.path);
100
117
  }
101
118
  }
102
119
 
103
- setValue(value: TValue, opts?: Parameters<typeof this.modifyValue>[1]) {
104
- return this.modifyValue(() => value, opts);
120
+ clearIssues() {
121
+ return this.controller.clearFieldIssues(this.path);
105
122
  }
106
123
 
107
- modifyValue(
108
- modifier: (currentValue: TValue | undefined) => TValue | void,
124
+ private modifyData(
125
+ draftConsumer: (draft: Draft<typeof this.controller.data>) => void,
109
126
  opts?: {
110
127
  shouldTouch?: boolean;
111
128
  shouldMarkDirty?: boolean;
112
129
  },
113
- ): void {
114
- if (opts?.shouldTouch == null || opts?.shouldTouch) {
130
+ ) {
131
+ if (opts?.shouldTouch == null || opts?.shouldTouch === true) {
115
132
  this.touch();
116
133
  }
117
134
 
@@ -123,11 +140,7 @@ export class FormField<TOutput extends object, TValue> {
123
140
  const oldValues = ascendantFields.map((field) => field?.value);
124
141
  oldValues.forEach((v) => ensureImmerability(v));
125
142
 
126
- this.controller._data = produce(this.controller._data, (draft) => {
127
- FieldPath.modifyValue(draft as TOutput, this.path, (oldValue) => {
128
- return modifier(oldValue);
129
- });
130
- });
143
+ this.controller._data = produce(this.controller._data, draftConsumer);
131
144
 
132
145
  const newValues = ascendantFields.map((field) => field?.value);
133
146
  newValues.forEach((v) => ensureImmerability(v));
@@ -138,10 +151,10 @@ export class FormField<TOutput extends object, TValue> {
138
151
  const ctorA = a.constructor;
139
152
  const ctorB = b.constructor;
140
153
  if (ctorA !== ctorB) return;
141
- return this.controller.equalityComparators?.[ctorA]?.(a, b);
154
+ return this.controller.equalityComparators?.get(ctorA)?.(a, b);
142
155
  };
143
156
 
144
- const valueChanged = !Reconsile.deepEqual(
157
+ const valueChanged = !Reconcile.deepEqual(
145
158
  oldValues[oldValues.length - 1],
146
159
  newValues[newValues.length - 1],
147
160
  compareCustom,
@@ -151,7 +164,7 @@ export class FormField<TOutput extends object, TValue> {
151
164
  for (let i = ascendantFields.length - 1; i >= 0; i--) {
152
165
  const field = ascendantFields[i];
153
166
  this.controller.events.emit(
154
- "valueChanged",
167
+ "fieldValueChanged",
155
168
  field.path,
156
169
  newValues[i],
157
170
  oldValues[i],
@@ -160,7 +173,7 @@ export class FormField<TOutput extends object, TValue> {
160
173
  }
161
174
 
162
175
  if (opts?.shouldMarkDirty == null || opts?.shouldMarkDirty) {
163
- const gotDirty = !Reconsile.deepEqual(
176
+ const gotDirty = !Reconcile.deepEqual(
164
177
  initialValues[initialValues.length - 1],
165
178
  newValues[newValues.length - 1],
166
179
  compareCustom,
@@ -169,9 +182,30 @@ export class FormField<TOutput extends object, TValue> {
169
182
  }
170
183
  }
171
184
 
185
+ // TODO: impl
186
+ // private modifyInitialData() {}
187
+
188
+ setValue(value: TValue, opts?: Parameters<typeof this.modifyData>[1]) {
189
+ return this.modifyData((data) => {
190
+ FieldPath.setValue(data as TOutput, this.path, value as never);
191
+ }, opts);
192
+ }
193
+
194
+ modifyValue(
195
+ modifier: (currentValue: TValue) => undefined,
196
+ opts?: Parameters<typeof this.modifyData>[1],
197
+ ): void {
198
+ return this.modifyData((data) => {
199
+ FieldPath.modifyValue(data as TOutput, this.path, (oldValue) => {
200
+ modifier(oldValue as TValue);
201
+ });
202
+ }, opts);
203
+ }
204
+
172
205
  reset() {
173
206
  this._setTouched(false);
174
207
  this._setDirty(false);
208
+ this.controller.events.emit("fieldReset", this.path);
175
209
  }
176
210
 
177
211
  touch() {
@@ -179,22 +213,24 @@ export class FormField<TOutput extends object, TValue> {
179
213
  }
180
214
 
181
215
  markDirty() {
182
- this.touch();
183
216
  this._setDirty(true);
184
217
  }
185
218
 
186
- triggerValidation() {
219
+ validate() {
187
220
  this.controller.validateField(this.path);
188
221
  }
189
222
 
190
- focus(opts?: { shouldTouch?: boolean }) {
223
+ focus(opts?: {
224
+ shouldTouch?: boolean;
225
+ scrollOptions?: ScrollIntoViewOptions;
226
+ }) {
191
227
  if (opts?.shouldTouch == null || opts.shouldTouch) {
192
228
  this.target?.addEventListener("focus", () => this.touch(), {
193
229
  once: true,
194
230
  });
195
231
  }
196
232
 
197
- this.target?.scrollIntoView();
233
+ this.target?.scrollIntoView(opts?.scrollOptions);
198
234
  this.target?.focus();
199
235
  }
200
236
  }
package/src/index.ts CHANGED
@@ -4,9 +4,9 @@ export * from "./field/Reconcile";
4
4
 
5
5
  export * from "./form/FormController";
6
6
  export * from "./form/FormField";
7
- export * from "./form/NonullFormField";
8
7
 
9
8
  export * from "./validation/CustomValidation";
10
9
 
11
- export * from "./types/DeepPartial";
10
+ export * from "./types/DeepHelpers";
11
+ export * from "./types/Suppliable";
12
12
  export * from "./types/Mixin";
@@ -0,0 +1,15 @@
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
+ };
8
+
9
+ export type DeepReadonly<T> = {
10
+ [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K];
11
+ } & {
12
+ readonly [K in keyof T as T[K] extends (...args: any[]) => any
13
+ ? never
14
+ : K]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
15
+ };
@@ -0,0 +1,13 @@
1
+ import { FormField } from "packages/core/src/form/FormField";
2
+
3
+ // TODO: Consider
4
+ export type FieldValue<T extends FormField<any, any>> =
5
+ NonNullable<T> extends { value: infer V } ? NonNullable<V> : never;
6
+
7
+ // TODO: Move to a proper test file
8
+ // declare const x: FormField<{ a: 1; b: 2 }, number>;
9
+
10
+ // type X0 = FieldValue<typeof x>;
11
+ // // ^?
12
+ // type X1 = (typeof x)["value"];
13
+ // // ^?
@@ -0,0 +1,7 @@
1
+ export type Suppliable<T> = T | (() => T);
2
+
3
+ export function supply<T>(suppliable: T | (() => T)): T {
4
+ return typeof suppliable === "function"
5
+ ? (suppliable as () => T)()
6
+ : suppliable;
7
+ }
@@ -1,5 +1,5 @@
1
1
  import { StandardSchemaV1 } from "@standard-schema/spec";
2
- import { DeepPartial } from "../types/DeepPartial";
2
+ import { DeepPartial } from "../types/DeepHelpers";
3
3
  import { FieldPath } from "../field/FieldPath";
4
4
 
5
5
  export type CustomValidationIssue<TOutput extends object> = {
@@ -1,9 +0,0 @@
1
- import { FormField } from './FormField';
2
- import { Mixin } from '../types/Mixin';
3
- export type NonnullFormField<TOutput extends object, TValue> = Mixin<FormField<TOutput, TValue>, {
4
- modifyValue: (modifier: (currentValue: TValue) => TValue | void, opts?: {
5
- shouldTouch?: boolean;
6
- shouldMarkDirty?: boolean;
7
- }) => void;
8
- }>;
9
- //# sourceMappingURL=NonullFormField.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"NonullFormField.d.ts","sourceRoot":"","sources":["../../src/form/NonullFormField.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAEvC,MAAM,MAAM,gBAAgB,CAAC,OAAO,SAAS,MAAM,EAAE,MAAM,IAAI,KAAK,CAClE,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,EAC1B;IACE,WAAW,EAAE,CACX,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,EACjD,IAAI,CAAC,EAAE;QACL,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,eAAe,CAAC,EAAE,OAAO,CAAC;KAC3B,KACE,IAAI,CAAC;CACX,CACF,CAAC"}
@@ -1,6 +0,0 @@
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 ? never : K]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
5
- };
6
- //# sourceMappingURL=DeepPartial.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"DeepPartial.d.ts","sourceRoot":"","sources":["../../src/types/DeepPartial.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;KAC1B,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;CACzE,GAAG;KACD,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,GAAG,GACjD,KAAK,GACL,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CACxD,CAAC"}
@@ -1,15 +0,0 @@
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
- >;
@@ -1,7 +0,0 @@
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
- };
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "."
6
- },
7
- "include": ["src"]
8
- }
package/vite.config.ts DELETED
@@ -1,18 +0,0 @@
1
- import { defineConfig } from "vite";
2
-
3
- import dts from "vite-plugin-dts";
4
-
5
- export default defineConfig({
6
- plugins: [dts({ entryRoot: "src" })],
7
- build: {
8
- lib: {
9
- entry: "src/index.ts",
10
- formats: ["es"],
11
- fileName: "index",
12
- },
13
- rollupOptions: {
14
- external: [],
15
- },
16
- sourcemap: true,
17
- },
18
- });