@goodie-forms/react 1.0.0-alpha → 1.1.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.
package/package.json CHANGED
@@ -1,32 +1,32 @@
1
- {
2
- "name": "@goodie-forms/react",
3
- "version": "1.0.0-alpha",
4
- "type": "module",
5
- "main": "dist/index.js",
6
- "types": "dist/index.d.ts",
7
- "exports": {
8
- ".": {
9
- "types": "./dist/index.d.ts",
10
- "default": "./dist/index.js"
11
- }
12
- },
13
- "scripts": {
14
- "build": "vite build",
15
- "prepublish": "npm run build"
16
- },
17
- "peerDependencies": {
18
- "react": "^18.0.0"
19
- },
20
- "dependencies": {
21
- "@goodie-forms/core": "workspace:*",
22
- "@vitejs/plugin-react": "^5.1.2",
23
- "react-dom": "^19.2.3"
24
- },
25
- "devDependencies": {
26
- "@types/react": "^19.2.9",
27
- "@types/react-dom": "^19.2.3",
28
- "typescript": "^5.9.3",
29
- "vite": "^7.3.1",
30
- "vite-plugin-dts": "^4.5.4"
31
- }
32
- }
1
+ {
2
+ "name": "@goodie-forms/react",
3
+ "version": "1.1.0-alpha",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "vite build",
15
+ "prepublish": "npm run build"
16
+ },
17
+ "peerDependencies": {
18
+ "react": "^18.0.0"
19
+ },
20
+ "dependencies": {
21
+ "@goodie-forms/core": "workspace:*",
22
+ "@vitejs/plugin-react": "^5.1.2",
23
+ "react-dom": "^19.2.3"
24
+ },
25
+ "devDependencies": {
26
+ "@types/react": "^19.2.9",
27
+ "@types/react-dom": "^19.2.3",
28
+ "typescript": "^5.9.3",
29
+ "vite": "^7.3.1",
30
+ "vite-plugin-dts": "^4.5.4"
31
+ }
32
+ }
@@ -1,107 +1,120 @@
1
- import { Field, FormField } 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: FormField<TShape, TPath>;
29
- }
30
-
31
- export interface FieldRendererProps<
32
- TShape extends object,
33
- TPath extends Field.Paths<TShape>,
34
- > {
35
- form: UseForm<TShape>;
36
- path: TPath;
37
- resetOnUnmount?: boolean;
38
- defaultValue?: Field.GetValue<TShape, TPath>;
39
- render: (params: RenderParams<TShape, TPath>) => ReactNode;
40
- }
41
-
42
- export function FieldRenderer<
43
- TShape extends object,
44
- TPath extends Field.Paths<TShape>,
45
- >(props: FieldRendererProps<TShape, TPath>) {
46
- const elementRef = useRef<HTMLElement>(null);
47
-
48
- const field = useFormField(props.form, props.path, props.defaultValue);
49
-
50
- const handlers: RenderParams<TShape, TPath>["handlers"] = {
51
- onChange(event) {
52
- const { target } = event;
53
- if (target !== field.boundElement) return;
54
- if (!("value" in target)) return;
55
- if (typeof target.value !== "string") return;
56
- field.setValue(target.value as Field.GetValue<TShape, TPath>, {
57
- shouldTouch: true,
58
- shouldMarkDirty: true,
59
- });
60
- },
61
- onFocus() {
62
- field.touch();
63
- },
64
- onBlur() {
65
- if (
66
- props.form.hookConfigs?.validateMode === "onBlur" ||
67
- props.form.hookConfigs?.validateMode === "onChange"
68
- ) {
69
- props.form.controller.validateField(props.path);
70
- }
71
- },
72
- };
73
-
74
- useEffect(() => {
75
- const { events } = props.form.controller;
76
-
77
- return composeFns(
78
- events.on("valueChanged", (path) => {
79
- if (path !== props.path) return;
80
- if (props.form.hookConfigs?.validateMode === "onChange") {
81
- props.form.controller.validateField(path);
82
- }
83
- }),
84
- );
85
- }, []);
86
-
87
- useEffect(() => {
88
- field.bindElement(elementRef.current!);
89
-
90
- return () => {
91
- if (props.resetOnUnmount) {
92
- props.form.controller.unbindField(props.path);
93
- }
94
- };
95
- }, []);
96
-
97
- return (
98
- <>
99
- {props.render({
100
- ref: elementRef,
101
- value: field.value,
102
- handlers: handlers,
103
- field: field,
104
- })}
105
- </>
106
- );
107
- }
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,51 +1,46 @@
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
- const [, setIsSubmitting] = useState(() => controller.isSubmitting);
20
- const [, setIsValid] = useState(() => controller.isValid);
21
-
22
- useEffect(() => {
23
- const noop = () => {};
24
-
25
- return composeFns(
26
- controller.events.on("statusChanged", (state) => {
27
- // Only re-render when submission state changes
28
- setIsSubmitting(state === "submitting");
29
- setIsValid(controller.isValid);
30
- }),
31
- hookConfigs?.watchIssues
32
- ? controller.events.on("validationIssuesUpdated", () =>
33
- renderControl.forceRerender(),
34
- )
35
- : noop,
36
- hookConfigs?.watchValues
37
- ? controller.events.on("valueChanged", () =>
38
- renderControl.forceRerender(),
39
- )
40
- : noop,
41
- );
42
- }, [controller]);
43
-
44
- return {
45
- formConfigs,
46
- hookConfigs,
47
- controller,
48
- };
49
- }
50
-
51
- export type UseForm<TShape extends object> = ReturnType<typeof useForm<TShape>>;
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
+ };
44
+ }
45
+
46
+ export type UseForm<TShape extends object> = ReturnType<typeof useForm<TShape>>;
@@ -1,38 +1,40 @@
1
- import type { 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<TShape extends object>(
9
- form: UseForm<TShape>,
10
- options?: {
11
- include?: Field.Paths<TShape>[];
12
- },
13
- ) {
14
- const renderControl = useRenderControl();
15
-
16
- useEffect(() => {
17
- const { events } = form.controller;
18
-
19
- return composeFns(
20
- events.on("validationIssuesUpdated", (path) => {
21
- if (options?.include?.includes?.(path) ?? true) {
22
- renderControl.forceRerender();
23
- }
24
- }),
25
- );
26
- }, []);
27
-
28
- const filteredIssues = form.controller._issues.filter((issue) => {
29
- if (options?.include == null) return true;
30
- const path = issue.path!.join(".") as Field.Paths<TShape>;
31
- return options.include.includes(path);
32
- });
33
-
34
- return groupBy(
35
- filteredIssues,
36
- (issue) => issue.path!.join(".") as Field.Paths<TShape>,
37
- );
38
- }
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<TShape extends object>(
9
+ form: UseForm<TShape>,
10
+ options?: {
11
+ include?: Field.Paths<TShape>[];
12
+ },
13
+ ) {
14
+ const renderControl = useRenderControl();
15
+
16
+ const filteredIssues = form.controller._issues.filter((issue) => {
17
+ if (options?.include == null) return true;
18
+ const path = Field.parsePath(issue.path!) as Field.Paths<TShape>;
19
+ return options.include.includes(path);
20
+ });
21
+
22
+ const observedIssues = groupBy(
23
+ filteredIssues,
24
+ (issue) => Field.parsePath(issue.path!) as Field.Paths<TShape>,
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 observedIssues;
40
+ }
@@ -1,46 +1,55 @@
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
- defaultValue?: Field.GetValue<TShape, TPath>,
14
- ) {
15
- const renderControl = useRenderControl();
16
-
17
- const [field] = useState(() => {
18
- return (
19
- form.controller.getField(path) ??
20
- form.controller.bindField(path, {
21
- defaultValue: defaultValue,
22
- })
23
- );
24
- });
25
-
26
- useEffect(() => {
27
- const { events } = form.controller;
28
-
29
- return composeFns(
30
- events.on("valueChanged", (_path) => {
31
- if (_path === path) renderControl.forceRerender();
32
- }),
33
- events.on("fieldTouchUpdated", (_path) => {
34
- if (_path === path) renderControl.forceRerender();
35
- }),
36
- events.on("fieldDirtyUpdated", (_path) => {
37
- if (_path === path) renderControl.forceRerender();
38
- }),
39
- events.on("validationIssuesUpdated", (_path) => {
40
- if (_path === path) renderControl.forceRerender();
41
- }),
42
- );
43
- }, []);
44
-
45
- return field;
46
- }
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,34 +1,45 @@
1
- import { Field } from "@goodie-forms/core";
2
- import { useEffect } from "react";
3
- import { composeFns } from "../utils/composeFns";
4
- import type { UseForm } from "./useForm";
5
- import { useRenderControl } from "./useRenderControl";
6
-
7
- export function useFormValuesObserver<TShape extends object>(
8
- form: UseForm<TShape>,
9
- options?: {
10
- include?: Field.Paths<TShape>[];
11
- },
12
- ) {
13
- const renderControl = useRenderControl();
14
-
15
- useEffect(() => {
16
- const { events } = form.controller;
17
-
18
- return composeFns(
19
- events.on("valueChanged", (path) => {
20
- if (options?.include?.includes?.(path) ?? true) {
21
- renderControl.forceRerender();
22
- }
23
- }),
24
- );
25
- }, []);
26
-
27
- return options?.include == null
28
- ? form.controller._data
29
- : options.include.reduce((data, path) => {
30
- const value = Field.getValue(form.controller._data as TShape, path)!;
31
- Field.setValue(data, path, value);
32
- return data;
33
- }, {} as TShape);
34
- }
1
+ import { Field } from "@goodie-forms/core";
2
+ import { useEffect } from "react";
3
+ import { composeFns } from "../utils/composeFns";
4
+ import type { UseForm } from "./useForm";
5
+ import { useRenderControl } from "./useRenderControl";
6
+
7
+ export function useFormValuesObserver<
8
+ TShape extends object,
9
+ TPaths extends Field.Paths<TShape>[] | undefined = undefined
10
+ >(
11
+ form: UseForm<TShape>,
12
+ options?: {
13
+ include?: TPaths;
14
+ }
15
+ ) {
16
+ const renderControl = useRenderControl();
17
+
18
+ const observedValues =
19
+ options?.include == null
20
+ ? form.controller._data
21
+ : options.include.reduce((data, path) => {
22
+ const value = Field.getValue(form.controller._data as TShape, path)!;
23
+ Field.setValue(data, path, value);
24
+ return data;
25
+ }, {} as TShape);
26
+
27
+ useEffect(() => {
28
+ const { events } = form.controller;
29
+
30
+ return composeFns(
31
+ events.on("valueChanged", (changedPath) => {
32
+ const watchingChange =
33
+ options?.include == null
34
+ ? true
35
+ : options.include.some(
36
+ (path) =>
37
+ path === changedPath || Field.isDescendant(path, changedPath)
38
+ );
39
+ if (watchingChange) renderControl.forceRerender();
40
+ })
41
+ );
42
+ }, []);
43
+
44
+ return observedValues;
45
+ }