rn-inputforge 0.3.0 → 0.4.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 (196) hide show
  1. package/README.md +224 -19
  2. package/lib/commonjs/components/FieldFlowCheckbox/FieldFlowCheckbox.js +131 -0
  3. package/lib/commonjs/components/FieldFlowCheckbox/FieldFlowCheckbox.js.map +1 -0
  4. package/lib/commonjs/components/FieldFlowCheckbox/index.js +13 -0
  5. package/lib/commonjs/components/FieldFlowCheckbox/index.js.map +1 -0
  6. package/lib/commonjs/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js +536 -0
  7. package/lib/commonjs/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js.map +1 -0
  8. package/lib/commonjs/components/FieldFlowDateTimePicker/index.js +13 -0
  9. package/lib/commonjs/components/FieldFlowDateTimePicker/index.js.map +1 -0
  10. package/lib/commonjs/components/FieldFlowDateTimePicker/types.js +6 -0
  11. package/lib/commonjs/components/FieldFlowDateTimePicker/types.js.map +1 -0
  12. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js +24 -9
  13. package/lib/commonjs/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -1
  14. package/lib/commonjs/components/FieldFlowInput/FieldFlowInput.js +55 -24
  15. package/lib/commonjs/components/FieldFlowInput/FieldFlowInput.js.map +1 -1
  16. package/lib/commonjs/components/FieldFlowInput/styles.js +32 -11
  17. package/lib/commonjs/components/FieldFlowInput/styles.js.map +1 -1
  18. package/lib/commonjs/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js +66 -0
  19. package/lib/commonjs/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js.map +1 -0
  20. package/lib/commonjs/components/FieldFlowMaskedInput/index.js +13 -0
  21. package/lib/commonjs/components/FieldFlowMaskedInput/index.js.map +1 -0
  22. package/lib/commonjs/components/FieldFlowMaskedInput/types.js +6 -0
  23. package/lib/commonjs/components/FieldFlowMaskedInput/types.js.map +1 -0
  24. package/lib/commonjs/components/FieldFlowOTP/FieldFlowOTP.js +255 -0
  25. package/lib/commonjs/components/FieldFlowOTP/FieldFlowOTP.js.map +1 -0
  26. package/lib/commonjs/components/FieldFlowOTP/index.js +13 -0
  27. package/lib/commonjs/components/FieldFlowOTP/index.js.map +1 -0
  28. package/lib/commonjs/components/FieldFlowOTP/types.js +6 -0
  29. package/lib/commonjs/components/FieldFlowOTP/types.js.map +1 -0
  30. package/lib/commonjs/components/FieldFlowRadio/FieldFlowRadio.js +164 -0
  31. package/lib/commonjs/components/FieldFlowRadio/FieldFlowRadio.js.map +1 -0
  32. package/lib/commonjs/components/FieldFlowRadio/index.js +13 -0
  33. package/lib/commonjs/components/FieldFlowRadio/index.js.map +1 -0
  34. package/lib/commonjs/components/FieldFlowSwitch/FieldFlowSwitch.js +152 -0
  35. package/lib/commonjs/components/FieldFlowSwitch/FieldFlowSwitch.js.map +1 -0
  36. package/lib/commonjs/components/FieldFlowSwitch/index.js +13 -0
  37. package/lib/commonjs/components/FieldFlowSwitch/index.js.map +1 -0
  38. package/lib/commonjs/hooks/index.js.map +1 -1
  39. package/lib/commonjs/hooks/useFieldFlowForm.js +146 -31
  40. package/lib/commonjs/hooks/useFieldFlowForm.js.map +1 -1
  41. package/lib/commonjs/icons/Icon.js +719 -30
  42. package/lib/commonjs/icons/Icon.js.map +1 -1
  43. package/lib/commonjs/icons/index.js +7 -0
  44. package/lib/commonjs/icons/index.js.map +1 -1
  45. package/lib/commonjs/icons/renderIcon.js +24 -0
  46. package/lib/commonjs/icons/renderIcon.js.map +1 -0
  47. package/lib/commonjs/index.js +127 -0
  48. package/lib/commonjs/index.js.map +1 -1
  49. package/lib/commonjs/utils/masks.js +130 -0
  50. package/lib/commonjs/utils/masks.js.map +1 -0
  51. package/lib/commonjs/validation/index.js +25 -0
  52. package/lib/commonjs/validation/index.js.map +1 -1
  53. package/lib/commonjs/validation/zodAdapter.js +91 -0
  54. package/lib/commonjs/validation/zodAdapter.js.map +1 -0
  55. package/lib/module/components/FieldFlowCheckbox/FieldFlowCheckbox.js +126 -0
  56. package/lib/module/components/FieldFlowCheckbox/FieldFlowCheckbox.js.map +1 -0
  57. package/lib/module/components/FieldFlowCheckbox/index.js +4 -0
  58. package/lib/module/components/FieldFlowCheckbox/index.js.map +1 -0
  59. package/lib/module/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js +531 -0
  60. package/lib/module/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.js.map +1 -0
  61. package/lib/module/components/FieldFlowDateTimePicker/index.js +4 -0
  62. package/lib/module/components/FieldFlowDateTimePicker/index.js.map +1 -0
  63. package/lib/module/components/FieldFlowDateTimePicker/types.js +4 -0
  64. package/lib/module/components/FieldFlowDateTimePicker/types.js.map +1 -0
  65. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js +24 -9
  66. package/lib/module/components/FieldFlowDropdown/FieldFlowDropdown.js.map +1 -1
  67. package/lib/module/components/FieldFlowInput/FieldFlowInput.js +59 -28
  68. package/lib/module/components/FieldFlowInput/FieldFlowInput.js.map +1 -1
  69. package/lib/module/components/FieldFlowInput/styles.js +32 -11
  70. package/lib/module/components/FieldFlowInput/styles.js.map +1 -1
  71. package/lib/module/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js +61 -0
  72. package/lib/module/components/FieldFlowMaskedInput/FieldFlowMaskedInput.js.map +1 -0
  73. package/lib/module/components/FieldFlowMaskedInput/index.js +4 -0
  74. package/lib/module/components/FieldFlowMaskedInput/index.js.map +1 -0
  75. package/lib/module/components/FieldFlowMaskedInput/types.js +4 -0
  76. package/lib/module/components/FieldFlowMaskedInput/types.js.map +1 -0
  77. package/lib/module/components/FieldFlowOTP/FieldFlowOTP.js +250 -0
  78. package/lib/module/components/FieldFlowOTP/FieldFlowOTP.js.map +1 -0
  79. package/lib/module/components/FieldFlowOTP/index.js +4 -0
  80. package/lib/module/components/FieldFlowOTP/index.js.map +1 -0
  81. package/lib/module/components/FieldFlowOTP/types.js +4 -0
  82. package/lib/module/components/FieldFlowOTP/types.js.map +1 -0
  83. package/lib/module/components/FieldFlowRadio/FieldFlowRadio.js +159 -0
  84. package/lib/module/components/FieldFlowRadio/FieldFlowRadio.js.map +1 -0
  85. package/lib/module/components/FieldFlowRadio/index.js +4 -0
  86. package/lib/module/components/FieldFlowRadio/index.js.map +1 -0
  87. package/lib/module/components/FieldFlowSwitch/FieldFlowSwitch.js +147 -0
  88. package/lib/module/components/FieldFlowSwitch/FieldFlowSwitch.js.map +1 -0
  89. package/lib/module/components/FieldFlowSwitch/index.js +4 -0
  90. package/lib/module/components/FieldFlowSwitch/index.js.map +1 -0
  91. package/lib/module/hooks/index.js.map +1 -1
  92. package/lib/module/hooks/useFieldFlowForm.js +147 -32
  93. package/lib/module/hooks/useFieldFlowForm.js.map +1 -1
  94. package/lib/module/icons/Icon.js +720 -31
  95. package/lib/module/icons/Icon.js.map +1 -1
  96. package/lib/module/icons/index.js +1 -0
  97. package/lib/module/icons/index.js.map +1 -1
  98. package/lib/module/icons/renderIcon.js +19 -0
  99. package/lib/module/icons/renderIcon.js.map +1 -0
  100. package/lib/module/index.js +11 -3
  101. package/lib/module/index.js.map +1 -1
  102. package/lib/module/utils/masks.js +118 -0
  103. package/lib/module/utils/masks.js.map +1 -0
  104. package/lib/module/validation/index.js +1 -0
  105. package/lib/module/validation/index.js.map +1 -1
  106. package/lib/module/validation/zodAdapter.js +84 -0
  107. package/lib/module/validation/zodAdapter.js.map +1 -0
  108. package/lib/typescript/components/FieldFlowCheckbox/FieldFlowCheckbox.d.ts +36 -0
  109. package/lib/typescript/components/FieldFlowCheckbox/FieldFlowCheckbox.d.ts.map +1 -0
  110. package/lib/typescript/components/FieldFlowCheckbox/index.d.ts +3 -0
  111. package/lib/typescript/components/FieldFlowCheckbox/index.d.ts.map +1 -0
  112. package/lib/typescript/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.d.ts +4 -0
  113. package/lib/typescript/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.d.ts.map +1 -0
  114. package/lib/typescript/components/FieldFlowDateTimePicker/index.d.ts +3 -0
  115. package/lib/typescript/components/FieldFlowDateTimePicker/index.d.ts.map +1 -0
  116. package/lib/typescript/components/FieldFlowDateTimePicker/types.d.ts +55 -0
  117. package/lib/typescript/components/FieldFlowDateTimePicker/types.d.ts.map +1 -0
  118. package/lib/typescript/components/FieldFlowDropdown/FieldFlowDropdown.d.ts.map +1 -1
  119. package/lib/typescript/components/FieldFlowDropdown/types.d.ts +7 -1
  120. package/lib/typescript/components/FieldFlowDropdown/types.d.ts.map +1 -1
  121. package/lib/typescript/components/FieldFlowInput/FieldFlowInput.d.ts.map +1 -1
  122. package/lib/typescript/components/FieldFlowInput/styles.d.ts +19 -2
  123. package/lib/typescript/components/FieldFlowInput/styles.d.ts.map +1 -1
  124. package/lib/typescript/components/FieldFlowInput/types.d.ts +24 -5
  125. package/lib/typescript/components/FieldFlowInput/types.d.ts.map +1 -1
  126. package/lib/typescript/components/FieldFlowMaskedInput/FieldFlowMaskedInput.d.ts +5 -0
  127. package/lib/typescript/components/FieldFlowMaskedInput/FieldFlowMaskedInput.d.ts.map +1 -0
  128. package/lib/typescript/components/FieldFlowMaskedInput/index.d.ts +3 -0
  129. package/lib/typescript/components/FieldFlowMaskedInput/index.d.ts.map +1 -0
  130. package/lib/typescript/components/FieldFlowMaskedInput/types.d.ts +20 -0
  131. package/lib/typescript/components/FieldFlowMaskedInput/types.d.ts.map +1 -0
  132. package/lib/typescript/components/FieldFlowOTP/FieldFlowOTP.d.ts +4 -0
  133. package/lib/typescript/components/FieldFlowOTP/FieldFlowOTP.d.ts.map +1 -0
  134. package/lib/typescript/components/FieldFlowOTP/index.d.ts +3 -0
  135. package/lib/typescript/components/FieldFlowOTP/index.d.ts.map +1 -0
  136. package/lib/typescript/components/FieldFlowOTP/types.d.ts +52 -0
  137. package/lib/typescript/components/FieldFlowOTP/types.d.ts.map +1 -0
  138. package/lib/typescript/components/FieldFlowRadio/FieldFlowRadio.d.ts +39 -0
  139. package/lib/typescript/components/FieldFlowRadio/FieldFlowRadio.d.ts.map +1 -0
  140. package/lib/typescript/components/FieldFlowRadio/index.d.ts +3 -0
  141. package/lib/typescript/components/FieldFlowRadio/index.d.ts.map +1 -0
  142. package/lib/typescript/components/FieldFlowSwitch/FieldFlowSwitch.d.ts +31 -0
  143. package/lib/typescript/components/FieldFlowSwitch/FieldFlowSwitch.d.ts.map +1 -0
  144. package/lib/typescript/components/FieldFlowSwitch/index.d.ts +3 -0
  145. package/lib/typescript/components/FieldFlowSwitch/index.d.ts.map +1 -0
  146. package/lib/typescript/hooks/index.d.ts +1 -1
  147. package/lib/typescript/hooks/index.d.ts.map +1 -1
  148. package/lib/typescript/hooks/useFieldFlowForm.d.ts +46 -7
  149. package/lib/typescript/hooks/useFieldFlowForm.d.ts.map +1 -1
  150. package/lib/typescript/icons/Icon.d.ts +2 -2
  151. package/lib/typescript/icons/Icon.d.ts.map +1 -1
  152. package/lib/typescript/icons/index.d.ts +2 -0
  153. package/lib/typescript/icons/index.d.ts.map +1 -1
  154. package/lib/typescript/icons/renderIcon.d.ts +6 -0
  155. package/lib/typescript/icons/renderIcon.d.ts.map +1 -0
  156. package/lib/typescript/index.d.ts +20 -6
  157. package/lib/typescript/index.d.ts.map +1 -1
  158. package/lib/typescript/types/index.d.ts +4 -0
  159. package/lib/typescript/types/index.d.ts.map +1 -1
  160. package/lib/typescript/utils/masks.d.ts +37 -0
  161. package/lib/typescript/utils/masks.d.ts.map +1 -0
  162. package/lib/typescript/validation/index.d.ts +2 -0
  163. package/lib/typescript/validation/index.d.ts.map +1 -1
  164. package/lib/typescript/validation/zodAdapter.d.ts +53 -0
  165. package/lib/typescript/validation/zodAdapter.d.ts.map +1 -0
  166. package/package.json +27 -5
  167. package/src/components/FieldFlowCheckbox/FieldFlowCheckbox.tsx +174 -0
  168. package/src/components/FieldFlowCheckbox/index.ts +2 -0
  169. package/src/components/FieldFlowDateTimePicker/FieldFlowDateTimePicker.tsx +585 -0
  170. package/src/components/FieldFlowDateTimePicker/index.ts +2 -0
  171. package/src/components/FieldFlowDateTimePicker/types.ts +57 -0
  172. package/src/components/FieldFlowDropdown/FieldFlowDropdown.tsx +17 -7
  173. package/src/components/FieldFlowDropdown/types.ts +8 -1
  174. package/src/components/FieldFlowInput/FieldFlowInput.tsx +69 -26
  175. package/src/components/FieldFlowInput/styles.ts +41 -6
  176. package/src/components/FieldFlowInput/types.ts +24 -5
  177. package/src/components/FieldFlowMaskedInput/FieldFlowMaskedInput.tsx +76 -0
  178. package/src/components/FieldFlowMaskedInput/index.ts +2 -0
  179. package/src/components/FieldFlowMaskedInput/types.ts +22 -0
  180. package/src/components/FieldFlowOTP/FieldFlowOTP.tsx +305 -0
  181. package/src/components/FieldFlowOTP/index.ts +2 -0
  182. package/src/components/FieldFlowOTP/types.ts +57 -0
  183. package/src/components/FieldFlowRadio/FieldFlowRadio.tsx +215 -0
  184. package/src/components/FieldFlowRadio/index.ts +2 -0
  185. package/src/components/FieldFlowSwitch/FieldFlowSwitch.tsx +172 -0
  186. package/src/components/FieldFlowSwitch/index.ts +2 -0
  187. package/src/hooks/index.ts +3 -0
  188. package/src/hooks/useFieldFlowForm.ts +209 -33
  189. package/src/icons/Icon.tsx +451 -5
  190. package/src/icons/index.ts +2 -0
  191. package/src/icons/renderIcon.tsx +17 -0
  192. package/src/index.ts +47 -4
  193. package/src/types/index.ts +5 -0
  194. package/src/utils/masks.ts +137 -0
  195. package/src/validation/index.ts +7 -0
  196. package/src/validation/zodAdapter.ts +103 -0
@@ -0,0 +1,172 @@
1
+ import React, { useEffect, useMemo, useRef } from 'react';
2
+ import { Animated, Pressable, StyleSheet, Text, View, ViewStyle, TextStyle } from 'react-native';
3
+ import { useFieldFlowTheme, mergeDeep } from '../../theme/ThemeProvider';
4
+ import { defaultTheme } from '../../theme/defaultTheme';
5
+ import { renderFieldFlowIcon, type FieldFlowIconSource } from '../../icons';
6
+ import type { DeepPartial, FieldFlowTheme } from '../../types';
7
+
8
+ export interface FieldFlowSwitchProps {
9
+ value: boolean;
10
+ onChange: (value: boolean) => void;
11
+ label?: string;
12
+ helperText?: string;
13
+ errorText?: string;
14
+ disabled?: boolean;
15
+ required?: boolean;
16
+ switchPosition?: 'left' | 'right';
17
+ leftIcon?: FieldFlowIconSource;
18
+ iconSize?: number;
19
+ /** Track color when on. */
20
+ onTrackColor?: string;
21
+ /** Track color when off. */
22
+ offTrackColor?: string;
23
+ thumbColor?: string;
24
+ trackStyle?: ViewStyle;
25
+ thumbStyle?: ViewStyle;
26
+ containerStyle?: ViewStyle;
27
+ labelStyle?: TextStyle;
28
+ helperStyle?: TextStyle;
29
+ errorStyle?: TextStyle;
30
+ theme?: DeepPartial<FieldFlowTheme>;
31
+ testID?: string;
32
+ }
33
+
34
+ export function FieldFlowSwitch({
35
+ value,
36
+ onChange,
37
+ label,
38
+ helperText,
39
+ errorText,
40
+ disabled,
41
+ required,
42
+ switchPosition = 'right',
43
+ leftIcon,
44
+ iconSize = 18,
45
+ onTrackColor,
46
+ offTrackColor,
47
+ thumbColor = '#FFFFFF',
48
+ trackStyle,
49
+ thumbStyle,
50
+ containerStyle,
51
+ labelStyle,
52
+ helperStyle,
53
+ errorStyle,
54
+ theme: themeOverride,
55
+ testID,
56
+ }: FieldFlowSwitchProps) {
57
+ const contextTheme = useFieldFlowTheme();
58
+ const theme = useMemo(
59
+ () => (themeOverride ? mergeDeep(contextTheme ?? defaultTheme, themeOverride) : contextTheme ?? defaultTheme),
60
+ [contextTheme, themeOverride],
61
+ );
62
+ const styles = useMemo(() => createStyles(theme, Boolean(disabled)), [theme, disabled]);
63
+
64
+ const anim = useRef(new Animated.Value(value ? 1 : 0)).current;
65
+ useEffect(() => {
66
+ Animated.timing(anim, {
67
+ toValue: value ? 1 : 0,
68
+ duration: theme.animation.duration,
69
+ useNativeDriver: false,
70
+ }).start();
71
+ }, [anim, theme.animation.duration, value]);
72
+
73
+ const trackColor = anim.interpolate({
74
+ inputRange: [0, 1],
75
+ outputRange: [offTrackColor ?? theme.colors.border, onTrackColor ?? theme.colors.borderFocused],
76
+ });
77
+ const thumbTranslate = anim.interpolate({
78
+ inputRange: [0, 1],
79
+ outputRange: [2, 22],
80
+ });
81
+
82
+ const switchNode = (
83
+ <Animated.View style={[styles.track, { backgroundColor: trackColor }, trackStyle]}>
84
+ <Animated.View
85
+ style={[styles.thumb, { backgroundColor: thumbColor, transform: [{ translateX: thumbTranslate }] }, thumbStyle]}
86
+ />
87
+ </Animated.View>
88
+ );
89
+
90
+ const labelNode = label ? (
91
+ <Text style={[styles.label, labelStyle]}>
92
+ {label}
93
+ {required ? ' *' : ''}
94
+ </Text>
95
+ ) : null;
96
+
97
+ return (
98
+ <View style={[styles.wrapper, containerStyle]} testID={testID}>
99
+ <Pressable
100
+ onPress={() => !disabled && onChange(!value)}
101
+ disabled={disabled}
102
+ style={styles.row}
103
+ accessibilityRole="switch"
104
+ accessibilityState={{ checked: value, disabled: Boolean(disabled) }}
105
+ accessibilityLabel={label}
106
+ hitSlop={6}
107
+ >
108
+ {leftIcon ? (
109
+ <View style={styles.leftIcon}>{renderFieldFlowIcon(leftIcon, theme.colors.icon, iconSize)}</View>
110
+ ) : null}
111
+ {switchPosition === 'left' ? switchNode : null}
112
+ {labelNode}
113
+ {switchPosition === 'right' ? switchNode : null}
114
+ </Pressable>
115
+ {errorText ? (
116
+ <Text style={[styles.errorText, errorStyle, helperStyle]}>{errorText}</Text>
117
+ ) : helperText ? (
118
+ <Text style={[styles.helperText, helperStyle]}>{helperText}</Text>
119
+ ) : null}
120
+ </View>
121
+ );
122
+ }
123
+
124
+ function createStyles(theme: FieldFlowTheme, disabled: boolean) {
125
+ return StyleSheet.create({
126
+ wrapper: {
127
+ marginBottom: theme.spacing.gap * 2,
128
+ opacity: disabled ? 0.6 : 1,
129
+ },
130
+ row: {
131
+ flexDirection: 'row',
132
+ alignItems: 'center',
133
+ justifyContent: 'space-between',
134
+ gap: theme.spacing.gap,
135
+ },
136
+ leftIcon: {
137
+ marginRight: 2,
138
+ },
139
+ label: {
140
+ flex: 1,
141
+ fontSize: theme.typography.fontSize,
142
+ color: theme.colors.text,
143
+ fontFamily: theme.typography.fontFamily,
144
+ },
145
+ track: {
146
+ width: 44,
147
+ height: 26,
148
+ borderRadius: 13,
149
+ justifyContent: 'center',
150
+ },
151
+ thumb: {
152
+ width: 20,
153
+ height: 20,
154
+ borderRadius: 10,
155
+ shadowColor: '#000',
156
+ shadowOpacity: 0.15,
157
+ shadowRadius: 2,
158
+ shadowOffset: { width: 0, height: 1 },
159
+ elevation: 2,
160
+ },
161
+ helperText: {
162
+ marginTop: theme.spacing.gap / 1.5,
163
+ fontSize: theme.typography.helperFontSize,
164
+ color: theme.colors.label,
165
+ },
166
+ errorText: {
167
+ marginTop: theme.spacing.gap / 1.5,
168
+ fontSize: theme.typography.helperFontSize,
169
+ color: theme.colors.error,
170
+ },
171
+ });
172
+ }
@@ -0,0 +1,2 @@
1
+ export { FieldFlowSwitch } from './FieldFlowSwitch';
2
+ export type { FieldFlowSwitchProps } from './FieldFlowSwitch';
@@ -6,6 +6,9 @@ export type {
6
6
  UseFieldFlowFormOptions,
7
7
  UseFieldFlowFormResult,
8
8
  FieldFlowFormRules,
9
+ AsyncFieldValidator,
10
+ AsyncFieldValidators,
11
+ CrossFieldRule,
9
12
  } from './useFieldFlowForm';
10
13
 
11
14
  export { useFieldState } from './useFieldState';
@@ -1,15 +1,59 @@
1
- import { useCallback, useMemo, useState } from 'react';
2
- import type { FieldFlowRules } from '../types';
1
+ import { useCallback, useMemo, useRef, useState } from 'react';
2
+ import type { FieldFlowRules, ValidationResult } from '../types';
3
3
  import { runRules } from '../validation/validators';
4
4
 
5
5
  export type FieldFlowFormRules<T extends Record<string, string>> = {
6
6
  [K in keyof T]?: FieldFlowRules;
7
7
  };
8
8
 
9
+ export type AsyncFieldValidator<T extends Record<string, string>> = (
10
+ value: string,
11
+ values: T,
12
+ ) => Promise<ValidationResult> | ValidationResult;
13
+
14
+ export type AsyncFieldValidators<T extends Record<string, string>> = {
15
+ [K in keyof T]?: AsyncFieldValidator<T>;
16
+ };
17
+
18
+ export interface CrossFieldRule<T extends Record<string, string>> {
19
+ /** Field that receives the error message when validation fails. */
20
+ target: keyof T;
21
+ /** When set, this rule re-runs when any of these fields change (in addition to `target`). */
22
+ deps?: (keyof T)[];
23
+ validate: (values: T) => ValidationResult;
24
+ }
25
+
9
26
  export interface UseFieldFlowFormOptions<T extends Record<string, string>> {
10
27
  initialValues: T;
11
28
  rules?: FieldFlowFormRules<T>;
12
- /** Called only when every field passes validation. */
29
+ /**
30
+ * Cross-field rules (e.g. password confirm). Run after per-field sync rules.
31
+ *
32
+ * @example
33
+ * crossFieldRules: [{
34
+ * target: 'confirm',
35
+ * deps: ['password'],
36
+ * validate: v => v.password === v.confirm
37
+ * ? { valid: true }
38
+ * : { valid: false, message: 'Passwords must match' },
39
+ * }]
40
+ */
41
+ crossFieldRules?: CrossFieldRule<T>[];
42
+ /**
43
+ * Whole-form validator (e.g. from `zodFormValidate(schema)`).
44
+ * Merged after sync + cross-field rules; field keys map to error messages.
45
+ */
46
+ formValidate?: (
47
+ values: T,
48
+ ) => Partial<Record<keyof T, string | undefined>> | Promise<Partial<Record<keyof T, string | undefined>>>;
49
+ /**
50
+ * Per-field async validators (e.g. "email already taken").
51
+ * Debounced on change after the field has been touched; always awaited on submit.
52
+ */
53
+ asyncValidators?: AsyncFieldValidators<T>;
54
+ /** Debounce for async field validators on change. Default: 400ms. */
55
+ asyncDebounceMs?: number;
56
+ /** Called only when every field passes sync + cross-field + form + async validation. */
13
57
  onSubmit?: (values: T) => void | Promise<void>;
14
58
  }
15
59
 
@@ -17,111 +61,243 @@ export interface UseFieldFlowFormResult<T extends Record<string, string>> {
17
61
  values: T;
18
62
  errors: Partial<Record<keyof T, string>>;
19
63
  touched: Partial<Record<keyof T, boolean>>;
64
+ /** Fields currently waiting on an async validator. */
65
+ validating: Partial<Record<keyof T, boolean>>;
20
66
  isSubmitting: boolean;
21
67
  isValid: boolean;
68
+ /** True while any async field validator is in flight. */
69
+ isValidating: boolean;
22
70
  setValue: (field: keyof T, value: string) => void;
23
71
  handleChange: (field: keyof T) => (value: string) => void;
24
72
  handleBlur: (field: keyof T) => () => void;
25
- validateField: (field: keyof T) => boolean;
26
- validateAll: () => boolean;
73
+ validateField: (field: keyof T) => Promise<boolean>;
74
+ validateAll: () => Promise<boolean>;
27
75
  submit: () => Promise<void>;
28
76
  reset: () => void;
29
77
  }
30
78
 
31
79
  /**
32
80
  * Drop-in form state manager for a set of FieldFlow inputs. Handles values,
33
- * per-field + whole-form validation (via the same `FieldFlowRules` used by
34
- * the input components), touched-state, and submission.
81
+ * per-field + cross-field + async validation, touched-state, and submission.
35
82
  */
36
83
  export function useFieldFlowForm<T extends Record<string, string>>({
37
84
  initialValues,
38
85
  rules,
86
+ crossFieldRules,
87
+ formValidate,
88
+ asyncValidators,
89
+ asyncDebounceMs = 400,
39
90
  onSubmit,
40
91
  }: UseFieldFlowFormOptions<T>): UseFieldFlowFormResult<T> {
41
92
  const [values, setValues] = useState<T>(initialValues);
42
93
  const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
43
94
  const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
95
+ const [validating, setValidating] = useState<Partial<Record<keyof T, boolean>>>({});
44
96
  const [isSubmitting, setIsSubmitting] = useState(false);
45
97
 
98
+ const valuesRef = useRef(values);
99
+ valuesRef.current = values;
100
+ const debounceTimers = useRef<Partial<Record<keyof T, ReturnType<typeof setTimeout>>>>({});
101
+ const asyncGen = useRef(0);
102
+
103
+ const syncErrorFor = useCallback(
104
+ (field: keyof T, fieldValue: string, allValues: T): string | undefined => {
105
+ const result = runRules(fieldValue, rules?.[field]);
106
+ if (!result.valid) return result.message;
107
+
108
+ if (crossFieldRules) {
109
+ for (const rule of crossFieldRules) {
110
+ if (rule.target !== field) continue;
111
+ const cross = rule.validate(allValues);
112
+ if (!cross.valid) return cross.message ?? 'Invalid';
113
+ }
114
+ }
115
+ return undefined;
116
+ },
117
+ [rules, crossFieldRules],
118
+ );
119
+
120
+ const runAsyncForField = useCallback(
121
+ async (field: keyof T, fieldValue: string, allValues: T): Promise<string | undefined> => {
122
+ const validator = asyncValidators?.[field];
123
+ if (!validator) return undefined;
124
+ setValidating(prev => ({ ...prev, [field]: true }));
125
+ try {
126
+ const result = await validator(fieldValue, allValues);
127
+ return result.valid ? undefined : result.message;
128
+ } finally {
129
+ setValidating(prev => ({ ...prev, [field]: false }));
130
+ }
131
+ },
132
+ [asyncValidators],
133
+ );
134
+
135
+ const scheduleAsync = useCallback(
136
+ (field: keyof T, fieldValue: string, allValues: T) => {
137
+ if (!asyncValidators?.[field]) return;
138
+ const existing = debounceTimers.current[field];
139
+ if (existing) clearTimeout(existing);
140
+
141
+ debounceTimers.current[field] = setTimeout(async () => {
142
+ const gen = ++asyncGen.current;
143
+ const message = await runAsyncForField(field, fieldValue, allValues);
144
+ if (gen !== asyncGen.current) return;
145
+ setErrors(prev => {
146
+ const syncMsg = syncErrorFor(field, fieldValue, allValues);
147
+ if (syncMsg) return { ...prev, [field]: syncMsg };
148
+ return { ...prev, [field]: message };
149
+ });
150
+ }, asyncDebounceMs);
151
+ },
152
+ [asyncDebounceMs, asyncValidators, runAsyncForField, syncErrorFor],
153
+ );
154
+
46
155
  const validateField = useCallback(
47
- (field: keyof T): boolean => {
48
- const fieldRules = rules?.[field];
49
- const result = runRules(values[field] ?? '', fieldRules);
50
- setErrors(prev => ({ ...prev, [field]: result.valid ? undefined : result.message }));
51
- return result.valid;
156
+ async (field: keyof T): Promise<boolean> => {
157
+ const allValues = valuesRef.current;
158
+ const fieldValue = allValues[field] ?? '';
159
+ const syncError = syncErrorFor(field, fieldValue, allValues);
160
+ if (syncError) {
161
+ setErrors(prev => ({ ...prev, [field]: syncError }));
162
+ return false;
163
+ }
164
+ const asyncError = await runAsyncForField(field, fieldValue, allValues);
165
+ setErrors(prev => ({ ...prev, [field]: asyncError }));
166
+ return !asyncError;
52
167
  },
53
- [rules, values],
168
+ [runAsyncForField, syncErrorFor],
54
169
  );
55
170
 
56
- const validateAll = useCallback((): boolean => {
57
- if (!rules) return true;
58
- let allValid = true;
171
+ const validateAll = useCallback(async (): Promise<boolean> => {
172
+ const allValues = valuesRef.current;
59
173
  const nextErrors: Partial<Record<keyof T, string>> = {};
60
- for (const key of Object.keys(rules) as (keyof T)[]) {
61
- const result = runRules(values[key] ?? '', rules[key]);
62
- if (!result.valid) {
63
- allValid = false;
64
- nextErrors[key] = result.message;
174
+ const keys = new Set<keyof T>([
175
+ ...(Object.keys(allValues) as (keyof T)[]),
176
+ ...(rules ? (Object.keys(rules) as (keyof T)[]) : []),
177
+ ...(crossFieldRules?.map(r => r.target) ?? []),
178
+ ...(asyncValidators ? (Object.keys(asyncValidators) as (keyof T)[]) : []),
179
+ ]);
180
+
181
+ for (const key of keys) {
182
+ const syncError = syncErrorFor(key, allValues[key] ?? '', allValues);
183
+ if (syncError) nextErrors[key] = syncError;
184
+ }
185
+
186
+ if (crossFieldRules) {
187
+ for (const rule of crossFieldRules) {
188
+ if (nextErrors[rule.target]) continue;
189
+ const result = rule.validate(allValues);
190
+ if (!result.valid) nextErrors[rule.target] = result.message ?? 'Invalid';
65
191
  }
66
192
  }
193
+
194
+ if (formValidate) {
195
+ const formErrors = await formValidate(allValues);
196
+ for (const key of Object.keys(formErrors) as (keyof T)[]) {
197
+ const msg = formErrors[key];
198
+ if (msg && !nextErrors[key]) nextErrors[key] = msg;
199
+ }
200
+ }
201
+
202
+ if (asyncValidators) {
203
+ await Promise.all(
204
+ (Object.keys(asyncValidators) as (keyof T)[]).map(async key => {
205
+ if (nextErrors[key]) return;
206
+ const asyncError = await runAsyncForField(key, allValues[key] ?? '', allValues);
207
+ if (asyncError) nextErrors[key] = asyncError;
208
+ }),
209
+ );
210
+ }
211
+
67
212
  setErrors(nextErrors);
68
213
  setTouched(prev => {
69
214
  const next = { ...prev };
70
- for (const key of Object.keys(rules)) next[key as keyof T] = true;
215
+ for (const key of keys) next[key] = true;
71
216
  return next;
72
217
  });
73
- return allValid;
74
- }, [rules, values]);
218
+ return Object.values(nextErrors).every(e => !e);
219
+ }, [rules, crossFieldRules, formValidate, asyncValidators, syncErrorFor, runAsyncForField]);
75
220
 
76
221
  const setValue = useCallback((field: keyof T, value: string) => {
77
- setValues(prev => ({ ...prev, [field]: value }));
222
+ setValues(prev => {
223
+ const next = { ...prev, [field]: value };
224
+ valuesRef.current = next;
225
+ return next;
226
+ });
78
227
  }, []);
79
228
 
80
229
  const handleChange = useCallback(
81
230
  (field: keyof T) => (value: string) => {
82
231
  setValue(field, value);
232
+ const allValues = { ...valuesRef.current, [field]: value };
233
+
83
234
  if (touched[field]) {
84
- // live-validate once the user has already blurred this field once
85
- const result = runRules(value, rules?.[field]);
86
- setErrors(prev => ({ ...prev, [field]: result.valid ? undefined : result.message }));
235
+ const syncError = syncErrorFor(field, value, allValues);
236
+ setErrors(prev => ({ ...prev, [field]: syncError }));
237
+ if (!syncError) scheduleAsync(field, value, allValues);
238
+ }
239
+
240
+ if (crossFieldRules) {
241
+ for (const rule of crossFieldRules) {
242
+ if (rule.target === field) continue;
243
+ if (rule.deps && !rule.deps.includes(field)) continue;
244
+ if (!touched[rule.target] && !touched[field]) continue;
245
+ const result = rule.validate(allValues);
246
+ setErrors(prev => ({
247
+ ...prev,
248
+ [rule.target]: result.valid
249
+ ? syncErrorFor(rule.target, allValues[rule.target] ?? '', allValues)
250
+ : result.message ?? 'Invalid',
251
+ }));
252
+ }
87
253
  }
88
254
  },
89
- [rules, setValue, touched],
255
+ [setValue, touched, syncErrorFor, scheduleAsync, crossFieldRules],
90
256
  );
91
257
 
92
258
  const handleBlur = useCallback(
93
259
  (field: keyof T) => () => {
94
260
  setTouched(prev => ({ ...prev, [field]: true }));
95
- validateField(field);
261
+ void validateField(field);
96
262
  },
97
263
  [validateField],
98
264
  );
99
265
 
100
266
  const submit = useCallback(async () => {
101
- const valid = validateAll();
267
+ const valid = await validateAll();
102
268
  if (!valid) return;
103
269
  setIsSubmitting(true);
104
270
  try {
105
- await onSubmit?.(values);
271
+ await onSubmit?.(valuesRef.current);
106
272
  } finally {
107
273
  setIsSubmitting(false);
108
274
  }
109
- }, [onSubmit, validateAll, values]);
275
+ }, [onSubmit, validateAll]);
110
276
 
111
277
  const reset = useCallback(() => {
278
+ asyncGen.current += 1;
279
+ for (const key of Object.keys(debounceTimers.current) as (keyof T)[]) {
280
+ const t = debounceTimers.current[key];
281
+ if (t) clearTimeout(t);
282
+ }
112
283
  setValues(initialValues);
284
+ valuesRef.current = initialValues;
113
285
  setErrors({});
114
286
  setTouched({});
287
+ setValidating({});
115
288
  }, [initialValues]);
116
289
 
117
290
  const isValid = useMemo(() => Object.values(errors).every(e => !e), [errors]);
291
+ const isValidating = useMemo(() => Object.values(validating).some(Boolean), [validating]);
118
292
 
119
293
  return {
120
294
  values,
121
295
  errors,
122
296
  touched,
297
+ validating,
123
298
  isSubmitting,
124
299
  isValid,
300
+ isValidating,
125
301
  setValue,
126
302
  handleChange,
127
303
  handleBlur,