Skip to content

Commit 7061928

Browse files
Hyperkid123claude
andcommitted
feat: upgrade final-form ecosystem to latest major versions
Bump final-form (v4→v5), final-form-arrays (v3→v4), react-final-form (v6→v7), and react-final-form-arrays (v3→v5). All packages migrated from Flow to TypeScript with stricter type signatures. Key type adaptations: - FormProps now takes 0-1 generic args (was 2) - UseFieldConfig is no longer generic - FormState/FormApi require InitialFormValues extends Partial<FormValues> - FieldMetaState fields (valid, validating, active) are now optional Replace final-form-focus v2 with an inline focus decorator that preserves the v1 submit-only focus behavior. The v2 package introduced an unintended regression: it subscribes to errors continuously and steals focus during typing, breaking form interactions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f8a3e92 commit 7061928

9 files changed

Lines changed: 135 additions & 95 deletions

File tree

package-lock.json

Lines changed: 49 additions & 71 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/common/src/wizard/enter-handler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { AnyObject } from '@data-driven-forms/react-form-renderer';
44

55
interface FormOptions {
66
valid: boolean;
7-
getState: () => AnyObject & { validating: boolean; values: AnyObject };
7+
getState: () => AnyObject & { validating?: boolean; values: AnyObject };
88
getRegisteredFields: () => AnyObject;
99
}
1010

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import type { FieldMetaState } from 'react-final-form';
2-
3-
export interface ExtendedFieldMeta extends FieldMetaState<any>, Record<string, unknown> {
1+
export interface ExtendedFieldMeta extends Record<string, unknown> {
2+
error?: string;
3+
submitError?: string;
4+
touched?: boolean;
45
warning?: any;
56
}
67

7-
export const validationError = (meta: ExtendedFieldMeta, validateOnMount?: boolean): string | undefined => {
8+
export const validationError = (meta: ExtendedFieldMeta, validateOnMount?: boolean): string | false | undefined => {
89
if (validateOnMount) {
910
return meta.error || meta.submitError;
1011
}
1112

12-
return meta.touched && (meta.error || meta.submitError);
13+
return meta.touched ? meta.error || meta.submitError : false;
1314
};
1415

1516
export default validationError;

packages/mui-component-mapper/src/wizard/step-buttons.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ const StyledGrid = styled(Grid)(() => ({
3939

4040
interface FormState {
4141
values: AnyObject;
42-
valid: boolean;
43-
validating: boolean;
44-
submitting: boolean;
42+
valid?: boolean;
43+
validating?: boolean;
44+
submitting?: boolean;
4545
}
4646

4747
interface NextButtonProps {

packages/react-form-renderer/package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,11 @@
2828
"process": "^0.11.10"
2929
},
3030
"dependencies": {
31-
"final-form": "^4.20.10",
32-
"final-form-arrays": "^3.0.2",
33-
"final-form-focus": "^1.1.2",
31+
"final-form": "^5.0.1",
32+
"final-form-arrays": "^4.0.1",
3433
"lodash": "^4.18.1",
35-
"react-final-form": "^6.5.0",
36-
"react-final-form-arrays": "^3.1.1"
34+
"react-final-form": "^7.0.1",
35+
"react-final-form-arrays": "^5.0.0"
3736
},
3837
"peerDependencies": {
3938
"react": "^17.0.2 || ^18.0.0 || ^19.0.0",
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { FormApi, getIn } from 'final-form';
2+
3+
type FocusableInput = { name: string; focus: () => void };
4+
type GetInputs = () => FocusableInput[];
5+
type FindInput = (inputs: FocusableInput[], errors: Record<string, any>) => FocusableInput | undefined;
6+
7+
const isFocusableInput = (el: any): boolean => !!(el && typeof el.focus === 'function');
8+
9+
const defaultGetInputs: GetInputs = () => {
10+
if (typeof document === 'undefined') return [];
11+
return Array.prototype.slice
12+
.call(document.forms)
13+
.reduce((acc: FocusableInput[], form: HTMLFormElement) => {
14+
return acc.concat(Array.prototype.slice.call(form).filter(isFocusableInput));
15+
}, []);
16+
};
17+
18+
const defaultFindInput: FindInput = (inputs, errors) =>
19+
inputs.find((input) => input.name && getIn(errors, input.name));
20+
21+
const createFocusDecorator = (getInputs?: GetInputs, findInputFn?: FindInput) => {
22+
return (form: FormApi) => {
23+
const focusOnFirstError = (errors: Record<string, any>) => {
24+
const inputs = (getInputs || defaultGetInputs)();
25+
const firstInput = (findInputFn || defaultFindInput)(inputs, errors);
26+
if (firstInput) firstInput.focus();
27+
};
28+
29+
const originalSubmit = form.submit;
30+
let state: { errors?: Record<string, any>; submitErrors?: Record<string, any> } = {};
31+
32+
const unsubscribe = form.subscribe(
33+
(nextState) => { state = nextState; },
34+
{ errors: true, submitErrors: true }
35+
);
36+
37+
const afterSubmit = () => {
38+
if (state.errors && Object.keys(state.errors).length) {
39+
focusOnFirstError(state.errors);
40+
} else if (state.submitErrors && Object.keys(state.submitErrors).length) {
41+
focusOnFirstError(state.submitErrors);
42+
}
43+
};
44+
45+
form.submit = () => {
46+
const result = originalSubmit.call(form);
47+
if (result && typeof result.then === 'function') {
48+
result.then(afterSubmit, () => {});
49+
} else {
50+
afterSubmit();
51+
}
52+
return result;
53+
};
54+
55+
return () => {
56+
unsubscribe();
57+
form.submit = originalSubmit;
58+
};
59+
};
60+
};
61+
62+
export default createFocusDecorator;

packages/react-form-renderer/src/form-renderer/form-renderer.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import arrayMutators from 'final-form-arrays';
2-
import createFocusDecorator from 'final-form-focus';
2+
import createFocusDecorator from './focus-decorator';
33
import React, { useCallback, useMemo, useRef, useState, cloneElement, ReactNode, ComponentType, FunctionComponent, ReactElement } from 'react';
44
import { FormProps } from 'react-final-form';
55
import { FormApi } from 'final-form';
66

77
import defaultSchemaValidator from '../default-schema-validator';
88
import defaultValidatorMapper from '../validator-mapper';
99
import Form from '../form';
10-
import RendererContext from '../renderer-context';
10+
import RendererContext, { FormOptions } from '../renderer-context';
1111
import renderForm from './render-form';
1212
import SchemaErrorComponent from './schema-error-component';
1313
import Schema from '../common-types/schema';
@@ -21,14 +21,14 @@ import { ConditionMapper } from './condition-mapper';
2121

2222
export interface FormRendererProps<
2323
FormValues = Record<string, any>,
24-
InitialFormValues = Partial<FormValues>,
24+
InitialFormValues extends Partial<FormValues> = Partial<FormValues>,
2525
FormTemplateProps extends FormTemplateRenderProps = FormTemplateRenderProps
26-
> extends Omit<NoIndex<FormProps<FormValues, InitialFormValues>>, 'onSubmit' | 'children'> {
26+
> extends Omit<NoIndex<FormProps<FormValues>>, 'onSubmit' | 'children'> {
2727
initialValues?: InitialFormValues;
2828
onCancel?: (values: FormValues, ...args: any[]) => void;
2929
onReset?: () => void;
3030
onError?: (...args: any[]) => void;
31-
onSubmit?: FormProps<FormValues, InitialFormValues>['onSubmit'];
31+
onSubmit?: FormProps<FormValues>['onSubmit'];
3232
schema: Schema | (Record<string, any> & { fields: Array<Record<string, any>> });
3333
clearOnUnmount?: boolean;
3434
clearedValue?: any;
@@ -82,7 +82,7 @@ const renderChildren = (children: ReactNode | ((props: Record<string, any>) => R
8282

8383
function FormRenderer<
8484
FormValues = Record<string, any>,
85-
InitialFormValues = Partial<FormValues>,
85+
InitialFormValues extends Partial<FormValues> = Partial<FormValues>,
8686
FTP extends FormTemplateRenderProps = FormTemplateRenderProps
8787
>({
8888
actionMapper,
@@ -214,7 +214,7 @@ function FormRenderer<
214214
onCancel: isFunc(onCancel) ? handleCancelCallback(getState) : undefined,
215215
onReset: handleResetCallback(reset),
216216
onError: handleErrorCallback,
217-
getState,
217+
getState: getState as FormOptions['getState'],
218218
valid,
219219
clearedValue,
220220
submit,
@@ -240,7 +240,7 @@ function FormRenderer<
240240
</RendererContext.Provider>
241241
)}
242242
{...props}
243-
initialValues={initialValues}
243+
initialValues={initialValues as Partial<FormValues>}
244244
/>
245245
);
246246
}

packages/react-form-renderer/src/renderer-context/renderer-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Field from '../common-types/field';
77
import Schema from '../common-types/schema';
88
import { ConditionMapper } from '../form-renderer/condition-mapper';
99

10-
export interface FormOptions<FormValues = Record<string, any>, InitialFormValues = Partial<FormValues>>
10+
export interface FormOptions<FormValues = Record<string, any>, InitialFormValues extends Partial<FormValues> = Partial<FormValues>>
1111
extends FormApi<FormValues, InitialFormValues> {
1212
registerInputFile?: (name: keyof FormValues) => void;
1313
unRegisterInputFile?: (name: keyof FormValues) => void;

packages/react-form-renderer/src/use-field-api/use-field-api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface UseFieldApiConfig extends AnyObject {
3636
type?: string;
3737
}
3838

39-
export interface UseFieldApiComponentConfig extends UseFieldConfig<any> {
39+
export interface UseFieldApiComponentConfig extends UseFieldConfig {
4040
name: string;
4141
}
4242

0 commit comments

Comments
 (0)