envoc-form 4.0.1-1 → 4.0.1-11
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/README.md +2 -2
- package/es/AddressInput/AddressInput.d.ts +5 -5
- package/es/ConfirmBaseForm/ConfirmBaseForm.d.ts +3 -1
- package/es/ConfirmBaseForm/ConfirmBaseForm.js +3 -2
- package/es/ConfirmDeleteForm/ConfirmDeleteForm.js +2 -1
- package/es/DatePicker/DatePickerGroup.d.ts +3 -2
- package/es/DatePicker/DatePickerGroup.js +27 -5
- package/es/DatePicker/StringDateOnlyPickerGroup.d.ts +5 -0
- package/es/DatePicker/{DateOnlyDatePickerGroup.js → StringDateOnlyPickerGroup.js} +7 -6
- package/es/DatePicker/StringDatePickerGroup.d.ts +1 -1
- package/es/Field/Field.d.ts +3 -3
- package/es/Field/Field.js +9 -4
- package/es/FieldArray/FieldArray.d.ts +2 -2
- package/es/FieldArray/FieldArray.js +10 -7
- package/es/File/FileGroup.d.ts +1 -1
- package/es/File/FileGroup.js +5 -3
- package/es/File/FileList.d.ts +2 -2
- package/es/File/FileList.js +2 -1
- package/es/Form/Form.d.ts +8 -3
- package/es/Form/Form.js +32 -3
- package/es/Form/FormBasedPreventNavigation.js +31 -14
- package/es/FormActions.js +5 -1
- package/es/FormDefaults.d.ts +3 -0
- package/es/FormDefaults.js +1 -0
- package/es/Group.d.ts +2 -1
- package/es/Group.js +8 -5
- package/es/Input/IconInputGroup.d.ts +1 -1
- package/es/Input/IconInputGroup.js +3 -1
- package/es/Input/InputGroup.d.ts +3 -3
- package/es/Input/InputGroup.js +3 -2
- package/es/Input/MoneyInputGroup.d.ts +1 -1
- package/es/Input/MoneyInputGroup.js +2 -1
- package/es/Input/NumberInputGroup.d.ts +1 -1
- package/es/Input/NumberInputGroup.js +2 -1
- package/es/Input/StringInputGroup.d.ts +1 -1
- package/es/Input/StringInputGroup.js +3 -1
- package/es/Normalization/normalizers.d.ts +2 -2
- package/es/Select/BooleanSelectGroup.d.ts +1 -1
- package/es/Select/NumberSelectGroup.d.ts +2 -2
- package/es/Select/SelectGroup.d.ts +2 -2
- package/es/Select/SelectGroup.js +8 -3
- package/es/StandardFormActions.js +2 -1
- package/es/SubmitFormButton.d.ts +1 -1
- package/es/SubmitFormButton.js +4 -2
- package/es/TextArea/TextAreaGroup.d.ts +1 -1
- package/es/TextArea/TextAreaGroup.js +4 -2
- package/es/Validation/validators.d.ts +8 -8
- package/es/Validation/validators.js +3 -0
- package/es/__Tests__/FormTestBase.d.ts +3 -3
- package/es/__Tests__/FormTestBase.js +1 -1
- package/es/index.d.ts +8 -6
- package/es/index.js +4 -1
- package/lib/AddressInput/AddressInput.d.ts +5 -5
- package/lib/ConfirmBaseForm/ConfirmBaseForm.d.ts +3 -1
- package/lib/ConfirmBaseForm/ConfirmBaseForm.js +3 -2
- package/lib/ConfirmDeleteForm/ConfirmDeleteForm.js +2 -1
- package/lib/DatePicker/DatePickerGroup.d.ts +3 -2
- package/lib/DatePicker/DatePickerGroup.js +28 -4
- package/lib/DatePicker/StringDateOnlyPickerGroup.d.ts +5 -0
- package/lib/DatePicker/{DateOnlyDatePickerGroup.js → StringDateOnlyPickerGroup.js} +8 -7
- package/lib/DatePicker/StringDatePickerGroup.d.ts +1 -1
- package/lib/Field/Field.d.ts +3 -3
- package/lib/Field/Field.js +9 -4
- package/lib/FieldArray/FieldArray.d.ts +2 -2
- package/lib/FieldArray/FieldArray.js +10 -7
- package/lib/File/FileGroup.d.ts +1 -1
- package/lib/File/FileGroup.js +5 -3
- package/lib/File/FileList.d.ts +2 -2
- package/lib/File/FileList.js +2 -1
- package/lib/Form/Form.d.ts +8 -3
- package/lib/Form/Form.js +32 -3
- package/lib/Form/FormBasedPreventNavigation.js +31 -14
- package/lib/FormActions.js +5 -1
- package/lib/FormDefaults.d.ts +3 -0
- package/lib/FormDefaults.js +4 -0
- package/lib/Group.d.ts +2 -1
- package/lib/Group.js +8 -5
- package/lib/Input/IconInputGroup.d.ts +1 -1
- package/lib/Input/IconInputGroup.js +3 -1
- package/lib/Input/InputGroup.d.ts +3 -3
- package/lib/Input/InputGroup.js +3 -2
- package/lib/Input/MoneyInputGroup.d.ts +1 -1
- package/lib/Input/MoneyInputGroup.js +2 -1
- package/lib/Input/NumberInputGroup.d.ts +1 -1
- package/lib/Input/NumberInputGroup.js +2 -1
- package/lib/Input/StringInputGroup.d.ts +1 -1
- package/lib/Input/StringInputGroup.js +3 -1
- package/lib/Normalization/normalizers.d.ts +2 -2
- package/lib/Select/BooleanSelectGroup.d.ts +1 -1
- package/lib/Select/NumberSelectGroup.d.ts +2 -2
- package/lib/Select/SelectGroup.d.ts +2 -2
- package/lib/Select/SelectGroup.js +8 -3
- package/lib/StandardFormActions.js +2 -1
- package/lib/SubmitFormButton.d.ts +1 -1
- package/lib/SubmitFormButton.js +4 -2
- package/lib/TextArea/TextAreaGroup.d.ts +1 -1
- package/lib/TextArea/TextAreaGroup.js +4 -2
- package/lib/Validation/validators.d.ts +8 -8
- package/lib/Validation/validators.js +3 -0
- package/lib/__Tests__/FormTestBase.d.ts +3 -3
- package/lib/__Tests__/FormTestBase.js +2 -2
- package/lib/index.d.ts +8 -6
- package/lib/index.js +8 -3
- package/package.json +4 -2
- package/src/AddressInput/AddressInput.tsx +5 -5
- package/src/AddressInput/__snapshots__/AddressInput.test.tsx.snap +15 -10
- package/src/ConfirmBaseForm/ConfirmBaseForm.tsx +13 -3
- package/src/ConfirmBaseForm/__snapshots__/ConfirmBaseForm.test.tsx.snap +3 -3
- package/src/ConfirmDeleteForm/ConfirmDeleteForm.tsx +8 -1
- package/src/ConfirmDeleteForm/__snapshots__/ConfirmDeleteForm.test.tsx.snap +2 -2
- package/src/DatePicker/DatePicker.test.tsx +3 -3
- package/src/DatePicker/DatePickerGroup.tsx +45 -8
- package/src/DatePicker/StringDateOnlyPickerGroup.tsx +23 -0
- package/src/DatePicker/StringDatePickerGroup.tsx +1 -1
- package/src/DatePicker/__snapshots__/DatePicker.test.tsx.snap +3 -2
- package/src/Field/Field.tsx +22 -7
- package/src/FieldArray/FieldArray.tsx +25 -13
- package/src/File/FileGroup.tsx +15 -3
- package/src/File/FileList.tsx +5 -3
- package/src/File/__snapshots__/FileGroup.test.tsx.snap +5 -3
- package/src/Form/Form.tsx +56 -4
- package/src/Form/FormBasedPreventNavigation.tsx +34 -18
- package/src/Form/__snapshots__/Form.test.tsx.snap +1 -0
- package/src/FormActions.tsx +8 -2
- package/src/FormDefaults.ts +1 -0
- package/src/Group.tsx +21 -6
- package/src/Input/IconInputGroup.tsx +7 -4
- package/src/Input/InputGroup.tsx +18 -5
- package/src/Input/MoneyInputGroup.tsx +6 -2
- package/src/Input/NumberInputGroup.tsx +6 -2
- package/src/Input/StringInputGroup.tsx +7 -3
- package/src/Input/__Tests__/__snapshots__/IconInputGroup.test.tsx.snap +4 -2
- package/src/Input/__Tests__/__snapshots__/MoneyInputGroup.test.tsx.snap +4 -2
- package/src/Input/__Tests__/__snapshots__/NumberInputGroup.test.tsx.snap +4 -2
- package/src/Input/__Tests__/__snapshots__/StringInputGroup.test.tsx.snap +4 -2
- package/src/Normalization/normalizers.ts +2 -2
- package/src/Select/BooleanSelectGroup.tsx +1 -1
- package/src/Select/NumberSelectGroup.tsx +2 -2
- package/src/Select/SelectGroup.tsx +16 -4
- package/src/Select/__tests__/__snapshots__/BooleanSelectGroup.test.tsx.snap +3 -2
- package/src/Select/__tests__/__snapshots__/NumberSelectGroup.test.tsx.snap +6 -4
- package/src/Select/__tests__/__snapshots__/StringSelectGroup.test.tsx.snap +6 -4
- package/src/StandardFormActions.tsx +4 -1
- package/src/SubmitFormButton.tsx +9 -2
- package/src/TextArea/TextAreaGroup.tsx +13 -4
- package/src/Validation/validators.ts +16 -12
- package/src/__Tests__/FormTestBase.tsx +4 -4
- package/src/__Tests__/__snapshots__/StandardFormActions.test.tsx.snap +4 -2
- package/src/__Tests__/__snapshots__/SubmitFormButton.test.tsx.snap +3 -1
- package/src/index.ts +11 -10
- package/es/DatePicker/DateOnlyDatePickerGroup.d.ts +0 -10
- package/lib/DatePicker/DateOnlyDatePickerGroup.d.ts +0 -10
- package/src/DatePicker/DateOnlyDatePickerGroup.tsx +0 -24
package/src/Field/Field.tsx
CHANGED
@@ -4,6 +4,7 @@ import { InjectedFieldProps } from './InjectedFieldProps';
|
|
4
4
|
import useStandardFormInput from './useStandardField';
|
5
5
|
import { NormalizationFunction } from '../Normalization/NormalizationFunction';
|
6
6
|
import { ValidationFunction } from '../Validation/ValidationFunction';
|
7
|
+
import { required as requiredValidator } from '../Validation/validators';
|
7
8
|
|
8
9
|
// we attempted to support generic components but failed
|
9
10
|
// so, we assume the actual TRenderComponent has no generic arguments
|
@@ -13,7 +14,7 @@ export type RenderComponent<
|
|
13
14
|
TValue,
|
14
15
|
TRenderComponent extends ElementType
|
15
16
|
> = Partial<ComponentProps<TRenderComponent>> extends Partial<
|
16
|
-
InjectedFieldProps<TValue>
|
17
|
+
InjectedFieldProps<TValue | undefined | null>
|
17
18
|
>
|
18
19
|
? TRenderComponent
|
19
20
|
: never;
|
@@ -22,7 +23,7 @@ export type RenderComponentProps<
|
|
22
23
|
TValue,
|
23
24
|
TRenderComponent extends ElementType
|
24
25
|
> = Partial<ComponentProps<TRenderComponent>> extends Partial<
|
25
|
-
InjectedFieldProps<TValue>
|
26
|
+
InjectedFieldProps<TValue | undefined | null>
|
26
27
|
>
|
27
28
|
? ComponentProps<TRenderComponent>
|
28
29
|
: never;
|
@@ -60,9 +61,9 @@ function Field<
|
|
60
61
|
name,
|
61
62
|
Component,
|
62
63
|
id,
|
63
|
-
normalize,
|
64
|
-
validate,
|
65
64
|
disabled,
|
65
|
+
validate,
|
66
|
+
normalize,
|
66
67
|
...rest
|
67
68
|
}: FieldProps<TForm, TProp, TRenderComponent>,
|
68
69
|
ref: LegacyRef<any>
|
@@ -70,11 +71,17 @@ function Field<
|
|
70
71
|
const [input, meta] = useStandardFormInput<TForm[TProp]>({
|
71
72
|
name: String(name),
|
72
73
|
id: id,
|
73
|
-
normalize: normalize,
|
74
|
-
validate: validate,
|
75
74
|
disabled: disabled,
|
75
|
+
validate: validate,
|
76
|
+
normalize: normalize,
|
76
77
|
});
|
77
78
|
|
79
|
+
const isRequired =
|
80
|
+
rest?.required !== undefined
|
81
|
+
? rest.required
|
82
|
+
: validate === requiredValidator ||
|
83
|
+
(Array.isArray(validate) && validate.includes(requiredValidator));
|
84
|
+
|
78
85
|
// a bit of a hack so JSX is happy with us
|
79
86
|
const Wrapped = Component as React.ComponentType<
|
80
87
|
InjectedFieldProps<TForm[TProp]>
|
@@ -82,7 +89,15 @@ function Field<
|
|
82
89
|
|
83
90
|
return (
|
84
91
|
<FieldNameContext.Provider value={input.name}>
|
85
|
-
<Wrapped
|
92
|
+
<Wrapped
|
93
|
+
{...rest}
|
94
|
+
ref={ref}
|
95
|
+
id={input.id}
|
96
|
+
input={input}
|
97
|
+
meta={meta}
|
98
|
+
required={isRequired}
|
99
|
+
disabled={disabled}
|
100
|
+
/>
|
86
101
|
</FieldNameContext.Provider>
|
87
102
|
);
|
88
103
|
}
|
@@ -3,12 +3,13 @@ import classNames from 'classnames';
|
|
3
3
|
import Field, { FieldProps } from '../Field/Field';
|
4
4
|
import { FieldNameContext } from '../Field/FieldNameContext';
|
5
5
|
import useStandardFormInput from '../Field/useStandardField';
|
6
|
+
import { FormDefaults } from '../FormDefaults';
|
6
7
|
import { ValidationFunction } from '../Validation/ValidationFunction';
|
7
8
|
|
8
9
|
export type FieldArrayProps<
|
9
10
|
TForm extends object,
|
10
11
|
TProp extends keyof TForm
|
11
|
-
> = TForm[TProp] extends Array<any> | undefined
|
12
|
+
> = TForm[TProp] extends Array<any> | undefined | null
|
12
13
|
? {
|
13
14
|
name: TProp;
|
14
15
|
label?: string;
|
@@ -22,8 +23,8 @@ export type FieldArrayProps<
|
|
22
23
|
}
|
23
24
|
: never;
|
24
25
|
|
25
|
-
export type ArrayFormBuilderProp<TValue extends Array<any> | undefined> =
|
26
|
-
TValue extends Array<infer TForm> | undefined
|
26
|
+
export type ArrayFormBuilderProp<TValue extends Array<any> | undefined | null> =
|
27
|
+
TValue extends Array<infer TForm> | undefined | null
|
27
28
|
? TForm extends object
|
28
29
|
? {
|
29
30
|
Field: <
|
@@ -51,7 +52,7 @@ export default function FieldArray<
|
|
51
52
|
children,
|
52
53
|
...rest
|
53
54
|
}: FieldArrayProps<TForm, TProp>) {
|
54
|
-
const [input
|
55
|
+
const [input] = useStandardFormInput<TForm[TProp]>({
|
55
56
|
name: String(name),
|
56
57
|
validate: validate,
|
57
58
|
disabled: disabled,
|
@@ -64,18 +65,23 @@ export default function FieldArray<
|
|
64
65
|
: [];
|
65
66
|
|
66
67
|
return (
|
67
|
-
<div className=
|
68
|
-
<div className=
|
69
|
-
<div className=
|
68
|
+
<div className={FormDefaults.cssClassPrefix + 'field-array'}>
|
69
|
+
<div className={FormDefaults.cssClassPrefix + 'field-array-header'}>
|
70
|
+
<div className={FormDefaults.cssClassPrefix + 'field-array-title'}>
|
71
|
+
{label}
|
72
|
+
</div>
|
70
73
|
<button
|
71
|
-
className={classNames(
|
74
|
+
className={classNames(
|
75
|
+
FormDefaults.cssClassPrefix + 'add-array-item-button',
|
76
|
+
{ [FormDefaults.cssClassPrefix + 'disabled']: disabled }
|
77
|
+
)}
|
72
78
|
title="Add Item"
|
73
79
|
type="button"
|
74
80
|
onClick={addItem}>
|
75
81
|
+
|
76
82
|
</button>
|
77
83
|
</div>
|
78
|
-
<div className=
|
84
|
+
<div className={FormDefaults.cssClassPrefix + 'field-array-body'}>
|
79
85
|
{values.map((value, index) => {
|
80
86
|
const itemName = `${input.name}[${index}]`;
|
81
87
|
return (
|
@@ -85,9 +91,12 @@ export default function FieldArray<
|
|
85
91
|
(value && value['id']) ||
|
86
92
|
itemName
|
87
93
|
}
|
88
|
-
className={classNames(
|
89
|
-
|
90
|
-
|
94
|
+
className={classNames(
|
95
|
+
FormDefaults.cssClassPrefix + 'field-array-item',
|
96
|
+
{
|
97
|
+
[FormDefaults.cssClassPrefix + 'removed']: value.isDeleted,
|
98
|
+
}
|
99
|
+
)}
|
91
100
|
role="listitem">
|
92
101
|
<FieldNameContext.Provider value={itemName}>
|
93
102
|
{children({
|
@@ -96,7 +105,10 @@ export default function FieldArray<
|
|
96
105
|
} as any)}
|
97
106
|
</FieldNameContext.Provider>
|
98
107
|
<button
|
99
|
-
className={classNames(
|
108
|
+
className={classNames(
|
109
|
+
FormDefaults.cssClassPrefix + 'remove-array-item-button',
|
110
|
+
{ [FormDefaults.cssClassPrefix + 'disabled']: disabled }
|
111
|
+
)}
|
100
112
|
type="button"
|
101
113
|
title="Remove Item"
|
102
114
|
onClick={() => removeItem(value)}>
|
package/src/File/FileGroup.tsx
CHANGED
@@ -2,11 +2,12 @@ import React, { ComponentType, LegacyRef } from 'react';
|
|
2
2
|
import classNames from 'classnames';
|
3
3
|
import FileList from './FileList';
|
4
4
|
import { InjectedFieldProps } from '../Field/InjectedFieldProps';
|
5
|
+
import { FormDefaults } from '../FormDefaults';
|
5
6
|
import Group, { GroupProps } from '../Group';
|
6
7
|
|
7
8
|
export interface FileGroupProps
|
8
9
|
// note: file props are of type "any" with the current type generation
|
9
|
-
extends InjectedFieldProps<any | undefined>,
|
10
|
+
extends InjectedFieldProps<any | undefined | null>,
|
10
11
|
Omit<GroupProps, keyof InjectedFieldProps<any> | 'children'>,
|
11
12
|
Omit<
|
12
13
|
React.HTMLProps<HTMLInputElement>,
|
@@ -22,6 +23,8 @@ function FileGroup(
|
|
22
23
|
label,
|
23
24
|
helpText,
|
24
25
|
className,
|
26
|
+
required,
|
27
|
+
disabled,
|
25
28
|
multiple,
|
26
29
|
...rest
|
27
30
|
}: FileGroupProps,
|
@@ -33,7 +36,13 @@ function FileGroup(
|
|
33
36
|
meta={meta}
|
34
37
|
label={label}
|
35
38
|
helpText={helpText}
|
36
|
-
className={classNames(
|
39
|
+
className={classNames(
|
40
|
+
className,
|
41
|
+
{ [FormDefaults.cssClassPrefix + 'multiple']: multiple },
|
42
|
+
FormDefaults.cssClassPrefix + 'file-group'
|
43
|
+
)}
|
44
|
+
required={required}
|
45
|
+
disabled={disabled}>
|
37
46
|
<input
|
38
47
|
{...input}
|
39
48
|
{...rest}
|
@@ -56,7 +65,10 @@ function FileGroup(
|
|
56
65
|
value={undefined}
|
57
66
|
ref={ref}
|
58
67
|
type="file"
|
59
|
-
className={classNames(
|
68
|
+
className={classNames(
|
69
|
+
className,
|
70
|
+
FormDefaults.cssClassPrefix + 'file-group'
|
71
|
+
)}
|
60
72
|
/>
|
61
73
|
{/* Note: because input.value is any - due to how files are currently handled - type safeness isn't great here */}
|
62
74
|
<FileList files={input.value} />
|
package/src/File/FileList.tsx
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
import { FormDefaults } from '../FormDefaults';
|
2
|
+
|
1
3
|
export interface FileListProps {
|
2
|
-
files?: File | File[] | undefined;
|
3
|
-
rejectedFiles?: File | File[] | undefined;
|
4
|
+
files?: File | File[] | undefined | null;
|
5
|
+
rejectedFiles?: File | File[] | undefined | null;
|
4
6
|
}
|
5
7
|
export default function FileList({ files, rejectedFiles }: FileListProps) {
|
6
8
|
return (
|
7
|
-
<div className=
|
9
|
+
<div className={FormDefaults.cssClassPrefix + 'file-list'}>
|
8
10
|
{!files ? null : Array.isArray(files) ? (
|
9
11
|
files.map((x, i) => <File file={x} key={i} />)
|
10
12
|
) : (
|
@@ -4,9 +4,10 @@ exports[`FileGroup has matching snapshot 1`] = `
|
|
4
4
|
<DocumentFragment>
|
5
5
|
<form
|
6
6
|
action="#"
|
7
|
+
class="envoc-form-form"
|
7
8
|
>
|
8
9
|
<div
|
9
|
-
class="file-group group"
|
10
|
+
class="envoc-form-file-group envoc-form-group"
|
10
11
|
>
|
11
12
|
<div
|
12
13
|
id="profileimage-error-scroll-target"
|
@@ -18,13 +19,14 @@ exports[`FileGroup has matching snapshot 1`] = `
|
|
18
19
|
Profile Image
|
19
20
|
</label>
|
20
21
|
<input
|
21
|
-
class="file-group"
|
22
|
+
class="envoc-form-file-group"
|
23
|
+
id="profileImage"
|
22
24
|
name="profileImage"
|
23
25
|
type="file"
|
24
26
|
value=""
|
25
27
|
/>
|
26
28
|
<div
|
27
|
-
class="file-list"
|
29
|
+
class="envoc-form-file-list"
|
28
30
|
/>
|
29
31
|
</div>
|
30
32
|
</form>
|
package/src/Form/Form.tsx
CHANGED
@@ -5,6 +5,7 @@ import {
|
|
5
5
|
useMemo,
|
6
6
|
useState,
|
7
7
|
} from 'react';
|
8
|
+
import classNames from 'classnames';
|
8
9
|
import {
|
9
10
|
Form as FormikFormWrapper,
|
10
11
|
Formik,
|
@@ -20,6 +21,7 @@ import {
|
|
20
21
|
} from './ServerErrorContext';
|
21
22
|
import Field, { FieldProps } from '../Field/Field';
|
22
23
|
import FieldArray, { FieldArrayProps } from '../FieldArray/FieldArray';
|
24
|
+
import { FormDefaults } from '../FormDefaults';
|
23
25
|
import objectContainsNonSerializableProperty from '../utils/objectContainsNonSerializableProperty';
|
24
26
|
import objectToFormData from '../utils/objectToFormData';
|
25
27
|
import { ValidatedApiResult } from '../Validation/ValidatedApiResult';
|
@@ -38,10 +40,14 @@ export type FormBuilderProp<TForm extends object> = {
|
|
38
40
|
) => JSX.Element;
|
39
41
|
};
|
40
42
|
|
41
|
-
export interface
|
43
|
+
export interface FullFormProps<TForm extends object> {
|
42
44
|
children: (formBuilder: FormBuilderProp<TForm>) => JSX.Element;
|
43
45
|
onSubmit: (
|
44
|
-
formValues:
|
46
|
+
formValues: TForm,
|
47
|
+
formikBag: FormikHelpers<TForm>
|
48
|
+
) => Promise<ValidatedApiResult>;
|
49
|
+
onFormDataSubmit: (
|
50
|
+
formValues: FormData,
|
45
51
|
formikBag: FormikHelpers<TForm>
|
46
52
|
) => Promise<ValidatedApiResult>;
|
47
53
|
className?: string;
|
@@ -50,12 +56,26 @@ export interface FormProps<TForm extends object> {
|
|
50
56
|
initialValues?: TForm;
|
51
57
|
}
|
52
58
|
|
59
|
+
type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<
|
60
|
+
T,
|
61
|
+
Exclude<keyof T, Keys>
|
62
|
+
> &
|
63
|
+
{
|
64
|
+
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
|
65
|
+
}[Keys];
|
66
|
+
|
67
|
+
export type FormProps<TForm extends object> = RequireAtLeastOne<
|
68
|
+
FullFormProps<TForm>,
|
69
|
+
'onSubmit' | 'onFormDataSubmit'
|
70
|
+
>;
|
71
|
+
|
53
72
|
export default function Form<TForm extends object>({
|
54
73
|
children,
|
55
74
|
className,
|
56
75
|
style,
|
57
76
|
ignoreLostChanges,
|
58
77
|
onSubmit,
|
78
|
+
onFormDataSubmit,
|
59
79
|
initialValues,
|
60
80
|
...props
|
61
81
|
}: FormProps<TForm>) {
|
@@ -92,7 +112,12 @@ export default function Form<TForm extends object>({
|
|
92
112
|
onSubmit={handleSubmit}
|
93
113
|
{...props}>
|
94
114
|
<ServerErrorContext.Provider value={serverErrorContextValue}>
|
95
|
-
<FormikFormWrapper
|
115
|
+
<FormikFormWrapper
|
116
|
+
className={classNames(
|
117
|
+
className,
|
118
|
+
FormDefaults.cssClassPrefix + 'form'
|
119
|
+
)}
|
120
|
+
style={style}>
|
96
121
|
<FocusError serverErrors={serverErrorContextValue} />
|
97
122
|
<FormBasedPreventNavigation ignoreLostChanges={ignoreLostChanges} />
|
98
123
|
{children({
|
@@ -107,6 +132,7 @@ export default function Form<TForm extends object>({
|
|
107
132
|
|
108
133
|
function handleSubmit(values: TForm, formikBag: FormikHelpers<TForm>) {
|
109
134
|
let formData: FormData | undefined = undefined;
|
135
|
+
let submitFunc: () => Promise<ValidatedApiResult>;
|
110
136
|
if (objectContainsNonSerializableProperty(values)) {
|
111
137
|
formData = objectToFormData(values, {
|
112
138
|
indices: true,
|
@@ -114,9 +140,35 @@ export default function Form<TForm extends object>({
|
|
114
140
|
allowEmptyArrays: true,
|
115
141
|
noFileListBrackets: true,
|
116
142
|
});
|
143
|
+
if (onFormDataSubmit === undefined) {
|
144
|
+
throw new Error(
|
145
|
+
'No onFormDataSubmit supplied for non-serializable properties.'
|
146
|
+
);
|
147
|
+
}
|
148
|
+
submitFunc = () =>
|
149
|
+
onFormDataSubmit(formData ?? new FormData(), formikBag);
|
150
|
+
} else {
|
151
|
+
if (onSubmit === undefined) {
|
152
|
+
formData = objectToFormData(values, {
|
153
|
+
indices: true,
|
154
|
+
dotNotation: true,
|
155
|
+
allowEmptyArrays: true,
|
156
|
+
noFileListBrackets: true,
|
157
|
+
});
|
158
|
+
if (onFormDataSubmit === undefined) {
|
159
|
+
// This error should never occur, as this case is covered by RequireAtLeastOne type safety
|
160
|
+
throw new Error(
|
161
|
+
'No onFormDataSubmit supplied for non-serializable properties.'
|
162
|
+
);
|
163
|
+
}
|
164
|
+
submitFunc = () =>
|
165
|
+
onFormDataSubmit(formData ?? new FormData(), formikBag);
|
166
|
+
} else {
|
167
|
+
submitFunc = () => onSubmit(values, formikBag);
|
168
|
+
}
|
117
169
|
}
|
118
170
|
|
119
|
-
return
|
171
|
+
return submitFunc()
|
120
172
|
.then((response) => {
|
121
173
|
return response;
|
122
174
|
})
|
@@ -8,8 +8,13 @@ import {
|
|
8
8
|
import { useFormikContext } from 'formik';
|
9
9
|
|
10
10
|
interface Navigator extends BaseNavigator {
|
11
|
-
block
|
11
|
+
block?: History['block'];
|
12
12
|
}
|
13
|
+
|
14
|
+
//The Current state of the world (2023-05-10) is that useBlocker exists in react-router, but is only available for data routers, which we currently don't use
|
15
|
+
// `block` was available on UNSAFE_NavigationContext previously, but was removed
|
16
|
+
// Modifying `push` prevents _most_ but not all navigations with a prompt, long term solution may be to remove FileSystemRoutes and swap to a data router (https://reactrouter.com/en/main/routers/create-browser-router)
|
17
|
+
|
13
18
|
// see: https://github.com/remix-run/react-router/issues/8139#issuecomment-1023105785
|
14
19
|
type NavigationContextWithBlock = ContextType<typeof NavigationContext> & {
|
15
20
|
navigator: Navigator;
|
@@ -34,33 +39,44 @@ export default function FormBasedPreventNavigation({
|
|
34
39
|
if (!preventNavigate) {
|
35
40
|
return;
|
36
41
|
}
|
42
|
+
let unblock = () => {};
|
43
|
+
const push = navigator.push;
|
37
44
|
|
38
45
|
// TODO: https://reactrouter.com/docs/en/v6/upgrading/v5#prompt-is-not-currently-supported
|
39
46
|
// this is a workaround until we get native support for prompt on navigate
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
tx.retry();
|
44
|
-
}
|
45
|
-
};
|
46
|
-
const unblock = navigator.block((tx: Transition) => {
|
47
|
-
const autoUnblockingTx = {
|
48
|
-
...tx,
|
49
|
-
retry() {
|
50
|
-
// Automatically unblock the transition so it can play all the way
|
51
|
-
// through before retrying it. TODO: Figure out how to re-enable
|
52
|
-
// this block if the transition is cancelled for some reason.
|
53
|
-
unblock();
|
47
|
+
if (navigator.block) {
|
48
|
+
const blocker: Blocker = (tx) => {
|
49
|
+
if (window.confirm(promptMessage)) {
|
54
50
|
tx.retry();
|
55
|
-
}
|
51
|
+
}
|
56
52
|
};
|
53
|
+
unblock = navigator.block((tx: Transition) => {
|
54
|
+
const autoUnblockingTx = {
|
55
|
+
...tx,
|
56
|
+
retry() {
|
57
|
+
// Automatically unblock the transition so it can play all the way
|
58
|
+
// through before retrying it. TODO: Figure out how to re-enable
|
59
|
+
// this block if the transition is cancelled for some reason.
|
60
|
+
unblock();
|
61
|
+
tx.retry();
|
62
|
+
},
|
63
|
+
};
|
57
64
|
|
58
|
-
|
59
|
-
|
65
|
+
blocker(autoUnblockingTx);
|
66
|
+
});
|
67
|
+
} else {
|
68
|
+
//https://gist.github.com/MarksCode/64e438c82b0b2a1161e01c88ca0d0355
|
69
|
+
navigator.push = (...args: Parameters<typeof push>) => {
|
70
|
+
if (window.confirm(promptMessage)) {
|
71
|
+
push(...args);
|
72
|
+
}
|
73
|
+
};
|
74
|
+
}
|
60
75
|
|
61
76
|
window.addEventListener('beforeunload', beforeUnload);
|
62
77
|
return () => {
|
63
78
|
unblock();
|
79
|
+
navigator.push = push;
|
64
80
|
window.removeEventListener('beforeunload', beforeUnload);
|
65
81
|
};
|
66
82
|
|
package/src/FormActions.tsx
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { FormDefaults } from './FormDefaults';
|
1
2
|
import { useFormikContext } from 'formik';
|
2
3
|
import SubmitFormButton from './SubmitFormButton';
|
3
4
|
|
@@ -15,13 +16,18 @@ export default function FormActions({
|
|
15
16
|
return (
|
16
17
|
<>
|
17
18
|
<SubmitFormButton
|
18
|
-
className=
|
19
|
+
className={FormDefaults.cssClassPrefix + 'form-actions'}
|
19
20
|
allowPristineSubmit={allowPristineSubmit}
|
20
21
|
disabled={disabled}
|
21
22
|
/>
|
22
23
|
<button
|
23
24
|
type="button"
|
24
|
-
className=
|
25
|
+
className={
|
26
|
+
FormDefaults.cssClassPrefix +
|
27
|
+
'form-actions ' +
|
28
|
+
FormDefaults.cssClassPrefix +
|
29
|
+
'cancel-form-button'
|
30
|
+
}
|
25
31
|
disabled={isSubmitting || disabled}
|
26
32
|
onClick={handleCancel || goBack}>
|
27
33
|
Cancel
|
@@ -0,0 +1 @@
|
|
1
|
+
export const FormDefaults = { cssClassPrefix: 'envoc-form-' };
|
package/src/Group.tsx
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import classNames from 'classnames';
|
2
|
+
import { FormDefaults } from './FormDefaults';
|
2
3
|
import FieldErrorScrollTarget from './Field/FieldErrorScrollTarget';
|
3
4
|
import { InjectedFieldProps } from './Field/InjectedFieldProps';
|
4
5
|
|
@@ -14,6 +15,8 @@ export interface GroupProps extends InjectedFieldProps<any> {
|
|
14
15
|
helpText?: string | React.ReactNode;
|
15
16
|
|
16
17
|
disabled?: boolean;
|
18
|
+
|
19
|
+
required?: boolean;
|
17
20
|
}
|
18
21
|
|
19
22
|
/** contains standard field bits like a label, helper text, error scroll target, validation message container, etc */
|
@@ -25,19 +28,31 @@ export default function Group({
|
|
25
28
|
meta,
|
26
29
|
input,
|
27
30
|
disabled,
|
31
|
+
required,
|
28
32
|
}: GroupProps) {
|
29
33
|
return (
|
30
34
|
<div
|
31
|
-
className={classNames(className, 'group', {
|
32
|
-
'
|
33
|
-
disabled: disabled,
|
35
|
+
className={classNames(className, FormDefaults.cssClassPrefix + 'group', {
|
36
|
+
[FormDefaults.cssClassPrefix + 'invalid']: meta.error,
|
37
|
+
[FormDefaults.cssClassPrefix + 'disabled']: disabled,
|
38
|
+
[FormDefaults.cssClassPrefix + 'required']: required,
|
34
39
|
})}>
|
35
40
|
<FieldErrorScrollTarget />
|
36
|
-
{meta.warning &&
|
41
|
+
{meta.warning && (
|
42
|
+
<div className={FormDefaults.cssClassPrefix + 'warning'}>
|
43
|
+
{meta.warning}
|
44
|
+
</div>
|
45
|
+
)}
|
37
46
|
<label htmlFor={input.id}>{label}</label>
|
38
47
|
{children}
|
39
|
-
{meta.error &&
|
40
|
-
|
48
|
+
{meta.error && (
|
49
|
+
<div className={FormDefaults.cssClassPrefix + 'error'}>
|
50
|
+
{meta.error}
|
51
|
+
</div>
|
52
|
+
)}
|
53
|
+
{helpText && (
|
54
|
+
<div className={FormDefaults.cssClassPrefix + 'help'}>{helpText}</div>
|
55
|
+
)}
|
41
56
|
</div>
|
42
57
|
);
|
43
58
|
}
|
@@ -1,13 +1,13 @@
|
|
1
1
|
import React, { LegacyRef } from 'react';
|
2
2
|
import classNames from 'classnames';
|
3
3
|
import InputGroupWithRef, { InputGroupProps } from './InputGroup';
|
4
|
-
import
|
4
|
+
import { FormDefaults } from '../FormDefaults';
|
5
5
|
|
6
6
|
// TODO: make className tailwind-able instead of css. make it typeof string ????
|
7
7
|
// or should we just give a div a className and let each project decide? (this seems to be the patern)
|
8
8
|
export interface IconInputGroupProps
|
9
9
|
extends Omit<
|
10
|
-
InputGroupProps<string | undefined>,
|
10
|
+
InputGroupProps<string | undefined | null>,
|
11
11
|
'onChange' | 'type' | 'value'
|
12
12
|
> {
|
13
13
|
type?: 'color' | 'email' | 'search' | 'tel' | 'text' | 'url';
|
@@ -26,8 +26,11 @@ function IconInputGroup(
|
|
26
26
|
<InputGroupWithRef
|
27
27
|
icon={icon}
|
28
28
|
ref={ref}
|
29
|
-
className={classNames(
|
30
|
-
|
29
|
+
className={classNames(
|
30
|
+
className,
|
31
|
+
FormDefaults.cssClassPrefix + 'icon-input-group'
|
32
|
+
)}
|
33
|
+
value={input.value ?? ''}
|
31
34
|
onChange={(e) => {
|
32
35
|
if (!e.target.value) {
|
33
36
|
input.onChange(undefined);
|
package/src/Input/InputGroup.tsx
CHANGED
@@ -1,17 +1,22 @@
|
|
1
1
|
import React, { ChangeEventHandler, LegacyRef } from 'react';
|
2
2
|
import classNames from 'classnames';
|
3
3
|
import { InjectedFieldProps } from '../Field/InjectedFieldProps';
|
4
|
+
import { FormDefaults } from '../FormDefaults';
|
4
5
|
import Group, { GroupProps } from '../Group';
|
5
6
|
|
6
7
|
export interface InputGroupProps<TValue>
|
7
8
|
extends InjectedFieldProps<TValue>,
|
8
9
|
Omit<
|
9
10
|
React.HTMLProps<HTMLInputElement>,
|
10
|
-
keyof InjectedFieldProps<any>
|
11
|
+
| keyof InjectedFieldProps<any>
|
12
|
+
| 'children'
|
13
|
+
| 'className'
|
14
|
+
| 'label'
|
15
|
+
| 'value'
|
11
16
|
>,
|
12
17
|
Omit<GroupProps, keyof InjectedFieldProps<any> | 'children'> {
|
13
18
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
14
|
-
value: string | number;
|
19
|
+
value: string | number | null | undefined;
|
15
20
|
icon?: React.ReactNode;
|
16
21
|
}
|
17
22
|
|
@@ -23,6 +28,7 @@ function InputGroup<TValue>(
|
|
23
28
|
label,
|
24
29
|
helpText,
|
25
30
|
className,
|
31
|
+
required,
|
26
32
|
disabled,
|
27
33
|
onChange,
|
28
34
|
value,
|
@@ -35,18 +41,25 @@ function InputGroup<TValue>(
|
|
35
41
|
<Group
|
36
42
|
input={input}
|
37
43
|
meta={meta}
|
44
|
+
required={required}
|
38
45
|
disabled={disabled}
|
39
46
|
label={label}
|
40
47
|
helpText={helpText}
|
41
|
-
className={classNames(
|
48
|
+
className={classNames(
|
49
|
+
className,
|
50
|
+
FormDefaults.cssClassPrefix + 'input-group'
|
51
|
+
)}>
|
42
52
|
{icon}
|
43
53
|
<input
|
44
54
|
{...input}
|
45
55
|
{...rest}
|
46
|
-
value={value}
|
56
|
+
value={value ?? ''}
|
47
57
|
onChange={onChange}
|
48
58
|
ref={ref}
|
49
|
-
className={classNames(
|
59
|
+
className={classNames(
|
60
|
+
className,
|
61
|
+
FormDefaults.cssClassPrefix + 'input-group'
|
62
|
+
)}
|
50
63
|
/>
|
51
64
|
</Group>
|
52
65
|
);
|
@@ -1,10 +1,11 @@
|
|
1
1
|
import React, { LegacyRef } from 'react';
|
2
2
|
import classNames from 'classnames';
|
3
3
|
import InputGroup, { InputGroupProps } from './InputGroup';
|
4
|
+
import { FormDefaults } from '../FormDefaults';
|
4
5
|
|
5
6
|
export interface MoneyInputGroupProps
|
6
7
|
extends Omit<
|
7
|
-
InputGroupProps<number | undefined>,
|
8
|
+
InputGroupProps<number | undefined | null>,
|
8
9
|
'onChange' | 'type' | 'value'
|
9
10
|
> {
|
10
11
|
parseFunc?: typeof parseInt | typeof parseFloat;
|
@@ -24,7 +25,10 @@ function MoneyInputGroup(
|
|
24
25
|
min={0}
|
25
26
|
{...rest}
|
26
27
|
type="number"
|
27
|
-
className={classNames(
|
28
|
+
className={classNames(
|
29
|
+
className,
|
30
|
+
FormDefaults.cssClassPrefix + 'money-group'
|
31
|
+
)}
|
28
32
|
value={input.value || ''}
|
29
33
|
onChange={(e) => {
|
30
34
|
if (!e.target.value) {
|