@react-native-ama/forms 1.0.1-alpha.1 → 1.0.1

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.
@@ -5,6 +5,7 @@ export type FormProps = React.PropsWithChildren<{
5
5
  }>;
6
6
  export type FormActions = {
7
7
  focusFirstInvalidField: () => void;
8
+ focusFieldAt: (fieldNumber: number) => void;
8
9
  };
9
10
  export declare const Form: React.ForwardRefExoticComponent<Omit<FormProps, "ref"> & React.RefAttributes<FormActions>>;
10
11
  export type FormContextValue = {
@@ -21,4 +22,6 @@ export type FormRef = {
21
22
  isEditable?: boolean;
22
23
  };
23
24
  export declare const FormContext: React.Context<FormContextValue | null>;
24
- export declare const useForm: () => FormContextValue;
25
+ export declare const useForm: ({ suppressError }?: {
26
+ suppressError?: boolean | undefined;
27
+ }) => FormContextValue;
@@ -41,7 +41,7 @@ exports.Form = React.forwardRef(({ children, onSubmit }, ref) => {
41
41
  const refs = React.useRef([]);
42
42
  const { setFocus } = (0, core_1.useFocus)();
43
43
  const checks = __DEV__ ? core_1.useChecks === null || core_1.useChecks === void 0 ? void 0 : (0, core_1.useChecks)() : undefined;
44
- const focusField = (nextField) => {
44
+ const focusField = React.useCallback((nextField) => {
45
45
  var _a, _b, _c;
46
46
  /**
47
47
  * Refs passed as prop have another ".current"
@@ -80,38 +80,51 @@ exports.Form = React.forwardRef(({ children, onSubmit }, ref) => {
80
80
  else if (nextRefElement === null || nextRefElement === void 0 ? void 0 : nextRefElement.current) {
81
81
  setFocus(nextRefElement === null || nextRefElement === void 0 ? void 0 : nextRefElement.current);
82
82
  }
83
- };
84
- const submitForm = () => __awaiter(void 0, void 0, void 0, function* () {
85
- const isValid = yield onSubmit();
86
- if (isValid) {
87
- return;
88
- }
89
- focusFirstInvalidField();
90
- });
91
- const focusFirstInvalidField = () => {
83
+ }, [checks, setFocus]);
84
+ const focusFieldAt = React.useCallback((position) => {
92
85
  react_native_1.InteractionManager.runAfterInteractions(() => {
93
86
  setTimeout(() => {
94
- const fieldWithError = (refs.current || []).find(fieldRef => fieldRef.hasValidation && fieldRef.hasError);
95
- __DEV__ &&
96
- fieldWithError == null &&
97
- (checks === null || checks === void 0 ? void 0 : checks.logResult('Form', {
98
- message: 'The form validation has failed, but no component with error was found',
99
- rule: 'NO_UNDEFINED',
100
- }));
87
+ const fieldWithError = refs.current[position];
101
88
  focusField(fieldWithError);
102
89
  }, 0);
103
90
  });
104
- };
91
+ }, [focusField]);
92
+ const focusFirstInvalidField = React.useCallback(() => {
93
+ const fieldWithError = (refs.current || []).findIndex(fieldRef => fieldRef.hasValidation && fieldRef.hasError);
94
+ __DEV__ &&
95
+ fieldWithError == null &&
96
+ (checks === null || checks === void 0 ? void 0 : checks.logResult('Form', {
97
+ message: 'The form validation has failed, but no component with error was found',
98
+ rule: 'NO_UNDEFINED',
99
+ }));
100
+ focusFieldAt(fieldWithError);
101
+ }, [focusFieldAt, checks]);
102
+ const submitForm = React.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
103
+ const isValid = yield onSubmit();
104
+ if (isValid) {
105
+ return;
106
+ }
107
+ focusFirstInvalidField();
108
+ }), [focusFirstInvalidField, onSubmit]);
105
109
  React.useImperativeHandle(ref, () => ({
106
110
  focusFirstInvalidField,
111
+ focusFieldAt,
107
112
  }));
108
113
  return (<exports.FormContext.Provider value={{ refs: refs.current, submitForm, focusField }}>
109
114
  {children}
110
115
  </exports.FormContext.Provider>);
111
116
  });
112
117
  exports.FormContext = React.createContext(null);
113
- const useForm = () => {
118
+ const DEFAULT_CONTEXT_VALUE = {
119
+ refs: [],
120
+ submitForm: () => Promise.resolve(),
121
+ focusField: () => { },
122
+ };
123
+ const useForm = ({ suppressError } = { suppressError: false }) => {
114
124
  const context = React.useContext(exports.FormContext);
125
+ if (!context && suppressError) {
126
+ return DEFAULT_CONTEXT_VALUE; // return default values so internal useForm hooks don't throw undefined errors when user choose to suppress errors
127
+ }
115
128
  if (!context) {
116
129
  __DEV__ &&
117
130
  console.error('Please wrap your form field inside the <Form /> component');
@@ -12,8 +12,9 @@ export type UseFormField = {
12
12
  hasValidation: boolean;
13
13
  hasError?: boolean;
14
14
  errorMessage?: string;
15
+ suppressError?: boolean;
15
16
  };
16
- export declare const useFormField: ({ ref, hasFocusCallback, id, nextFieldId, nextFormFieldRef, hasValidation, hasError, accessibilityHint, errorMessage, editable, style, }: UseFormField) => {
17
+ export declare const useFormField: ({ ref, hasFocusCallback, id, nextFieldId, nextFormFieldRef, hasValidation, hasError, accessibilityHint, errorMessage, editable, suppressError, style, }: UseFormField) => {
17
18
  focusNextFormField: () => void;
18
19
  isLastField: () => boolean;
19
20
  style: any;
@@ -9,8 +9,8 @@ const internal_1 = require("@react-native-ama/internal");
9
9
  const react_1 = __importDefault(require("react"));
10
10
  const react_native_1 = require("react-native");
11
11
  const Form_1 = require("../components/Form");
12
- const useFormField = ({ ref, hasFocusCallback, id, nextFieldId, nextFormFieldRef, hasValidation, hasError, accessibilityHint, errorMessage, editable = true, style = {}, }) => {
13
- const { refs, submitForm, focusField } = (0, Form_1.useForm)();
12
+ const useFormField = ({ ref, hasFocusCallback, id, nextFieldId, nextFormFieldRef, hasValidation, hasError, accessibilityHint, errorMessage, editable = true, suppressError, style = {}, }) => {
13
+ const { refs, submitForm, focusField } = (0, Form_1.useForm)({ suppressError });
14
14
  const fieldRef = react_1.default.useRef(ref);
15
15
  const checks = __DEV__ ? core_1.useChecks === null || core_1.useChecks === void 0 ? void 0 : (0, core_1.useChecks)() : undefined;
16
16
  const getMyIndex = () => {
@@ -18,7 +18,7 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
18
18
  style: any;
19
19
  allowFontScaling?: boolean | undefined;
20
20
  autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined;
21
- autoComplete?: "off" | "email" | "password" | "tel" | "url" | "additional-name" | "address-line1" | "address-line2" | "birthdate-day" | "birthdate-full" | "birthdate-month" | "birthdate-year" | "cc-csc" | "cc-exp" | "cc-exp-day" | "cc-exp-month" | "cc-exp-year" | "cc-number" | "country" | "current-password" | "family-name" | "gender" | "given-name" | "honorific-prefix" | "honorific-suffix" | "name" | "name-family" | "name-given" | "name-middle" | "name-middle-initial" | "name-prefix" | "name-suffix" | "new-password" | "nickname" | "one-time-code" | "organization" | "organization-title" | "password-new" | "postal-address" | "postal-address-country" | "postal-address-extended" | "postal-address-extended-postal-code" | "postal-address-locality" | "postal-address-region" | "postal-code" | "street-address" | "sms-otp" | "tel-country-code" | "tel-national" | "tel-device" | "username" | "username-new" | undefined;
21
+ autoComplete?: "off" | "additional-name" | "address-line1" | "address-line2" | "birthdate-day" | "birthdate-full" | "birthdate-month" | "birthdate-year" | "cc-csc" | "cc-exp" | "cc-exp-day" | "cc-exp-month" | "cc-exp-year" | "cc-number" | "cc-name" | "cc-given-name" | "cc-middle-name" | "cc-family-name" | "cc-type" | "country" | "current-password" | "email" | "family-name" | "gender" | "given-name" | "honorific-prefix" | "honorific-suffix" | "name" | "name-family" | "name-given" | "name-middle" | "name-middle-initial" | "name-prefix" | "name-suffix" | "new-password" | "nickname" | "one-time-code" | "organization" | "organization-title" | "password" | "password-new" | "postal-address" | "postal-address-country" | "postal-address-extended" | "postal-address-extended-postal-code" | "postal-address-locality" | "postal-address-region" | "postal-code" | "street-address" | "sms-otp" | "tel" | "tel-country-code" | "tel-national" | "tel-device" | "url" | "username" | "username-new" | undefined;
22
22
  autoCorrect?: boolean | undefined;
23
23
  autoFocus?: boolean | undefined;
24
24
  blurOnSubmit?: boolean | undefined;
@@ -45,6 +45,7 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
45
45
  onKeyPress?: ((e: NativeSyntheticEvent<import("react-native").TextInputKeyPressEventData>) => void) | undefined;
46
46
  placeholder?: string | undefined;
47
47
  placeholderTextColor?: import("react-native").ColorValue | undefined;
48
+ readOnly?: boolean | undefined;
48
49
  returnKeyType?: ReturnKeyTypeOptions | undefined;
49
50
  enterKeyHint?: import("react-native").EnterKeyHintTypeOptions | undefined;
50
51
  secureTextEntry?: boolean | undefined;
@@ -152,9 +153,10 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
152
153
  rejectResponderTermination?: boolean | null | undefined;
153
154
  selectionState?: import("react-native").DocumentSelectionState | undefined;
154
155
  spellCheck?: boolean | undefined;
155
- textContentType?: "none" | "password" | "name" | "nickname" | "username" | "URL" | "addressCity" | "addressCityAndState" | "addressState" | "countryName" | "creditCardNumber" | "emailAddress" | "familyName" | "fullStreetAddress" | "givenName" | "jobTitle" | "location" | "middleName" | "namePrefix" | "nameSuffix" | "organizationName" | "postalCode" | "streetAddressLine1" | "streetAddressLine2" | "sublocality" | "telephoneNumber" | "newPassword" | "oneTimeCode" | undefined;
156
+ textContentType?: "none" | "name" | "nickname" | "password" | "username" | "URL" | "addressCity" | "addressCityAndState" | "addressState" | "countryName" | "creditCardNumber" | "creditCardExpiration" | "creditCardExpirationMonth" | "creditCardExpirationYear" | "creditCardSecurityCode" | "creditCardType" | "creditCardName" | "creditCardGivenName" | "creditCardMiddleName" | "creditCardFamilyName" | "emailAddress" | "familyName" | "fullStreetAddress" | "givenName" | "jobTitle" | "location" | "middleName" | "namePrefix" | "nameSuffix" | "organizationName" | "postalCode" | "streetAddressLine1" | "streetAddressLine2" | "sublocality" | "telephoneNumber" | "newPassword" | "oneTimeCode" | "birthdate" | "birthdateDay" | "birthdateMonth" | "birthdateYear" | undefined;
156
157
  scrollEnabled?: boolean | undefined;
157
158
  lineBreakStrategyIOS?: "none" | "standard" | "hangul-word" | "push-out" | undefined;
159
+ smartInsertDelete?: boolean | undefined;
158
160
  cursorColor?: import("react-native").ColorValue | null | undefined;
159
161
  importantForAutofill?: "auto" | "yes" | "no" | "noExcludeDescendants" | "yesExcludeDescendants" | undefined;
160
162
  disableFullscreenUI?: boolean | undefined;
@@ -166,7 +168,7 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
166
168
  underlineColorAndroid?: import("react-native").ColorValue | undefined;
167
169
  textAlignVertical?: "center" | "auto" | "top" | "bottom" | undefined;
168
170
  showSoftInputOnFocus?: boolean | undefined;
169
- verticalAlign?: "auto" | "middle" | "top" | "bottom" | undefined;
171
+ verticalAlign?: "auto" | "top" | "bottom" | "middle" | undefined;
170
172
  labelComponent?: JSX.Element | undefined;
171
173
  labelPosition?: "beforeInput" | "afterInput" | undefined;
172
174
  nextFormField?: React.RefObject<import("react-native").TextInput> | undefined;
@@ -179,11 +181,12 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
179
181
  focusNextFormField: () => void;
180
182
  isLastField: () => boolean;
181
183
  ref?: React.RefObject<any> | React.ForwardedRef<any> | undefined;
184
+ suppressError?: boolean | undefined;
182
185
  nextFormFieldRef?: React.RefObject<any> | undefined;
183
186
  } | {
184
187
  allowFontScaling?: boolean | undefined;
185
188
  autoCapitalize?: "none" | "sentences" | "words" | "characters" | undefined;
186
- autoComplete?: "off" | "email" | "password" | "tel" | "url" | "additional-name" | "address-line1" | "address-line2" | "birthdate-day" | "birthdate-full" | "birthdate-month" | "birthdate-year" | "cc-csc" | "cc-exp" | "cc-exp-day" | "cc-exp-month" | "cc-exp-year" | "cc-number" | "country" | "current-password" | "family-name" | "gender" | "given-name" | "honorific-prefix" | "honorific-suffix" | "name" | "name-family" | "name-given" | "name-middle" | "name-middle-initial" | "name-prefix" | "name-suffix" | "new-password" | "nickname" | "one-time-code" | "organization" | "organization-title" | "password-new" | "postal-address" | "postal-address-country" | "postal-address-extended" | "postal-address-extended-postal-code" | "postal-address-locality" | "postal-address-region" | "postal-code" | "street-address" | "sms-otp" | "tel-country-code" | "tel-national" | "tel-device" | "username" | "username-new" | undefined;
189
+ autoComplete?: "off" | "additional-name" | "address-line1" | "address-line2" | "birthdate-day" | "birthdate-full" | "birthdate-month" | "birthdate-year" | "cc-csc" | "cc-exp" | "cc-exp-day" | "cc-exp-month" | "cc-exp-year" | "cc-number" | "cc-name" | "cc-given-name" | "cc-middle-name" | "cc-family-name" | "cc-type" | "country" | "current-password" | "email" | "family-name" | "gender" | "given-name" | "honorific-prefix" | "honorific-suffix" | "name" | "name-family" | "name-given" | "name-middle" | "name-middle-initial" | "name-prefix" | "name-suffix" | "new-password" | "nickname" | "one-time-code" | "organization" | "organization-title" | "password" | "password-new" | "postal-address" | "postal-address-country" | "postal-address-extended" | "postal-address-extended-postal-code" | "postal-address-locality" | "postal-address-region" | "postal-code" | "street-address" | "sms-otp" | "tel" | "tel-country-code" | "tel-national" | "tel-device" | "url" | "username" | "username-new" | undefined;
187
190
  autoCorrect?: boolean | undefined;
188
191
  autoFocus?: boolean | undefined;
189
192
  blurOnSubmit?: boolean | undefined;
@@ -210,6 +213,7 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
210
213
  onKeyPress?: ((e: NativeSyntheticEvent<import("react-native").TextInputKeyPressEventData>) => void) | undefined;
211
214
  placeholder?: string | undefined;
212
215
  placeholderTextColor?: import("react-native").ColorValue | undefined;
216
+ readOnly?: boolean | undefined;
213
217
  returnKeyType?: ReturnKeyTypeOptions | undefined;
214
218
  enterKeyHint?: import("react-native").EnterKeyHintTypeOptions | undefined;
215
219
  secureTextEntry?: boolean | undefined;
@@ -318,9 +322,10 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
318
322
  rejectResponderTermination?: boolean | null | undefined;
319
323
  selectionState?: import("react-native").DocumentSelectionState | undefined;
320
324
  spellCheck?: boolean | undefined;
321
- textContentType?: "none" | "password" | "name" | "nickname" | "username" | "URL" | "addressCity" | "addressCityAndState" | "addressState" | "countryName" | "creditCardNumber" | "emailAddress" | "familyName" | "fullStreetAddress" | "givenName" | "jobTitle" | "location" | "middleName" | "namePrefix" | "nameSuffix" | "organizationName" | "postalCode" | "streetAddressLine1" | "streetAddressLine2" | "sublocality" | "telephoneNumber" | "newPassword" | "oneTimeCode" | undefined;
325
+ textContentType?: "none" | "name" | "nickname" | "password" | "username" | "URL" | "addressCity" | "addressCityAndState" | "addressState" | "countryName" | "creditCardNumber" | "creditCardExpiration" | "creditCardExpirationMonth" | "creditCardExpirationYear" | "creditCardSecurityCode" | "creditCardType" | "creditCardName" | "creditCardGivenName" | "creditCardMiddleName" | "creditCardFamilyName" | "emailAddress" | "familyName" | "fullStreetAddress" | "givenName" | "jobTitle" | "location" | "middleName" | "namePrefix" | "nameSuffix" | "organizationName" | "postalCode" | "streetAddressLine1" | "streetAddressLine2" | "sublocality" | "telephoneNumber" | "newPassword" | "oneTimeCode" | "birthdate" | "birthdateDay" | "birthdateMonth" | "birthdateYear" | undefined;
322
326
  scrollEnabled?: boolean | undefined;
323
327
  lineBreakStrategyIOS?: "none" | "standard" | "hangul-word" | "push-out" | undefined;
328
+ smartInsertDelete?: boolean | undefined;
324
329
  cursorColor?: import("react-native").ColorValue | null | undefined;
325
330
  importantForAutofill?: "auto" | "yes" | "no" | "noExcludeDescendants" | "yesExcludeDescendants" | undefined;
326
331
  disableFullscreenUI?: boolean | undefined;
@@ -332,7 +337,7 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
332
337
  underlineColorAndroid?: import("react-native").ColorValue | undefined;
333
338
  textAlignVertical?: "center" | "auto" | "top" | "bottom" | undefined;
334
339
  showSoftInputOnFocus?: boolean | undefined;
335
- verticalAlign?: "auto" | "middle" | "top" | "bottom" | undefined;
340
+ verticalAlign?: "auto" | "top" | "bottom" | "middle" | undefined;
336
341
  labelComponent?: JSX.Element | undefined;
337
342
  labelPosition?: "beforeInput" | "afterInput" | undefined;
338
343
  nextFormField?: React.RefObject<import("react-native").TextInput> | undefined;
@@ -345,6 +350,7 @@ export declare const useTextInput: ({ onLayout, returnKeyType, onSubmitEditing,
345
350
  focusNextFormField: () => void;
346
351
  isLastField: () => boolean;
347
352
  ref?: React.RefObject<any> | React.ForwardedRef<any> | undefined;
353
+ suppressError?: boolean | undefined;
348
354
  nextFormFieldRef?: React.RefObject<any> | undefined;
349
355
  };
350
356
  export {};
package/dist/index.d.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  /// <reference types="react" />
2
+ import { isFocused } from '@react-native-ama/internal';
2
3
  import { FormActions, FormProps } from './components/Form';
3
4
  import { FormField, FormFieldProps } from './components/FormField';
4
5
  import { FormSubmit, FormSubmitProps } from './components/FormSubmit';
5
- import { FormSwitchProps } from './components/FormSwitch';
6
+ import { FormSwitch, FormSwitchProps } from './components/FormSwitch';
6
7
  type FormComponent = React.FunctionComponent<FormProps> & {
7
8
  Submit: (props: FormSubmitProps) => JSX.Element;
8
9
  Field: React.FunctionComponent<FormFieldProps>;
9
10
  Switch: (props: FormSwitchProps) => JSX.Element;
10
11
  };
11
12
  declare const Form: FormComponent;
12
- export { Form, FormField, FormSubmit };
13
+ export { Form, FormField, FormSubmit, FormSwitch };
13
14
  export { TextInput } from './components/TextInput';
14
15
  export { useFormField } from './hooks/useFormField';
15
16
  export { useTextInput } from './hooks/useTextInput';
17
+ export { isFocused };
16
18
  export { type FormProps, type FormActions, type FormFieldProps, type FormSubmitProps, type FormSwitchProps, };
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useTextInput = exports.useFormField = exports.TextInput = exports.FormSubmit = exports.FormField = exports.Form = void 0;
4
2
  // Components
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.isFocused = exports.useTextInput = exports.useFormField = exports.TextInput = exports.FormSwitch = exports.FormSubmit = exports.FormField = exports.Form = void 0;
5
+ const internal_1 = require("@react-native-ama/internal");
6
+ Object.defineProperty(exports, "isFocused", { enumerable: true, get: function () { return internal_1.isFocused; } });
5
7
  const Form_1 = require("./components/Form");
6
8
  const FormField_1 = require("./components/FormField");
7
9
  Object.defineProperty(exports, "FormField", { enumerable: true, get: function () { return FormField_1.FormField; } });
8
10
  const FormSubmit_1 = require("./components/FormSubmit");
9
11
  Object.defineProperty(exports, "FormSubmit", { enumerable: true, get: function () { return FormSubmit_1.FormSubmit; } });
10
12
  const FormSwitch_1 = require("./components/FormSwitch");
13
+ Object.defineProperty(exports, "FormSwitch", { enumerable: true, get: function () { return FormSwitch_1.FormSwitch; } });
11
14
  // @ts-ignore
12
15
  const Form = Form_1.Form;
13
16
  exports.Form = Form;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@react-native-ama/forms",
3
- "version": "1.0.1-alpha.1",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "react-native": "src/index",
6
6
  "types": "dist/index.d.ts",
@@ -24,9 +24,9 @@
24
24
  "test": "jest"
25
25
  },
26
26
  "dependencies": {
27
- "@react-native-ama/core": "*",
28
- "@react-native-ama/internal": "*",
29
- "@react-native-ama/react-native": "*"
27
+ "@react-native-ama/core": "~1.0.1",
28
+ "@react-native-ama/internal": "~1.0.1",
29
+ "@react-native-ama/react-native": "~1.0.1"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "*",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "homepage": "https://github.com/FormidableLabs/react-native-ama#readme",
49
49
  "publishConfig": {
50
- "provenance": false
50
+ "provenance": true
51
51
  },
52
52
  "author": "",
53
53
  "license": "MIT",
@@ -5,11 +5,12 @@ import { InteractionManager } from 'react-native';
5
5
 
6
6
  export type FormProps = React.PropsWithChildren<{
7
7
  onSubmit: () => boolean | Promise<boolean>;
8
- ref?: React.RefObject<FormActions>;
8
+ ref?: React.RefObject<FormActions>; // need to explicitly type for inference in <Form /> component
9
9
  }>;
10
10
 
11
11
  export type FormActions = {
12
12
  focusFirstInvalidField: () => void;
13
+ focusFieldAt: (fieldNumber: number) => void;
13
14
  };
14
15
 
15
16
  export const Form = React.forwardRef<FormActions, FormProps>(
@@ -19,51 +20,83 @@ export const Form = React.forwardRef<FormActions, FormProps>(
19
20
 
20
21
  const checks = __DEV__ ? useChecks?.() : undefined;
21
22
 
22
- const focusField = (nextField?: Partial<FormRef> | undefined) => {
23
- /**
24
- * Refs passed as prop have another ".current"
25
- */
26
- const nextRefElement = nextField?.ref?.current?.current
27
- ? nextField?.ref.current
28
- : nextField?.ref;
23
+ const focusField = React.useCallback(
24
+ (nextField?: Partial<FormRef> | undefined) => {
25
+ /**
26
+ * Refs passed as prop have another ".current"
27
+ */
28
+ const nextRefElement = nextField?.ref?.current?.current
29
+ ? nextField?.ref.current
30
+ : nextField?.ref;
31
+
32
+ const callFocus =
33
+ // @ts-ignore
34
+ nextRefElement?.current?.focus &&
35
+ nextField?.hasFocusCallback &&
36
+ nextField?.isEditable;
37
+
38
+ __DEV__ &&
39
+ nextRefElement == null &&
40
+ checks?.logResult('nextRefElement', {
41
+ message:
42
+ 'No next field found. Make sure you wrapped your form inside the <Form /> component',
43
+ rule: 'NO_UNDEFINED',
44
+ });
45
+
46
+ if (callFocus) {
47
+ /**
48
+ * On some apps, if we call focus immediately and the field is already focused we lose the focus.
49
+ * Same happens if we do not call `focus` if the field is already focused.
50
+ */
51
+ const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;
52
+
53
+ setTimeout(() => {
54
+ nextRefElement?.current?.focus();
55
+
56
+ __DEV__ &&
57
+ nextRefElement &&
58
+ checks?.checkFocusTrap({
59
+ ref: nextRefElement,
60
+ shouldHaveFocus: true,
61
+ });
62
+ }, timeoutValue);
63
+ } else if (nextRefElement?.current) {
64
+ setFocus(nextRefElement?.current);
65
+ }
66
+ },
67
+ [checks, setFocus],
68
+ );
29
69
 
30
- const callFocus =
31
- // @ts-ignore
32
- nextRefElement?.current?.focus &&
33
- nextField?.hasFocusCallback &&
34
- nextField?.isEditable;
70
+ const focusFieldAt = React.useCallback(
71
+ (position: number) => {
72
+ InteractionManager.runAfterInteractions(() => {
73
+ setTimeout(() => {
74
+ const fieldWithError = refs.current[position];
75
+
76
+ focusField(fieldWithError);
77
+ }, 0);
78
+ });
79
+ },
80
+ [focusField],
81
+ );
82
+
83
+ const focusFirstInvalidField = React.useCallback(() => {
84
+ const fieldWithError = (refs.current || []).findIndex(
85
+ fieldRef => fieldRef.hasValidation && fieldRef.hasError,
86
+ );
35
87
 
36
88
  __DEV__ &&
37
- nextRefElement == null &&
38
- checks?.logResult('nextRefElement', {
89
+ fieldWithError == null &&
90
+ checks?.logResult('Form', {
39
91
  message:
40
- 'No next field found. Make sure you wrapped your form inside the <Form /> component',
92
+ 'The form validation has failed, but no component with error was found',
41
93
  rule: 'NO_UNDEFINED',
42
94
  });
43
95
 
44
- if (callFocus) {
45
- /**
46
- * On some apps, if we call focus immediately and the field is already focused we lose the focus.
47
- * Same happens if we do not call `focus` if the field is already focused.
48
- */
49
- const timeoutValue = isFocused(nextRefElement.current) ? 50 : 0;
50
-
51
- setTimeout(() => {
52
- nextRefElement?.current?.focus();
53
-
54
- __DEV__ &&
55
- nextRefElement &&
56
- checks?.checkFocusTrap({
57
- ref: nextRefElement,
58
- shouldHaveFocus: true,
59
- });
60
- }, timeoutValue);
61
- } else if (nextRefElement?.current) {
62
- setFocus(nextRefElement?.current);
63
- }
64
- };
96
+ focusFieldAt(fieldWithError);
97
+ }, [focusFieldAt, checks]);
65
98
 
66
- const submitForm = async () => {
99
+ const submitForm = React.useCallback(async () => {
67
100
  const isValid = await onSubmit();
68
101
 
69
102
  if (isValid) {
@@ -71,30 +104,11 @@ export const Form = React.forwardRef<FormActions, FormProps>(
71
104
  }
72
105
 
73
106
  focusFirstInvalidField();
74
- };
75
-
76
- const focusFirstInvalidField = () => {
77
- InteractionManager.runAfterInteractions(() => {
78
- setTimeout(() => {
79
- const fieldWithError = (refs.current || []).find(
80
- fieldRef => fieldRef.hasValidation && fieldRef.hasError,
81
- );
82
-
83
- __DEV__ &&
84
- fieldWithError == null &&
85
- checks?.logResult('Form', {
86
- message:
87
- 'The form validation has failed, but no component with error was found',
88
- rule: 'NO_UNDEFINED',
89
- });
90
-
91
- focusField(fieldWithError);
92
- }, 0);
93
- });
94
- };
107
+ }, [focusFirstInvalidField, onSubmit]);
95
108
 
96
109
  React.useImperativeHandle(ref, () => ({
97
110
  focusFirstInvalidField,
111
+ focusFieldAt,
98
112
  }));
99
113
 
100
114
  return (
@@ -123,8 +137,21 @@ export type FormRef = {
123
137
 
124
138
  export const FormContext = React.createContext<FormContextValue | null>(null);
125
139
 
126
- export const useForm = () => {
140
+ const DEFAULT_CONTEXT_VALUE: FormContextValue = {
141
+ refs: [],
142
+ submitForm: () => Promise.resolve(),
143
+ focusField: () => {},
144
+ };
145
+
146
+ export const useForm = (
147
+ { suppressError }: { suppressError?: boolean } = { suppressError: false },
148
+ ) => {
127
149
  const context = React.useContext(FormContext);
150
+
151
+ if (!context && suppressError) {
152
+ return DEFAULT_CONTEXT_VALUE; // return default values so internal useForm hooks don't throw undefined errors when user choose to suppress errors
153
+ }
154
+
128
155
  if (!context) {
129
156
  __DEV__ &&
130
157
  console.error(
@@ -134,5 +161,6 @@ export const useForm = () => {
134
161
  'useForm must be used within a FormContextProvider, please wrap your form field inside the <Form /> component',
135
162
  );
136
163
  }
164
+
137
165
  return context;
138
166
  };
@@ -32,7 +32,7 @@ describe('FormSubmit', () => {
32
32
  );
33
33
  });
34
34
 
35
- it('hides the children from the accessiblity tree', () => {
35
+ it('hides the children from the accessibility tree', () => {
36
36
  const { getByTestId } = render(
37
37
  <FormSubmit
38
38
  accessibilityLabel="This is the label"
@@ -17,6 +17,7 @@ export type UseFormField = {
17
17
  hasValidation: boolean;
18
18
  hasError?: boolean;
19
19
  errorMessage?: string;
20
+ suppressError?: boolean;
20
21
  };
21
22
 
22
23
  export const useFormField = ({
@@ -30,9 +31,10 @@ export const useFormField = ({
30
31
  accessibilityHint,
31
32
  errorMessage,
32
33
  editable = true,
34
+ suppressError,
33
35
  style = {},
34
36
  }: UseFormField) => {
35
- const { refs, submitForm, focusField } = useForm();
37
+ const { refs, submitForm, focusField } = useForm({ suppressError });
36
38
  const fieldRef = React.useRef(ref);
37
39
 
38
40
  const checks = __DEV__ ? useChecks?.() : undefined;
package/src/index.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  // Components
2
+
3
+ import { isFocused } from '@react-native-ama/internal';
4
+
2
5
  import {
3
6
  FormActions,
4
7
  FormProps,
@@ -20,7 +23,7 @@ Form.Submit = FormSubmit;
20
23
  Form.Field = FormField;
21
24
  Form.Switch = FormSwitch;
22
25
 
23
- export { Form, FormField, FormSubmit };
26
+ export { Form, FormField, FormSubmit, FormSwitch };
24
27
 
25
28
  // Components
26
29
  export { TextInput } from './components/TextInput';
@@ -29,6 +32,9 @@ export { TextInput } from './components/TextInput';
29
32
  export { useFormField } from './hooks/useFormField';
30
33
  export { useTextInput } from './hooks/useTextInput';
31
34
 
35
+ // utils
36
+ export { isFocused };
37
+
32
38
  // Types
33
39
  export {
34
40
  type FormProps,