Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
63f6201
Updates package version in package-lock
Sep 10, 2025
0ac13b8
2143: Refactor RenderFormErrors into RenderValidatioErrors
Sep 10, 2025
1e17d84
2143: Improve typing of apiResonse
Sep 10, 2025
7a107cf
2143: Add changeset
Sep 10, 2025
0a77871
2143: Adds configurable error component
Sep 11, 2025
fbb68f9
2143: Updates react-hook-form to latest version
Sep 11, 2025
b4308d0
2143: Small refactor
Sep 11, 2025
4b091f0
2143: Refactor: Split components WIP
Sep 15, 2025
d3776ef
2143: Create usePydanticForm hook. Moves logic there WIP
Sep 15, 2025
cc6c271
2143: Moves api and parsing logic to usePydanticForm hook
Sep 15, 2025
360b06b
2143: Create react-hook-form in its own component
Sep 15, 2025
85be78a
2143: Fixes frontend validation lag
Sep 15, 2025
55e54cb
1243: Introduces config context and useGetConfig hook
Sep 15, 2025
7d6b38d
2143: Fixes passing title to header
Sep 15, 2025
38b07cf
2143: Fixes hasNext and hasPrevious in footer component
Sep 15, 2025
ca7daee
2143: Stores previous steps and uses them
Sep 16, 2025
55b5a88
2143: Handle backend validation errors
Sep 16, 2025
ba58772
2143: Handle 200 success response
Sep 16, 2025
ce81be0
2143: Handle 500 responses
Sep 16, 2025
4720959
2143: Re-implement back fucntionality
Sep 16, 2025
b0904d5
2143: reimplements going back
Sep 16, 2025
9a41da8
2143: Reimplement restoring history when going forward
Sep 16, 2025
9379bf8
2143: Readss fieldDataStorage as a hook
Sep 16, 2025
c2473a0
2143: Fixes arrayField
Sep 16, 2025
77a0503
2143: Restores object field
Sep 16, 2025
8f5a051
2143: Fixes linting errors
Sep 16, 2025
f3a1b75
2143: Restore validation error render component
Sep 16, 2025
5eee4af
2143: Create and expose a proxy hook for useFormContext
Sep 16, 2025
3d2a3ce
2143: Fixes using 201 as a status code
Sep 17, 2025
96bad36
2143: Fixes fieldDataStorage
Sep 17, 2025
34bfe49
2143: Adds changeset
Sep 17, 2025
0c67d61
2143: Remove disabled line
Sep 17, 2025
38924ac
2143: Fixes resetting form on formKey change
Sep 17, 2025
6b17a59
2143: TyPo
Sep 17, 2025
270efe3
Update frontend/packages/pydantic-forms/src/core/hooks/useFieldDataSt…
DutchBen Sep 17, 2025
9a4ebd2
2143: PR comments
Sep 17, 2025
fee729d
Update frontend/apps/example/src/app/page.tsx
DutchBen Sep 17, 2025
ee95c49
2143: Reject if not valid return code
Sep 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,19 +165,19 @@ def form_generator(state: State):
class TestForm0(FormPage):
model_config = ConfigDict(title="Form Title Page 1")

number: NumberExample
list: TestExampleNumberList
number: NumberExample = 3
# list: TestExampleNumberList
# list_list: unique_conlist(TestExampleNumberList, min_items=1, max_items=5)
# list_list_list: unique_conlist(
# unique_conlist(Person2, min_items=1, max_items=5),
# min_items=1,
# max_items=2,
# ) = [1, 2]
test: TestString
textList: unique_conlist(TestString, min_items=1, max_items=5)
test: TestString = "aa"
# textList: unique_conlist(TestString, min_items=1, max_items=5)
# numberList: TestExampleNumberList = [1, 2]
person: Person2
personList: unique_conlist(Person2, min_items=2, max_items=5)
# person: Person2
# personList: unique_conlist(Person2, min_items=2, max_items=5)
# ingleNumber: NumberExample
# number0: Annotated[int, Ge(18), Le(99)] = 17

Expand Down
5 changes: 5 additions & 0 deletions frontend/.changeset/floppy-eels-invent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pydantic-forms': patch
---

Improves handling 500 api errors
5 changes: 5 additions & 0 deletions frontend/.changeset/mighty-dingos-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pydantic-forms': minor
---

Refactors component and contextprovider setup
58 changes: 51 additions & 7 deletions frontend/apps/example/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use client';

import type { FieldValues } from 'react-hook-form';

import {
Locale,
PydanticForm,
Expand All @@ -11,6 +13,7 @@ import type {
PydanticFormApiProvider,
PydanticFormCustomDataProvider,
PydanticFormLabelProvider,
PydanticFormSuccessResponse,
} from 'pydantic-forms';

import { TextArea } from '@/fields';
Expand All @@ -23,15 +26,49 @@ export default function Home() {
}) => {
const url = 'http://localhost:8000/form';

const fetchResult = await fetch(url, {
return fetch(url, {
method: 'POST',
body: JSON.stringify(requestBody),
headers: {
'Content-Type': 'application/json',
},
});
const jsonResult = await fetchResult.json();
return jsonResult;
})
.then(async (fetchResult) => {
// Note: https://chatgpt.com/share/68c16538-5544-800c-9684-1e641168dbff
if (
fetchResult.status === 400 ||
fetchResult.status === 510 ||
fetchResult.status === 200 ||
fetchResult.status === 201
) {
const data = await fetchResult.json();

return new Promise<Record<string, unknown>>(
(resolve, reject) => {
if (
fetchResult.status === 510 ||
fetchResult.status === 400
) {
resolve({
...data,
status: fetchResult.status,
});
}
if (fetchResult.status === 200) {
resolve({ status: 200, data });
}
reject('No valid status in response');
},
);
}
throw new Error(
`Status not 400, 510 or 200: ${fetchResult.statusText}`,
);
}) //
.catch((error) => {
// Note: https://chatgpt.com/share/68c16538-5544-800c-9684-1e641168dbff
throw new Error(`Fetch error: ${error}`);
});
};

const pydanticLabelProvider: PydanticFormLabelProvider = async () => {
Expand Down Expand Up @@ -84,6 +121,15 @@ export default function Home() {
};
const locale = Locale.enGB;

const onSuccess = (
_: FieldValues[],
apiResponse: PydanticFormSuccessResponse,
) => {
alert(
`Form submitted successfully: ${JSON.stringify(apiResponse.data)}`,
);
};

return (
<div className={styles.page}>
<h1 style={{ marginBottom: '20px' }}>Pydantic Form </h1>
Expand All @@ -94,9 +140,7 @@ export default function Home() {
onCancel={() => {
alert('Form cancelled');
}}
onSuccess={() => {
alert('Form submitted successfully');
}}
onSuccess={onSuccess}
config={{
apiProvider: pydanticFormApiProvider,
labelProvider: pydanticLabelProvider,
Expand Down
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

52 changes: 33 additions & 19 deletions frontend/packages/pydantic-forms/src/PydanticForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,48 @@
* This is the component that will be included when we want to use a form.
* It initializes the context and calls the mainForm
*/
import React from 'react';
import React, { createContext } from 'react';

import RenderForm from '@/components/render/RenderForm';
import PydanticFormContextProvider from '@/core/PydanticFormContextProvider';
import type { PydanticFormContextProviderProps } from '@/core/PydanticFormContextProvider';
import { TranslationsProvider } from '@/messages/translationsProvider';
import {
PydanticFormConfig,
PydanticFormProps,
PydanticFormValidationErrorDetails,
} from '@/types';

import { PydanticFormHandler } from './core';
import { PydanticFormFieldDataStorageProvider } from './core/PydanticFieldDataStorageProvider';

export const PydanticFormConfigContext =
createContext<PydanticFormConfig | null>(null);

export const PydanticFormValidationErrorContext =
createContext<PydanticFormValidationErrorDetails | null>(null);

export const PydanticForm = ({
config,
formKey,
onCancel,
onSuccess,
title,
}: Omit<PydanticFormContextProviderProps, 'children'>) => (
<TranslationsProvider
customTranslations={config.customTranslations}
locale={config.locale}
>
<PydanticFormContextProvider
config={config}
onCancel={onCancel}
onSuccess={onSuccess}
title={title}
formKey={formKey}
}: PydanticFormProps) => {
return (
<TranslationsProvider
customTranslations={config.customTranslations}
locale={config.locale}
>
{RenderForm}
</PydanticFormContextProvider>
</TranslationsProvider>
);
<PydanticFormConfigContext.Provider value={config}>
<PydanticFormFieldDataStorageProvider>
<PydanticFormHandler
onCancel={onCancel}
onSuccess={onSuccess}
title={title}
formKey={formKey}
/>
</PydanticFormFieldDataStorageProvider>
</PydanticFormConfigContext.Provider>
</TranslationsProvider>
);
};

export default PydanticForm;
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import React from 'react';
import { useFieldArray } from 'react-hook-form';

import { usePydanticFormContext } from '@/core';
import { useGetConfig, useGetForm } from '@/core';
import { fieldToComponentMatcher } from '@/core/helper';
import { PydanticFormElementProps } from '@/types';
import { disableField, itemizeArrayItem } from '@/utils';

import { RenderFields } from '../render';

export const ArrayField = ({ pydanticFormField }: PydanticFormElementProps) => {
const { reactHookForm, config } = usePydanticFormContext();

const { control } = useGetForm();
const { componentMatcherExtender } = useGetConfig();
const disabled = pydanticFormField.attributes?.disabled || false;
const { control } = reactHookForm;

const { id: arrayName, arrayItem } = pydanticFormField;
const { fields, append, remove } = useFieldArray({
control,
Expand All @@ -25,7 +25,7 @@ export const ArrayField = ({ pydanticFormField }: PydanticFormElementProps) => {

const component = fieldToComponentMatcher(
arrayItem,
config?.componentMatcherExtender,
componentMatcherExtender,
);

const renderField = (field: Record<'id', string>, index: number) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,41 @@
*/
import React from 'react';

import { usePydanticFormContext } from '@/core';
import { useGetConfig } from '@/core';
import { useGetValidationErrors } from '@/core';
import { PydanticFormField } from '@/types';

import { FormRow } from './FormRow';

interface FieldWrapProps {
pydanticFormField: PydanticFormField;
isInvalid: boolean;
children: React.ReactNode;
frontendValidationMessage?: string;
}

export const FieldWrap = ({ pydanticFormField, children }: FieldWrapProps) => {
const { errorDetails, reactHookForm, config } = usePydanticFormContext();
const RowRenderer = config?.rowRenderer ? config.rowRenderer : FormRow;
const fieldState = reactHookForm.getFieldState(pydanticFormField.id);
export const FieldWrap = ({
pydanticFormField,
isInvalid,
frontendValidationMessage,
children,
}: FieldWrapProps) => {
const config = useGetConfig();
const validationErrors = useGetValidationErrors();
const RowRenderer = config.rowRenderer ?? FormRow;

const errorMsg =
errorDetails?.mapped?.[pydanticFormField.id]?.msg ??
fieldState.error?.message;
const isInvalid = errorMsg ?? fieldState.invalid;
validationErrors?.mapped?.[pydanticFormField.id]?.msg ??
frontendValidationMessage;
const isInvalidField = errorMsg ?? isInvalid;

return (
<RowRenderer
title={pydanticFormField.title}
description={pydanticFormField.description}
required={pydanticFormField.required}
isInvalid={!!isInvalid}
error={errorMsg as string}
isInvalid={!!isInvalidField}
error={errorMsg}
data-testid={pydanticFormField.id}
>
<div>{children}</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import React from 'react';

import { usePydanticFormContext } from '@/core';
import { useGetForm } from '@/core';
import { PydanticFormElementProps } from '@/types';

export const HiddenField = ({
pydanticFormField,
}: PydanticFormElementProps) => {
const { reactHookForm } = usePydanticFormContext();
const { register } = useGetForm();
return (
<input
type="hidden"
data-testid={pydanticFormField.id}
{...reactHookForm.register(pydanticFormField.id)}
{...register(pydanticFormField.id)}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import React from 'react';

import _ from 'lodash';

import type { PydanticFormControlledElementProps } from '@/types';

export const IntegerField = ({
Expand All @@ -20,8 +18,7 @@ export const IntegerField = ({
onChange(value);
}}
disabled={disabled}
// Value will be an object when it is added by an array field. We do this be able to add more than one empty field
value={_.isObject(value) ? '' : value}
value={value}
type="number"
style={{
padding: '8px',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from 'react';

import { usePydanticFormContext } from '@/core';
import { useGetConfig } from '@/core';
import { getPydanticFormComponents } from '@/core/helper';
import { PydanticFormElementProps } from '@/types';
import { disableField } from '@/utils';
Expand All @@ -10,11 +10,11 @@ import { RenderFields } from '../render';
export const ObjectField = ({
pydanticFormField,
}: PydanticFormElementProps) => {
const { config } = usePydanticFormContext();
const { componentMatcherExtender } = useGetConfig();
const disabled = pydanticFormField.attributes?.disabled || false;
const components = getPydanticFormComponents(
pydanticFormField.properties || {},
config?.componentMatcherExtender,
componentMatcherExtender,
);

// We have decided - for now - on the convention that all descendants of disabled fields will be disabled as well
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
*/
import React from 'react';

import _ from 'lodash';

import { PydanticFormControlledElementProps } from '@/types';

export const TextField = ({
Expand All @@ -23,8 +21,7 @@ export const TextField = ({
onChange(t.currentTarget.value);
}}
disabled={disabled}
// Value will be an object when it is added by an array field. We do this be able to add more than one empty field
value={_.isObject(value) ? '' : value}
value={value}
type="text"
style={{
padding: '8px',
Expand Down
Loading