Skip to content

Commit 6898f69

Browse files
authored
Merge pull request #100 from workfloworchestrator/1904-submit-empty-step
1904 Prevents uncontrolled fields from being added to form state
2 parents f1d5df5 + 4947b12 commit 6898f69

File tree

13 files changed

+148
-138
lines changed

13 files changed

+148
-138
lines changed

frontend/apps/example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.1.1",
44
"private": true,
55
"scripts": {
6-
"dev": "next dev",
6+
"dev": "next dev -H 127.0.0.1",
77
"build": "next build",
88
"start": "next start",
99
"test": "jest --passWithNoTests",

frontend/packages/pydantic-forms/src/components/componentMatcher.tsx

Lines changed: 0 additions & 89 deletions
This file was deleted.

frontend/packages/pydantic-forms/src/components/fields/ArrayField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import { useFieldArray } from 'react-hook-form';
33

44
import { usePydanticFormContext } from '@/core';
5-
import { fieldToComponentMatcher } from '@/core';
5+
import { fieldToComponentMatcher } from '@/core/helper';
66
import { PydanticFormElementProps } from '@/types';
77
import { itemizeArrayItem } from '@/utils';
88

frontend/packages/pydantic-forms/src/components/fields/ObjectField.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22

33
import { usePydanticFormContext } from '@/core';
4-
import { componentsMatcher } from '@/core';
4+
import { getPydanticFormComponents } from '@/core/helper';
55
import { PydanticFormElementProps } from '@/types';
66

77
import { RenderFields } from '../render';
@@ -10,8 +10,7 @@ export const ObjectField = ({
1010
pydanticFormField,
1111
}: PydanticFormElementProps) => {
1212
const { config } = usePydanticFormContext();
13-
14-
const components = componentsMatcher(
13+
const components = getPydanticFormComponents(
1514
pydanticFormField.properties || {},
1615
config?.componentMatcher,
1716
);

frontend/packages/pydantic-forms/src/components/render/RenderForm.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import React from 'react';
99

1010
import { useTranslations } from 'next-intl';
1111

12-
import { componentsMatcher } from '@/components/componentMatcher';
1312
import Footer from '@/components/form/Footer';
1413
import RenderFormErrors from '@/components/render/RenderFormErrors';
14+
import { getPydanticFormComponents } from '@/core/helper';
1515
import { PydanticFormComponents, PydanticFormContextProps } from '@/types';
1616

1717
import { FormRenderer } from './FormRenderer';
@@ -30,11 +30,13 @@ const RenderForm = (contextProps: PydanticFormContextProps) => {
3030
skipSuccessNotice,
3131
loadingComponent,
3232
} = contextProps;
33-
const {
34-
formRenderer,
35-
footerRenderer,
36-
componentMatcher: customComponentMatcher,
37-
} = config || {};
33+
const { formRenderer, footerRenderer } = config || {};
34+
35+
const pydanticFormComponents: PydanticFormComponents =
36+
getPydanticFormComponents(
37+
pydanticFormSchema?.properties || {},
38+
config?.componentMatcher,
39+
);
3840

3941
const t = useTranslations('renderForm');
4042

@@ -59,11 +61,6 @@ const RenderForm = (contextProps: PydanticFormContextProps) => {
5961
const Renderer = formRenderer ?? FormRenderer;
6062
const FooterRenderer = footerRenderer ?? Footer;
6163

62-
const pydanticFormComponents: PydanticFormComponents = componentsMatcher(
63-
pydanticFormSchema.properties,
64-
customComponentMatcher,
65-
);
66-
6764
return (
6865
<form action={''} onSubmit={submitForm}>
6966
{title !== false &&

frontend/packages/pydantic-forms/src/core/PydanticFormContextProvider.tsx

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import React, {
99
createContext,
1010
useCallback,
11-
useContext,
1211
useEffect,
1312
useRef,
1413
useState,
@@ -164,6 +163,7 @@ function PydanticFormContextProvider({
164163
);
165164

166165
const rhfRef = useRef<ReturnType<typeof useForm>>();
166+
167167
// build validation rules based on custom schema
168168
const resolver = useGetZodValidator(
169169
pydanticFormSchema,
@@ -251,13 +251,17 @@ function PydanticFormContextProvider({
251251
return;
252252
}
253253

254-
const initialData = getFormValuesFromFieldOrLabels(pydanticFormSchema, {
255-
...formLabels?.data,
256-
...customData,
257-
});
254+
const initialData = getFormValuesFromFieldOrLabels(
255+
pydanticFormSchema,
256+
{
257+
...formLabels?.data,
258+
...customData,
259+
},
260+
componentMatcher,
261+
);
258262

259263
rhf.reset(initialData);
260-
}, [customData, formLabels, pydanticFormSchema, rhf]);
264+
}, [customData, formLabels, pydanticFormSchema, rhf, componentMatcher]);
261265

262266
// a useeffect for filling data whenever formdefinition or labels update
263267
useEffect(() => {
@@ -398,16 +402,4 @@ function PydanticFormContextProvider({
398402
);
399403
}
400404

401-
export function usePydanticFormContext() {
402-
const context = useContext(PydanticFormContext);
403-
404-
if (!context) {
405-
throw new Error(
406-
'usePydanticFormContext must be used within a PydanticFormProvider',
407-
);
408-
}
409-
410-
return context;
411-
}
412-
413405
export default PydanticFormContextProvider;

frontend/packages/pydantic-forms/src/core/WrapFieldElement.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@ import React from 'react';
22
import { Controller } from 'react-hook-form';
33

44
import { FieldWrap } from '@/components/fields';
5+
import { usePydanticFormContext } from '@/core/hooks';
56
import type { PydanticFormControlledElement, PydanticFormField } from '@/types';
67

7-
import { usePydanticFormContext } from './PydanticFormContextProvider';
8-
98
export const WrapFieldElement = ({
109
PydanticFormControlledElement,
1110
pydanticFormField,

frontend/packages/pydantic-forms/src/core/helper.ts

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,24 @@
55
*/
66
import { ControllerRenderProps, FieldValues, useForm } from 'react-hook-form';
77

8+
import { z } from 'zod';
9+
10+
import defaultComponentMatchers from '@/components/defaultComponentMatchers';
11+
import { TextField } from '@/components/fields';
812
import {
13+
ElementMatch,
914
Properties,
15+
PydanticComponentMatcher,
1016
PydanticFormApiResponse,
17+
PydanticFormComponents,
1118
PydanticFormField,
1219
PydanticFormFieldAttributes,
1320
PydanticFormFieldOption,
1421
PydanticFormFieldType,
1522
PydanticFormFieldValidations,
1623
PydanticFormPropertySchemaParsed,
1724
PydanticFormSchema,
25+
PydanticFormsContextConfig,
1826
} from '@/types';
1927

2028
/**
@@ -250,20 +258,28 @@ export const isNullableField = (field: PydanticFormField) =>
250258
export const getFormValuesFromFieldOrLabels = (
251259
pydanticFormSchema: PydanticFormSchema,
252260
labelData?: Record<string, string>,
261+
componentMatcher?: PydanticFormsContextConfig['componentMatcher'],
253262
) => {
254263
const fieldValues: Record<string, string> = {};
255264

256265
const includedFields: string[] = [];
257266

258-
for (const [, field] of Object.entries(pydanticFormSchema.properties)) {
259-
includedFields.push(field.id);
267+
const pydanticFormComponents = getPydanticFormComponents(
268+
pydanticFormSchema.properties,
269+
componentMatcher,
270+
);
260271

261-
if (typeof field.default === 'undefined') {
262-
continue;
263-
}
272+
pydanticFormComponents.forEach((component) => {
273+
const { Element, pydanticFormField } = component;
264274

265-
fieldValues[field.id] = field.default;
266-
}
275+
if (Element.isControlledElement) {
276+
includedFields.push(pydanticFormField.id);
277+
278+
if (typeof pydanticFormField.default !== 'undefined') {
279+
fieldValues[pydanticFormField.id] = pydanticFormField.default;
280+
}
281+
}
282+
});
267283

268284
if (labelData) {
269285
for (const fieldId in labelData) {
@@ -324,3 +340,74 @@ export const rhfTriggerValidationsOnChange =
324340
// https://github.com/react-hook-form/react-hook-form/issues/10832
325341
rhf.trigger(field.name);
326342
};
343+
344+
export const getMatcher = (
345+
customComponentMatcher: PydanticFormsContextConfig['componentMatcher'],
346+
) => {
347+
const componentMatchers = customComponentMatcher
348+
? customComponentMatcher(defaultComponentMatchers)
349+
: defaultComponentMatchers;
350+
351+
return (field: PydanticFormField): PydanticComponentMatcher | undefined => {
352+
return componentMatchers.find(({ matcher }) => {
353+
return matcher(field);
354+
});
355+
};
356+
};
357+
358+
export const getClientSideValidationRule = (
359+
field: PydanticFormField | undefined,
360+
rhf?: ReturnType<typeof useForm>,
361+
customComponentMatcher?: PydanticFormsContextConfig['componentMatcher'],
362+
) => {
363+
if (!field) return z.unknown();
364+
const matcher = getMatcher(customComponentMatcher);
365+
366+
const componentMatch = matcher(field);
367+
368+
let validationRule = componentMatch?.validator?.(field, rhf) ?? z.unknown();
369+
370+
if (!field.required) {
371+
validationRule = validationRule.optional();
372+
}
373+
374+
if (field.validations.isNullable) {
375+
validationRule = validationRule.nullable();
376+
}
377+
378+
return validationRule;
379+
};
380+
381+
const defaultComponent: ElementMatch = {
382+
Element: TextField,
383+
isControlledElement: true,
384+
};
385+
386+
export const fieldToComponentMatcher = (
387+
pydanticFormField: PydanticFormField,
388+
customComponentMatcher: PydanticFormsContextConfig['componentMatcher'],
389+
) => {
390+
const matcher = getMatcher(customComponentMatcher);
391+
const matchedComponent = matcher(pydanticFormField);
392+
393+
const ElementMatch: ElementMatch = matchedComponent
394+
? matchedComponent.ElementMatch
395+
: defaultComponent; // Defaults to textField when there are no matches
396+
397+
return {
398+
Element: ElementMatch,
399+
pydanticFormField: pydanticFormField,
400+
};
401+
};
402+
export const getPydanticFormComponents = (
403+
properties: Properties,
404+
componentMatcher: PydanticFormsContextConfig['componentMatcher'],
405+
): PydanticFormComponents => {
406+
const components: PydanticFormComponents = Object.entries(properties).map(
407+
([, pydanticFormField]) => {
408+
return fieldToComponentMatcher(pydanticFormField, componentMatcher);
409+
},
410+
);
411+
412+
return components;
413+
};

frontend/packages/pydantic-forms/src/core/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ export * from './usePydanticFormParser';
33
export * from './useCustomDataProvider';
44
export * from './useGetZodValidator';
55
export * from './useLabelProvider';
6+
export * from './usePydanticFormContext';

0 commit comments

Comments
 (0)