@os-design/form 1.0.29 → 1.0.31
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 +13 -5
- package/src/BroadcastObserverManager.ts +33 -0
- package/src/ErrorData.ts +30 -0
- package/src/ErrorObserverManager.ts +40 -0
- package/src/Form.ts +38 -0
- package/src/Node.ts +42 -0
- package/src/ValueData.ts +31 -0
- package/src/ValueObserverManager.ts +83 -0
- package/src/index.tsx +246 -0
- package/src/types.ts +35 -0
- package/src/useFormContext.tsx +25 -0
- package/src/utils/clone.ts +27 -0
- package/src/utils/deleteFromArray.ts +8 -0
- package/src/utils/isEqual.ts +38 -0
- package/src/utils/path.ts +44 -0
- package/src/utils/useDeepEqualMemo.ts +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@os-design/form",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.31",
|
|
4
4
|
"license": "UNLICENSED",
|
|
5
5
|
"repository": "git@gitlab.com:os-team/libs/os-design.git",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -14,7 +14,15 @@
|
|
|
14
14
|
"./package.json": "./package.json"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"src",
|
|
19
|
+
"!**/*.test.ts",
|
|
20
|
+
"!**/*.test.tsx",
|
|
21
|
+
"!**/__tests__",
|
|
22
|
+
"!**/*.stories.tsx",
|
|
23
|
+
"!**/*.stories.mdx",
|
|
24
|
+
"!**/*.example.tsx",
|
|
25
|
+
"!**/*.emotion.d.ts"
|
|
18
26
|
],
|
|
19
27
|
"scripts": {
|
|
20
28
|
"clean": "rimraf dist",
|
|
@@ -28,11 +36,11 @@
|
|
|
28
36
|
"access": "public"
|
|
29
37
|
},
|
|
30
38
|
"devDependencies": {
|
|
31
|
-
"@os-design/core": "^1.0.
|
|
32
|
-
"@os-design/icons": "^1.0.
|
|
39
|
+
"@os-design/core": "^1.0.200",
|
|
40
|
+
"@os-design/icons": "^1.0.48"
|
|
33
41
|
},
|
|
34
42
|
"peerDependencies": {
|
|
35
43
|
"react": ">=18"
|
|
36
44
|
},
|
|
37
|
-
"gitHead": "
|
|
45
|
+
"gitHead": "3d6b264027712ef81a75379fe3fde3c76c3079af"
|
|
38
46
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Subscription } from './types';
|
|
2
|
+
import deleteFromArray from './utils/deleteFromArray';
|
|
3
|
+
|
|
4
|
+
export type BroadcastObserver<TName, TValue> = (
|
|
5
|
+
name: TName,
|
|
6
|
+
value: TValue
|
|
7
|
+
) => void;
|
|
8
|
+
|
|
9
|
+
class BroadcastObserverManager<TName, TValue> {
|
|
10
|
+
private readonly broadcastObservers: Array<BroadcastObserver<TName, TValue>>;
|
|
11
|
+
|
|
12
|
+
public constructor() {
|
|
13
|
+
this.broadcastObservers = [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public subscribeToAll(
|
|
17
|
+
observer: BroadcastObserver<TName, TValue>
|
|
18
|
+
): Subscription {
|
|
19
|
+
this.broadcastObservers.push(observer);
|
|
20
|
+
return {
|
|
21
|
+
unsubscribe: () => deleteFromArray(this.broadcastObservers, observer),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
protected callBroadcastObservers<N extends TName, V extends TValue>(
|
|
26
|
+
name: N,
|
|
27
|
+
value: V
|
|
28
|
+
): void {
|
|
29
|
+
this.broadcastObservers.forEach((observer) => observer(name, value));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default BroadcastObserverManager;
|
package/src/ErrorData.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import ErrorObserverManager from './ErrorObserverManager';
|
|
2
|
+
import { Errors } from './types';
|
|
3
|
+
import clone from './utils/clone';
|
|
4
|
+
|
|
5
|
+
class ErrorData<
|
|
6
|
+
TErrors extends Errors,
|
|
7
|
+
TName extends keyof TErrors = keyof TErrors
|
|
8
|
+
> extends ErrorObserverManager<TErrors> {
|
|
9
|
+
private readonly errors: TErrors;
|
|
10
|
+
|
|
11
|
+
public constructor(errors: TErrors) {
|
|
12
|
+
super();
|
|
13
|
+
this.errors = errors;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public get<T extends TName>(name: T): TErrors[T] {
|
|
17
|
+
return this.errors[name];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public getAll(): TErrors {
|
|
21
|
+
return clone(this.errors);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public set<T extends TName>(name: T, value: TErrors[T]): void {
|
|
25
|
+
this.errors[name] = value;
|
|
26
|
+
this.callObservers(name, value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default ErrorData;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import BroadcastObserverManager from './BroadcastObserverManager';
|
|
2
|
+
import { Errors, Observer, Subscription } from './types';
|
|
3
|
+
import deleteFromArray from './utils/deleteFromArray';
|
|
4
|
+
|
|
5
|
+
export type ErrorObserver = Observer<string | undefined>;
|
|
6
|
+
|
|
7
|
+
class ErrorObserverManager<
|
|
8
|
+
TErrors extends Errors,
|
|
9
|
+
TName extends keyof TErrors = keyof TErrors
|
|
10
|
+
> extends BroadcastObserverManager<TName, string | undefined> {
|
|
11
|
+
private readonly observers: {
|
|
12
|
+
[K in TName]?: ErrorObserver[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
public constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.observers = {};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private getObservers<T extends TName>(name: T): ErrorObserver[] {
|
|
21
|
+
if (!this.observers[name]) this.observers[name] = [];
|
|
22
|
+
return this.observers[name] as ErrorObserver[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public subscribe<T extends TName>(
|
|
26
|
+
name: T,
|
|
27
|
+
observer: ErrorObserver
|
|
28
|
+
): Subscription {
|
|
29
|
+
const observers = this.getObservers(name);
|
|
30
|
+
observers.push(observer);
|
|
31
|
+
return { unsubscribe: () => deleteFromArray(observers, observer) };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected callObservers<T extends TName>(name: T, value: string | undefined) {
|
|
35
|
+
this.callBroadcastObservers(name, value);
|
|
36
|
+
this.getObservers(name).forEach((observer) => observer(value));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default ErrorObserverManager;
|
package/src/Form.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import ErrorData from './ErrorData';
|
|
2
|
+
import { Errors, Path, Values } from './types';
|
|
3
|
+
import clone from './utils/clone';
|
|
4
|
+
import ValueData from './ValueData';
|
|
5
|
+
|
|
6
|
+
class Form<
|
|
7
|
+
TValues extends Values = Values,
|
|
8
|
+
TErrors extends Errors<TValues> = Errors<TValues>
|
|
9
|
+
> {
|
|
10
|
+
public initValues: TValues;
|
|
11
|
+
|
|
12
|
+
public readonly values: ValueData<TValues>;
|
|
13
|
+
|
|
14
|
+
public readonly errors: ErrorData<TErrors>;
|
|
15
|
+
|
|
16
|
+
public constructor(initValues: TValues) {
|
|
17
|
+
this.initValues = initValues;
|
|
18
|
+
this.values = new ValueData(clone(initValues));
|
|
19
|
+
this.errors = new ErrorData({} as TErrors);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public reset() {
|
|
23
|
+
// Reset values
|
|
24
|
+
Object.keys(this.values.getAll()).forEach((name) => {
|
|
25
|
+
this.values.set(name as Path<TValues>, clone(this.initValues[name]));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Reset errors
|
|
29
|
+
Object.keys(this.errors.getAll()).forEach((name) => {
|
|
30
|
+
this.errors.set(
|
|
31
|
+
name as keyof TErrors,
|
|
32
|
+
undefined as TErrors[keyof TErrors]
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default Form;
|
package/src/Node.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Observer } from './types';
|
|
2
|
+
import deleteFromArray from './utils/deleteFromArray';
|
|
3
|
+
|
|
4
|
+
class Node {
|
|
5
|
+
public readonly name: string;
|
|
6
|
+
|
|
7
|
+
public readonly parent: Node | null;
|
|
8
|
+
|
|
9
|
+
public readonly nodes: Record<string, Node>;
|
|
10
|
+
|
|
11
|
+
public readonly observers: Observer[];
|
|
12
|
+
|
|
13
|
+
public constructor(parent: Node | null = null, name = '') {
|
|
14
|
+
this.name = name;
|
|
15
|
+
this.parent = parent;
|
|
16
|
+
this.nodes = {};
|
|
17
|
+
this.observers = [];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public getNode(name: string): Node | undefined {
|
|
21
|
+
return this.nodes[name];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public createNode(name: string): Node {
|
|
25
|
+
this.nodes[name] = new Node(this, name);
|
|
26
|
+
return this.nodes[name];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public deleteNode(name: string): void {
|
|
30
|
+
delete this.nodes[name];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public addObserver(observer: Observer): void {
|
|
34
|
+
this.observers.push(observer);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public deleteObserver(observer: Observer): void {
|
|
38
|
+
deleteFromArray(this.observers, observer);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default Node;
|
package/src/ValueData.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Path, PathReturn, Values } from './types';
|
|
2
|
+
import clone from './utils/clone';
|
|
3
|
+
import { get, set } from './utils/path';
|
|
4
|
+
import ValueObserverManager from './ValueObserverManager';
|
|
5
|
+
|
|
6
|
+
class ValueData<
|
|
7
|
+
TValues extends Values,
|
|
8
|
+
TName extends Path<TValues> = Path<TValues>
|
|
9
|
+
> extends ValueObserverManager<TValues> {
|
|
10
|
+
private readonly values: TValues;
|
|
11
|
+
|
|
12
|
+
public constructor(values: TValues) {
|
|
13
|
+
super();
|
|
14
|
+
this.values = values;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public get<T extends TName>(name: T): PathReturn<TValues, T> {
|
|
18
|
+
return clone(get(this.values, name));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public getAll(): TValues {
|
|
22
|
+
return clone(this.values);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public set<T extends TName>(name: T, value: PathReturn<TValues, T>): void {
|
|
26
|
+
const ok = set(this.values, name, clone(value));
|
|
27
|
+
if (ok) this.callObservers(name, this.values);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default ValueData;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import BroadcastObserverManager from './BroadcastObserverManager';
|
|
2
|
+
import Node from './Node';
|
|
3
|
+
import { Observer, Path, PathReturn, Subscription, Values } from './types';
|
|
4
|
+
import { get, isObjectOrArray } from './utils/path';
|
|
5
|
+
|
|
6
|
+
const split = (path: string) => path.split('.');
|
|
7
|
+
|
|
8
|
+
class ValueObserverManager<
|
|
9
|
+
TValues extends Values,
|
|
10
|
+
TName extends Path<TValues> = Path<TValues>
|
|
11
|
+
> extends BroadcastObserverManager<TName, TValues[TName]> {
|
|
12
|
+
private readonly root: Node;
|
|
13
|
+
|
|
14
|
+
public constructor() {
|
|
15
|
+
super();
|
|
16
|
+
this.root = new Node();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private findNode(path: string) {
|
|
20
|
+
const keys = split(path);
|
|
21
|
+
let cur = this.root;
|
|
22
|
+
keys.forEach((key) => {
|
|
23
|
+
cur = cur.getNode(key) || cur.createNode(key);
|
|
24
|
+
});
|
|
25
|
+
return cur;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public subscribe<T extends TName>(
|
|
29
|
+
name: T,
|
|
30
|
+
observer: Observer<PathReturn<TValues, T>>
|
|
31
|
+
): Subscription {
|
|
32
|
+
const node = this.findNode(name);
|
|
33
|
+
node.addObserver(observer);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
unsubscribe: () => {
|
|
37
|
+
node.deleteObserver(observer);
|
|
38
|
+
if (node.observers.length === 0) {
|
|
39
|
+
node.parent?.deleteNode(node.name);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected callObservers<T extends TName>(name: T, data: TValues): void {
|
|
46
|
+
this.callBroadcastObservers(name, get(data, name));
|
|
47
|
+
this.callNodeObservers(name, data);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private callNodeObservers<T extends TName>(name: T, data: TValues): void {
|
|
51
|
+
const keys = split(name);
|
|
52
|
+
let curNode = this.root;
|
|
53
|
+
let curData = data;
|
|
54
|
+
|
|
55
|
+
// Call the observers for the root node
|
|
56
|
+
curNode.observers.forEach((observer) => observer(curData));
|
|
57
|
+
|
|
58
|
+
// Call observers on each node from the root to the specified one
|
|
59
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
60
|
+
const key = keys[i];
|
|
61
|
+
const node = curNode.getNode(key);
|
|
62
|
+
if (!node || !isObjectOrArray(curData)) return;
|
|
63
|
+
curNode = node;
|
|
64
|
+
curData = curData[key];
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
|
66
|
+
curNode.observers.forEach((observer) => observer(curData));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Call observers of all child nodes started from the specified one
|
|
70
|
+
this.callChildObservers(curNode, curData);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private callChildObservers(parent: Node, data: TValues): void {
|
|
74
|
+
if (!isObjectOrArray(data)) return;
|
|
75
|
+
Object.entries(parent.nodes).forEach(([name, node]) => {
|
|
76
|
+
const value = data[name];
|
|
77
|
+
node.observers.forEach((observer) => observer(value));
|
|
78
|
+
this.callChildObservers(node, value);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export default ValueObserverManager;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReactElement,
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import Form from './Form';
|
|
10
|
+
import { Path, PathReturn, Values } from './types';
|
|
11
|
+
import useFormContext from './useFormContext';
|
|
12
|
+
import isEqual from './utils/isEqual';
|
|
13
|
+
import { get } from './utils/path';
|
|
14
|
+
import useDeepEqualMemo from './utils/useDeepEqualMemo';
|
|
15
|
+
|
|
16
|
+
export * from './BroadcastObserverManager';
|
|
17
|
+
export * from './ErrorObserverManager';
|
|
18
|
+
export { default as Form } from './Form';
|
|
19
|
+
export * from './types';
|
|
20
|
+
export * from './useFormContext';
|
|
21
|
+
|
|
22
|
+
const createUseValueHook =
|
|
23
|
+
<TValues extends Values>(form: Form<TValues>) =>
|
|
24
|
+
<TName extends Path<TValues>>(name: TName) => {
|
|
25
|
+
const [value, setValue] = useState<PathReturn<TValues, TName>>(
|
|
26
|
+
form.values.get(name)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const subscription = form.values.subscribe(name, (v) => {
|
|
31
|
+
setValue(v);
|
|
32
|
+
});
|
|
33
|
+
return () => subscription.unsubscribe();
|
|
34
|
+
}, [name]);
|
|
35
|
+
|
|
36
|
+
return value;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const createUseErrorHook =
|
|
40
|
+
<TValues extends Values>(form: Form<TValues>) =>
|
|
41
|
+
<TName extends Path<TValues>>(name: TName) => {
|
|
42
|
+
const [value, setValue] = useState<string | undefined>(
|
|
43
|
+
form.errors.get(name)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const subscription = form.errors.subscribe(name, (v) => {
|
|
48
|
+
setValue(v);
|
|
49
|
+
});
|
|
50
|
+
return () => subscription.unsubscribe();
|
|
51
|
+
}, [name]);
|
|
52
|
+
|
|
53
|
+
return value;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type Transformer<TValues extends Values, TName extends Path<TValues>> = (
|
|
57
|
+
value: PathReturn<TValues, TName>
|
|
58
|
+
) => {
|
|
59
|
+
[K in Path<TValues>]?: PathReturn<TValues, K>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const createUseTransformerHook = <TValues extends Values>(
|
|
63
|
+
form: Form<TValues>
|
|
64
|
+
) => {
|
|
65
|
+
const useValue = createUseValueHook(form);
|
|
66
|
+
return <TName extends Path<TValues>>(
|
|
67
|
+
name: TName,
|
|
68
|
+
transformer: Transformer<TValues, TName>
|
|
69
|
+
) => {
|
|
70
|
+
const value = useValue(name);
|
|
71
|
+
|
|
72
|
+
const transformerRef = useRef(transformer);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
transformerRef.current = transformer;
|
|
75
|
+
}, [transformer]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const partialValues = transformerRef.current(value);
|
|
79
|
+
Object.entries(partialValues).forEach(([n, v]) => {
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
form.values.set(n as Path<TValues>, v as any);
|
|
82
|
+
});
|
|
83
|
+
}, [value]);
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
interface InputProps<TValue> {
|
|
88
|
+
value: TValue;
|
|
89
|
+
onChange: (value: TValue) => void;
|
|
90
|
+
}
|
|
91
|
+
interface FieldState {
|
|
92
|
+
error?: string;
|
|
93
|
+
modified: boolean;
|
|
94
|
+
reset: () => void;
|
|
95
|
+
}
|
|
96
|
+
interface FieldProps<TName, TValue, TData> {
|
|
97
|
+
name: TName;
|
|
98
|
+
data?: TData;
|
|
99
|
+
transformer?: (value: TValue) => TValue;
|
|
100
|
+
render: (
|
|
101
|
+
inputProps: InputProps<TValue>,
|
|
102
|
+
fieldState: FieldState,
|
|
103
|
+
data: TData
|
|
104
|
+
) => ReactElement | null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const createFieldComponent = <TValues extends Values>(form: Form<TValues>) => {
|
|
108
|
+
const useValue = createUseValueHook(form);
|
|
109
|
+
const useError = createUseErrorHook(form);
|
|
110
|
+
return <TName extends Path<TValues>, TData>(
|
|
111
|
+
props: FieldProps<TName, PathReturn<TValues, TName>, TData>
|
|
112
|
+
) => {
|
|
113
|
+
const { name, data, transformer = (v) => v, render } = props;
|
|
114
|
+
|
|
115
|
+
const value = useValue(name);
|
|
116
|
+
const error = useError(name);
|
|
117
|
+
|
|
118
|
+
const initValue = useMemo(() => get(form.initValues, name), [name]);
|
|
119
|
+
const modified = useMemo(
|
|
120
|
+
() => !isEqual(value, initValue),
|
|
121
|
+
[initValue, value]
|
|
122
|
+
);
|
|
123
|
+
const reset = useCallback(
|
|
124
|
+
() => form.values.set(name, initValue),
|
|
125
|
+
[initValue, name]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const transformerRef = useRef(transformer);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
transformerRef.current = transformer;
|
|
131
|
+
}, [transformer]);
|
|
132
|
+
|
|
133
|
+
const onChange = useCallback(
|
|
134
|
+
(v: PathReturn<TValues, TName>) => {
|
|
135
|
+
form.values.set(name, transformerRef.current(v));
|
|
136
|
+
},
|
|
137
|
+
[name]
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Reset the error when the value was changed
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
form.errors.set(name, undefined);
|
|
143
|
+
}, [name, value]);
|
|
144
|
+
|
|
145
|
+
const renderRef = useRef(render);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
renderRef.current = render;
|
|
148
|
+
}, [render]);
|
|
149
|
+
|
|
150
|
+
const inputProps = useMemo(() => ({ value, onChange }), [onChange, value]);
|
|
151
|
+
const fieldState = useMemo(
|
|
152
|
+
() => ({ error, modified, reset }),
|
|
153
|
+
[error, modified, reset]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const memoizedData = useDeepEqualMemo(() => data as TData, [data]);
|
|
157
|
+
|
|
158
|
+
return useMemo(
|
|
159
|
+
() => renderRef.current(inputProps, fieldState, memoizedData),
|
|
160
|
+
[fieldState, inputProps, memoizedData]
|
|
161
|
+
);
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const useModifiedFields = <TValues extends Values>(form: Form<TValues>) => {
|
|
166
|
+
const [modifiedFields, setModifiedFields] = useState<Array<Path<TValues>>>(
|
|
167
|
+
[]
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const modifiedFieldsRef = useRef(modifiedFields);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
modifiedFieldsRef.current = modifiedFields;
|
|
173
|
+
}, [modifiedFields]);
|
|
174
|
+
|
|
175
|
+
const initValuesRef = useRef(form.initValues);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
initValuesRef.current = form.initValues;
|
|
178
|
+
}, [form.initValues]);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
const subscription = form.values.subscribeToAll((name, value) => {
|
|
182
|
+
const isInitValue = isEqual(value, get(initValuesRef.current, name));
|
|
183
|
+
const fields = modifiedFieldsRef.current;
|
|
184
|
+
|
|
185
|
+
if (fields.includes(name)) {
|
|
186
|
+
if (isInitValue) {
|
|
187
|
+
setModifiedFields(fields.filter((field) => field !== name));
|
|
188
|
+
}
|
|
189
|
+
} else if (!isInitValue) {
|
|
190
|
+
setModifiedFields([...fields, name]);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
return () => subscription.unsubscribe();
|
|
194
|
+
}, [form.values]);
|
|
195
|
+
|
|
196
|
+
return modifiedFields;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const useFormResponse = <TValues extends Values>(form: Form<TValues>) => {
|
|
200
|
+
const Field = useMemo(() => createFieldComponent(form), [form]);
|
|
201
|
+
const useValue = useMemo(() => createUseValueHook(form), [form]);
|
|
202
|
+
const useError = useMemo(() => createUseErrorHook(form), [form]);
|
|
203
|
+
const useTransformer = useMemo(() => createUseTransformerHook(form), [form]);
|
|
204
|
+
|
|
205
|
+
const modifiedFields = useModifiedFields(form);
|
|
206
|
+
const modified = useMemo(
|
|
207
|
+
() => modifiedFields.length > 0,
|
|
208
|
+
[modifiedFields.length]
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
return useMemo(
|
|
212
|
+
() => ({
|
|
213
|
+
form,
|
|
214
|
+
Field,
|
|
215
|
+
useValue,
|
|
216
|
+
useError,
|
|
217
|
+
useTransformer,
|
|
218
|
+
modifiedFields,
|
|
219
|
+
modified,
|
|
220
|
+
}),
|
|
221
|
+
[Field, form, modified, modifiedFields, useError, useTransformer, useValue]
|
|
222
|
+
);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export const useForm = <TValues extends Values = Values>(
|
|
226
|
+
initValues: TValues
|
|
227
|
+
) => {
|
|
228
|
+
const memoizedInitValues = useDeepEqualMemo<TValues>(
|
|
229
|
+
() => initValues,
|
|
230
|
+
[initValues]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const formRef = useRef(new Form<TValues>(memoizedInitValues));
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
formRef.current.initValues = memoizedInitValues;
|
|
236
|
+
formRef.current.reset();
|
|
237
|
+
}, [memoizedInitValues]);
|
|
238
|
+
|
|
239
|
+
return useFormResponse(formRef.current);
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
export const useExistingForm = <TValues extends Values = Values>() => {
|
|
243
|
+
const form = useFormContext<Form<TValues> | null>();
|
|
244
|
+
if (!form) throw new Error('Wrap your form in a FormProvider');
|
|
245
|
+
return useFormResponse(form);
|
|
246
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
|
|
3
|
+
type Separator = '.';
|
|
4
|
+
type KeysWithSeparator<K1, K2> = `${K1 & string}${Separator}${K2 & string}`;
|
|
5
|
+
|
|
6
|
+
type OwnKeyOf<T> = Exclude<keyof T, keyof any[]> & string;
|
|
7
|
+
type IsAny<T> = unknown extends T & string ? true : false;
|
|
8
|
+
|
|
9
|
+
export type Path<T, K = OwnKeyOf<T>> = IsAny<T> extends true
|
|
10
|
+
? string
|
|
11
|
+
: T extends object
|
|
12
|
+
?
|
|
13
|
+
| K
|
|
14
|
+
| (K extends keyof T
|
|
15
|
+
? T[K] extends object
|
|
16
|
+
? KeysWithSeparator<K, Path<T[K]>>
|
|
17
|
+
: never
|
|
18
|
+
: never)
|
|
19
|
+
: never;
|
|
20
|
+
|
|
21
|
+
export type PathReturn<T, K> = K extends keyof T
|
|
22
|
+
? T[K]
|
|
23
|
+
: K extends `${infer U}${Separator}${infer R}`
|
|
24
|
+
? U extends keyof T
|
|
25
|
+
? PathReturn<T[U], R>
|
|
26
|
+
: never
|
|
27
|
+
: never;
|
|
28
|
+
|
|
29
|
+
export type Values<T = any> = Record<string, T>;
|
|
30
|
+
export type Errors<T = any> = Record<Path<T>, string | undefined>;
|
|
31
|
+
|
|
32
|
+
export type Observer<V = any> = (value: V) => void;
|
|
33
|
+
export interface Subscription {
|
|
34
|
+
unsubscribe: () => void;
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { createContext, ReactNode, useContext } from 'react';
|
|
2
|
+
import Form from './Form';
|
|
3
|
+
import { Values } from './types';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
const FormContext = createContext<any>(null);
|
|
7
|
+
|
|
8
|
+
export interface FormProviderProps<TValues extends Values> {
|
|
9
|
+
form: Form<TValues>;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line react/function-component-definition
|
|
14
|
+
export function FormProvider<TValues extends Values>({
|
|
15
|
+
form,
|
|
16
|
+
children,
|
|
17
|
+
}: FormProviderProps<TValues>) {
|
|
18
|
+
return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function useFormContext<T>() {
|
|
22
|
+
return useContext<T>(FormContext);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default useFormContext;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { isValidElement } from 'react';
|
|
2
|
+
|
|
3
|
+
const clone = <T>(value: T): T => {
|
|
4
|
+
if (typeof value === 'object') {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return value.map((item) => clone(item)) as T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (isValidElement(value)) return value;
|
|
10
|
+
if (typeof window !== 'undefined' && value instanceof File) {
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (value !== null) {
|
|
15
|
+
const clonedObj = {};
|
|
16
|
+
Object.entries(value).forEach(([objKey, objValue]) => {
|
|
17
|
+
clonedObj[objKey] = clone(objValue);
|
|
18
|
+
});
|
|
19
|
+
Object.setPrototypeOf(clonedObj, Object.getPrototypeOf(value));
|
|
20
|
+
return clonedObj as T;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default clone;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isValidElement } from 'react';
|
|
2
|
+
|
|
3
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
4
|
+
const isEqual = (value1: any, value2: any): boolean => {
|
|
5
|
+
if (typeof value1 !== typeof value2) return false;
|
|
6
|
+
|
|
7
|
+
if (typeof value1 === 'object') {
|
|
8
|
+
if (isValidElement(value1) || isValidElement(value2)) {
|
|
9
|
+
return value1 === value2;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(value1) !== Array.isArray(value2)) return false;
|
|
12
|
+
|
|
13
|
+
if (Array.isArray(value1)) {
|
|
14
|
+
if (value1.length !== value2.length) return false;
|
|
15
|
+
|
|
16
|
+
return value1.every((item1, index) => {
|
|
17
|
+
const item2 = value2[index];
|
|
18
|
+
return isEqual(item1, item2);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (value1 !== null && value2 !== null) {
|
|
23
|
+
if (Object.keys(value1).length !== Object.keys(value2).length) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Object.entries(value1).every(([objKey, objValue1]) => {
|
|
28
|
+
if (!Object.hasOwn(value2, objKey)) return false;
|
|
29
|
+
const objValue2 = value2[objKey];
|
|
30
|
+
return isEqual(objValue1, objValue2);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return Object.is(value1, value2);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default isEqual;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any,no-param-reassign */
|
|
2
|
+
|
|
3
|
+
export const isObjectOrArray = (value: any): value is object =>
|
|
4
|
+
typeof value === 'object' && value !== null;
|
|
5
|
+
|
|
6
|
+
const split = (path: string) => {
|
|
7
|
+
const groups = path.match(/^([^.]*)\.(.*)$/);
|
|
8
|
+
return groups ? [groups[1], groups[2]] : null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const get = (data: Record<string, any>, path: string): any => {
|
|
12
|
+
if (Object.hasOwn(data, path)) {
|
|
13
|
+
return data[path];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const groups = split(path);
|
|
17
|
+
if (!groups) return undefined;
|
|
18
|
+
|
|
19
|
+
const [key, nextPath] = groups;
|
|
20
|
+
if (!Object.hasOwn(data, key)) return undefined;
|
|
21
|
+
if (!isObjectOrArray(data[key])) return undefined;
|
|
22
|
+
|
|
23
|
+
return get(data[key], nextPath);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const set = (
|
|
27
|
+
data: Record<string, any>,
|
|
28
|
+
path: string,
|
|
29
|
+
value: any
|
|
30
|
+
): boolean => {
|
|
31
|
+
if (Object.hasOwn(data, path)) {
|
|
32
|
+
data[path] = value;
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const groups = split(path);
|
|
37
|
+
if (!groups) return false;
|
|
38
|
+
|
|
39
|
+
const [key, nextPath] = groups;
|
|
40
|
+
if (!Object.hasOwn(data, key)) return false;
|
|
41
|
+
if (!isObjectOrArray(data[key])) return false;
|
|
42
|
+
|
|
43
|
+
return set(data[key], nextPath, value);
|
|
44
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DependencyList, useMemo, useRef } from 'react';
|
|
2
|
+
import clone from './clone';
|
|
3
|
+
import isEqual from './isEqual';
|
|
4
|
+
|
|
5
|
+
const useDeepEqualMemoize = <T>(value: T) => {
|
|
6
|
+
const ref = useRef<T | undefined>(undefined);
|
|
7
|
+
if (!isEqual(ref.current, value)) {
|
|
8
|
+
ref.current = clone(value);
|
|
9
|
+
}
|
|
10
|
+
return ref.current;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/* eslint-disable react-hooks/exhaustive-deps */
|
|
14
|
+
const useDeepEqualMemo = <T>(
|
|
15
|
+
factory: () => T,
|
|
16
|
+
deps: DependencyList | undefined
|
|
17
|
+
): T => useMemo(factory, deps ? deps.map(useDeepEqualMemoize) : undefined);
|
|
18
|
+
|
|
19
|
+
export default useDeepEqualMemo;
|