@goodie-forms/react 1.1.6-alpha → 1.2.1-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.
@@ -1,120 +1,166 @@
1
- import { Field, FormField, NonnullFormField } from "@goodie-forms/core";
2
- import {
3
- ChangeEvent,
4
- FocusEvent,
5
- ReactNode,
6
- Ref,
7
- useEffect,
8
- useRef,
9
- } from "react";
10
- import { UseForm } from "../hooks/useForm";
11
- import { useFormField } from "../hooks/useFormField";
12
- import { composeFns } from "../utils/composeFns";
13
-
14
- export interface RenderParams<
15
- TShape extends object,
16
- TPath extends Field.Paths<TShape>
17
- > {
18
- ref: Ref<any | null>;
19
-
20
- value: Field.GetValue<TShape, TPath> | undefined;
21
-
22
- handlers: {
23
- onChange: (event: ChangeEvent<EventTarget>) => void;
24
- onFocus: (event: FocusEvent) => void;
25
- onBlur: (event: FocusEvent) => void;
26
- };
27
-
28
- field: undefined extends Field.GetValue<TShape, TPath>
29
- ? FormField<TShape, TPath>
30
- : NonnullFormField<TShape, TPath>;
31
- }
32
-
33
- type DefaultValueProps<TValue> = undefined extends TValue
34
- ? { defaultValue?: TValue | (() => TValue) }
35
- : { defaultValue: TValue | (() => TValue) };
36
-
37
- export type FieldRendererProps<
38
- TShape extends object,
39
- TPath extends Field.Paths<TShape>
40
- > = {
41
- form: UseForm<TShape>;
42
- path: TPath;
43
- overrideInitialValue?: boolean;
44
- unbindOnUnmount?: boolean;
45
- render: (params: RenderParams<TShape, TPath>) => ReactNode;
46
- } & DefaultValueProps<Field.GetValue<TShape, TPath>>;
47
-
48
- export function FieldRenderer<
49
- TShape extends object,
50
- TPath extends Field.Paths<TShape>
51
- >(props: FieldRendererProps<TShape, TPath>) {
52
- const elementRef = useRef<HTMLElement>(null);
53
-
54
- const field = useFormField(props.form, props.path, {
55
- overrideInitialValue: props.overrideInitialValue ?? true,
56
- defaultValue:
57
- typeof props.defaultValue === "function"
58
- ? (props.defaultValue as any)()
59
- : props.defaultValue,
60
- })!;
61
-
62
- const handlers: RenderParams<TShape, TPath>["handlers"] = {
63
- onChange(event) {
64
- const { target } = event;
65
- if (target !== field.boundElement) return;
66
- if (!("value" in target)) return;
67
- if (typeof target.value !== "string") return;
68
- field.setValue(target.value as Field.GetValue<TShape, TPath>, {
69
- shouldTouch: true,
70
- shouldMarkDirty: true,
71
- });
72
- },
73
- onFocus() {
74
- field.touch();
75
- },
76
- onBlur() {
77
- if (
78
- props.form.hookConfigs?.validateMode === "onBlur" ||
79
- props.form.hookConfigs?.validateMode === "onChange"
80
- ) {
81
- props.form.controller.validateField(props.path);
82
- }
83
- },
84
- };
85
-
86
- useEffect(() => {
87
- const { events } = props.form.controller;
88
-
89
- return composeFns(
90
- events.on("valueChanged", (path) => {
91
- if (path !== props.path && !Field.isDescendant(path, props.path))
92
- return;
93
- if (props.form.hookConfigs?.validateMode === "onChange") {
94
- props.form.controller.validateField(props.path);
95
- }
96
- })
97
- );
98
- }, []);
99
-
100
- useEffect(() => {
101
- field.bindElement(elementRef.current!);
102
-
103
- return () => {
104
- if (props.unbindOnUnmount) {
105
- props.form.controller.unbindField(props.path);
106
- }
107
- };
108
- }, []);
109
-
110
- return (
111
- <>
112
- {props.render({
113
- ref: elementRef,
114
- value: field.value,
115
- handlers: handlers,
116
- field: field as any,
117
- })}
118
- </>
119
- );
120
- }
1
+ import { FieldPath, FormField, NonnullFormField } from "@goodie-forms/core";
2
+ import {
3
+ ChangeEvent,
4
+ FocusEvent,
5
+ ReactNode,
6
+ Ref,
7
+ useEffect,
8
+ useRef,
9
+ } from "react";
10
+ import { useForm, UseForm } from "../hooks/useForm";
11
+ import { useFormField } from "../hooks/useFormField";
12
+ import { composeFns } from "../utils/composeFns";
13
+
14
+ export interface RenderParams<TOutput extends object, TValue> {
15
+ ref: Ref<any | null>;
16
+
17
+ value: TValue | undefined;
18
+
19
+ handlers: {
20
+ onChange: (event: ChangeEvent<EventTarget>) => void;
21
+ onFocus: (event: FocusEvent) => void;
22
+ onBlur: (event: FocusEvent) => void;
23
+ };
24
+
25
+ field: undefined extends TValue
26
+ ? FormField<TOutput, TValue>
27
+ : NonnullFormField<TOutput, TValue>;
28
+ }
29
+
30
+ type DefaultValueProps<TValue> = undefined extends TValue
31
+ ? { defaultValue?: TValue | (() => TValue) }
32
+ : { defaultValue: TValue | (() => TValue) };
33
+
34
+ export type FieldRendererProps<
35
+ TOutput extends object,
36
+ TPath extends FieldPath.Segments,
37
+ > = {
38
+ form: UseForm<TOutput>;
39
+ path: TPath;
40
+ overrideInitialValue?: boolean;
41
+ unbindOnUnmount?: boolean;
42
+ render: (
43
+ params: RenderParams<TOutput, FieldPath.Resolve<TOutput, NoInfer<TPath>>>,
44
+ ) => ReactNode;
45
+ } & DefaultValueProps<FieldPath.Resolve<TOutput, NoInfer<TPath>>>;
46
+
47
+ export function FieldRenderer<
48
+ TOutput extends object,
49
+ const TPath extends FieldPath.Segments,
50
+ >(props: FieldRendererProps<TOutput, TPath>) {
51
+ type TValue = FieldPath.Resolve<TOutput, TPath>;
52
+
53
+ const elementRef = useRef<HTMLElement>(null);
54
+
55
+ const field = useFormField(props.form, props.path, {
56
+ overrideInitialValue: props.overrideInitialValue ?? true,
57
+ defaultValue:
58
+ typeof props.defaultValue === "function"
59
+ ? (props.defaultValue as any)()
60
+ : props.defaultValue,
61
+ })!;
62
+
63
+ const handlers: RenderParams<TOutput, TValue>["handlers"] = {
64
+ onChange(event) {
65
+ const { target } = event;
66
+ if (target !== field.boundElement) return;
67
+ if (!("value" in target)) return;
68
+ if (typeof target.value !== "string") return;
69
+ field.setValue(target.value as TValue, {
70
+ shouldTouch: true,
71
+ shouldMarkDirty: true,
72
+ });
73
+ },
74
+ onFocus() {
75
+ field.touch();
76
+ },
77
+ onBlur() {
78
+ if (
79
+ props.form.hookConfigs?.validateMode === "onBlur" ||
80
+ props.form.hookConfigs?.validateMode === "onChange"
81
+ ) {
82
+ props.form.controller.validateField(props.path);
83
+ }
84
+ },
85
+ };
86
+
87
+ useEffect(() => {
88
+ const { events } = props.form.controller;
89
+
90
+ return composeFns(
91
+ events.on("valueChanged", (_path) => {
92
+ if (
93
+ !FieldPath.equals(_path, props.path) &&
94
+ !FieldPath.isDescendant(_path, props.path)
95
+ ) {
96
+ return;
97
+ }
98
+
99
+ if (props.form.hookConfigs?.validateMode === "onChange") {
100
+ props.form.controller.validateField(props.path);
101
+ }
102
+ }),
103
+ );
104
+ }, []);
105
+
106
+ useEffect(() => {
107
+ field.bindElement(elementRef.current!);
108
+
109
+ return () => {
110
+ if (props.unbindOnUnmount) {
111
+ props.form.controller.unbindField(props.path);
112
+ }
113
+ };
114
+ }, []);
115
+
116
+ return (
117
+ <>
118
+ {props.render({
119
+ ref: elementRef,
120
+ value: field.value,
121
+ handlers: handlers,
122
+ field: field as any,
123
+ })}
124
+ </>
125
+ );
126
+ }
127
+
128
+ /* ---- TESTS ---------------- */
129
+
130
+ // function TestComp() {
131
+ // const form = useForm<{ a?: { b: 99 } }>({});
132
+
133
+ // const jsx = (
134
+ // <>
135
+ // <FieldRenderer
136
+ // form={form}
137
+ // path={form.paths.fromProxy((data) => data.a.b)}
138
+ // defaultValue={() => 99 as const}
139
+ // render={({ ref, value, handlers, field }) => {
140
+ // // ^?
141
+ // return <></>;
142
+ // }}
143
+ // />
144
+
145
+ // {/* defaultField olmayabilir, çünkü "a" nullable */}
146
+ // <FieldRenderer
147
+ // form={form}
148
+ // path={form.paths.fromProxy((data) => data.a)}
149
+ // render={({ ref, value, handlers, field }) => {
150
+ // // ^?
151
+ // return <></>;
152
+ // }}
153
+ // />
154
+
155
+ // <FieldRenderer
156
+ // form={form}
157
+ // path={form.paths.fromStringPath("a.b")}
158
+ // defaultValue={() => 99 as const}
159
+ // render={({ ref, value, handlers, field }) => {
160
+ // // ^?
161
+ // return <></>;
162
+ // }}
163
+ // />
164
+ // </>
165
+ // );
166
+ // }
@@ -1,55 +1,50 @@
1
- import { FormController, type Form } from "@goodie-forms/core";
2
- import { useEffect, useState } from "react";
3
- import { composeFns } from "../utils/composeFns";
4
- import { useRenderControl } from "../hooks/useRenderControl";
5
-
6
- export function useForm<TShape extends object>(
7
- formConfigs: Form.FormConfigs<TShape>,
8
- hookConfigs?: {
9
- validateMode?: "onChange" | "onBlur" | "onSubmit";
10
- revalidateMode?: "onChange" | "onBlur" | "onSubmit";
11
- watchIssues?: boolean;
12
- watchValues?: boolean;
13
- }
14
- ) {
15
- const [controller] = useState(() => new FormController(formConfigs));
16
-
17
- const renderControl = useRenderControl();
18
-
19
- useEffect(() => {
20
- const noop = () => {};
21
-
22
- return composeFns(
23
- controller.events.on("submissionStatusChange", () => {
24
- renderControl.forceRerender();
25
- }),
26
- hookConfigs?.watchIssues
27
- ? controller.events.on("fieldIssuesUpdated", () =>
28
- renderControl.forceRerender()
29
- )
30
- : noop,
31
- hookConfigs?.watchValues
32
- ? controller.events.on("valueChanged", () =>
33
- renderControl.forceRerender()
34
- )
35
- : noop
36
- );
37
- }, [controller]);
38
-
39
- return {
40
- formConfigs,
41
- hookConfigs,
42
- controller,
43
- } as UseForm<TShape>;
44
- }
45
-
46
- export type UseForm<TShape extends object> = {
47
- formConfigs: Form.FormConfigs<TShape>;
48
- hookConfigs?: {
49
- validateMode?: "onChange" | "onBlur" | "onSubmit";
50
- revalidateMode?: "onChange" | "onBlur" | "onSubmit";
51
- watchIssues?: boolean;
52
- watchValues?: boolean;
53
- };
54
- controller: FormController<TShape>;
55
- };
1
+ import { FieldPathBuilder, FormController } from "@goodie-forms/core";
2
+ import { useEffect, useState } from "react";
3
+ import { useRenderControl } from "../hooks/useRenderControl";
4
+ import { composeFns } from "../utils/composeFns";
5
+
6
+ export function useForm<TOutput extends object>(
7
+ formConfigs: FormController.Configs<TOutput>,
8
+ hookConfigs?: {
9
+ validateMode?: "onChange" | "onBlur" | "onSubmit";
10
+ revalidateMode?: "onChange" | "onBlur" | "onSubmit";
11
+ watchIssues?: boolean;
12
+ watchValues?: boolean;
13
+ },
14
+ ) {
15
+ const [controller] = useState(() => new FormController(formConfigs));
16
+ const [paths] = useState(() => new FieldPathBuilder<TOutput>());
17
+
18
+ const renderControl = useRenderControl();
19
+
20
+ useEffect(() => {
21
+ const noop = () => {};
22
+
23
+ return composeFns(
24
+ controller.events.on("submissionStatusChange", () => {
25
+ renderControl.forceRerender();
26
+ }),
27
+ hookConfigs?.watchIssues
28
+ ? controller.events.on("fieldIssuesUpdated", () =>
29
+ renderControl.forceRerender(),
30
+ )
31
+ : noop,
32
+ hookConfigs?.watchValues
33
+ ? controller.events.on("valueChanged", () =>
34
+ renderControl.forceRerender(),
35
+ )
36
+ : noop,
37
+ );
38
+ }, [controller]);
39
+
40
+ return {
41
+ formConfigs,
42
+ paths,
43
+ hookConfigs,
44
+ controller,
45
+ };
46
+ }
47
+
48
+ export type UseForm<TOutput extends object> = ReturnType<
49
+ typeof useForm<TOutput>
50
+ >;
@@ -1,42 +1,44 @@
1
- import { Field } from "@goodie-forms/core";
2
- import { useEffect } from "react";
3
- import { composeFns } from "../utils/composeFns";
4
- import { groupBy } from "../utils/groupBy";
5
- import type { UseForm } from "./useForm";
6
- import { useRenderControl } from "./useRenderControl";
7
-
8
- export function useFormErrorObserver<
9
- TShape extends object,
10
- TInclude extends Field.Paths<TShape>[] | undefined
11
- >(
12
- form: UseForm<TShape>,
13
- options?: {
14
- include?: TInclude;
15
- }
16
- ) {
17
- const renderControl = useRenderControl();
18
-
19
- const filteredIssues = form.controller._issues.filter((issue) => {
20
- if (options?.include == null) return true;
21
- const path = Field.parsePath(issue.path!) as Field.Paths<TShape>;
22
- return options.include.includes(path);
23
- });
24
-
25
- const observedIssues = groupBy(filteredIssues, (issue) =>
26
- Field.parsePath(issue.path!)
27
- );
28
-
29
- useEffect(() => {
30
- const { events } = form.controller;
31
-
32
- return composeFns(
33
- events.on("fieldIssuesUpdated", (path) => {
34
- if (options?.include?.includes?.(path) ?? true) {
35
- renderControl.forceRerender();
36
- }
37
- })
38
- );
39
- }, []);
40
-
41
- return observedIssues;
42
- }
1
+ import { FieldPath } from "@goodie-forms/core";
2
+ import { groupBy } from "../utils/groupBy";
3
+ import { useEffect } from "react";
4
+ import { composeFns } from "../utils/composeFns";
5
+ import type { UseForm } from "./useForm";
6
+ import { useRenderControl } from "./useRenderControl";
7
+
8
+ export function useFormErrorObserver<
9
+ TOutput extends object,
10
+ TInclude extends FieldPath.Segments[] | undefined = undefined,
11
+ >(
12
+ form: UseForm<TOutput>,
13
+ options?: {
14
+ include?: TInclude;
15
+ },
16
+ ) {
17
+ const renderControl = useRenderControl();
18
+
19
+ const observedIssues = form.controller._issues.filter((issue) => {
20
+ if (options?.include == null) return true;
21
+ const normalizedIssuePath = FieldPath.normalize(issue.path);
22
+ return options.include.some((path) =>
23
+ FieldPath.equals(path, normalizedIssuePath),
24
+ );
25
+ });
26
+
27
+ useEffect(() => {
28
+ const { events } = form.controller;
29
+
30
+ return composeFns(
31
+ events.on("fieldIssuesUpdated", (path) => {
32
+ if (options?.include?.includes?.(path) ?? true) {
33
+ renderControl.forceRerender();
34
+ }
35
+ }),
36
+ );
37
+ }, []);
38
+
39
+ return groupBy(observedIssues, (issue) =>
40
+ issue.path == null
41
+ ? "$"
42
+ : FieldPath.toStringPath(FieldPath.normalize(issue.path)),
43
+ );
44
+ }
@@ -1,55 +1,63 @@
1
- import { Field } from "@goodie-forms/core";
2
- import { useEffect, useState } from "react";
3
- import { UseForm } from "../hooks/useForm";
4
- import { useRenderControl } from "../hooks/useRenderControl";
5
- import { composeFns } from "../utils/composeFns";
6
-
7
- export function useFormField<
8
- TShape extends object,
9
- TPath extends Field.Paths<TShape>
10
- >(
11
- form: UseForm<TShape>,
12
- path: TPath,
13
- bindingConfig?: Parameters<typeof form.controller.bindField<TPath>>[1]
14
- ) {
15
- const renderControl = useRenderControl();
16
-
17
- const [field, setField] = useState(() => {
18
- let field = form.controller.getField(path);
19
- if (field == null && bindingConfig != null) {
20
- field = form.controller.bindField(path, bindingConfig);
21
- }
22
- return field;
23
- });
24
-
25
- useEffect(() => {
26
- const { events } = form.controller;
27
-
28
- setField(form.controller.getField(path));
29
-
30
- return composeFns(
31
- events.on("fieldBound", (_path) => {
32
- if (_path === path) setField(form.controller.getField(path));
33
- }),
34
- events.on("fieldUnbound", (_path) => {
35
- if (_path === path) setField(undefined);
36
- }),
37
- events.on("valueChanged", (changedPath) => {
38
- if (changedPath === path || Field.isDescendant(changedPath, path)) {
39
- renderControl.forceRerender();
40
- }
41
- }),
42
- events.on("fieldTouchUpdated", (_path) => {
43
- if (_path === path) renderControl.forceRerender();
44
- }),
45
- events.on("fieldDirtyUpdated", (_path) => {
46
- if (_path === path) renderControl.forceRerender();
47
- }),
48
- events.on("fieldIssuesUpdated", (_path) => {
49
- if (_path === path) renderControl.forceRerender();
50
- })
51
- );
52
- }, []);
53
-
54
- return field;
55
- }
1
+ import { FieldPath } from "@goodie-forms/core";
2
+ import { useEffect, useState } from "react";
3
+ import { UseForm } from "../hooks/useForm";
4
+ import { useRenderControl } from "../hooks/useRenderControl";
5
+ import { composeFns } from "../utils/composeFns";
6
+
7
+ export function useFormField<
8
+ TOutput extends object,
9
+ TPath extends FieldPath.Segments,
10
+ >(
11
+ form: UseForm<TOutput>,
12
+ path: TPath,
13
+ bindingConfig?: Parameters<typeof form.controller.bindField<TPath>>[1],
14
+ ) {
15
+ const renderControl = useRenderControl();
16
+
17
+ const [field, setField] = useState(() => {
18
+ let field = form.controller.getField(path);
19
+ if (field == null && bindingConfig != null) {
20
+ field = form.controller.bindField(path, bindingConfig);
21
+ }
22
+ return field;
23
+ });
24
+
25
+ useEffect(() => {
26
+ const { events } = form.controller;
27
+
28
+ setField(form.controller.getField(path));
29
+
30
+ return composeFns(
31
+ events.on("fieldBound", (_path) => {
32
+ if (!FieldPath.equals(_path, path)) return;
33
+ setField(form.controller.getField(path));
34
+ }),
35
+ events.on("fieldUnbound", (_path) => {
36
+ if (!FieldPath.equals(_path, path)) return;
37
+ setField(undefined);
38
+ }),
39
+ events.on("valueChanged", (changedPath) => {
40
+ if (
41
+ FieldPath.equals(changedPath, path) ||
42
+ FieldPath.isDescendant(changedPath, path)
43
+ ) {
44
+ renderControl.forceRerender();
45
+ }
46
+ }),
47
+ events.on("fieldTouchUpdated", (_path) => {
48
+ if (!FieldPath.equals(_path, path)) return;
49
+ renderControl.forceRerender();
50
+ }),
51
+ events.on("fieldDirtyUpdated", (_path) => {
52
+ if (!FieldPath.equals(_path, path)) return;
53
+ renderControl.forceRerender();
54
+ }),
55
+ events.on("fieldIssuesUpdated", (_path) => {
56
+ if (!FieldPath.equals(_path, path)) return;
57
+ renderControl.forceRerender();
58
+ }),
59
+ );
60
+ }, []);
61
+
62
+ return field;
63
+ }