Skip to content

Commit

Permalink
Merge pull request #29 from workfloworchestrator/1785-npm-release
Browse files Browse the repository at this point in the history
1785 npm release
  • Loading branch information
DutchBen authored Mar 6, 2025
2 parents 2c55366 + 844c11b commit 298d91b
Show file tree
Hide file tree
Showing 32 changed files with 732 additions and 566 deletions.
24 changes: 19 additions & 5 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from pydantic import ConfigDict, Field
from pydantic import BaseModel, ConfigDict, Field
from pydantic_forms.core import FormPage, post_form
from pydantic_forms.types import State
from pydantic_forms.exception_handlers.fastapi import form_error_handler
Expand Down Expand Up @@ -112,6 +112,17 @@ class ListChoices(Choice):
_6 = ("6", "Option 6")


class Education(BaseModel):
degree: str | None
year: int | None


class Person(BaseModel):
name: str
age: Annotated[int, Ge(18), Le(99)]
education: Education


@app.post("/form")
async def form(form_data: list[dict] = []):
def form_generator(state: State):
Expand All @@ -120,18 +131,21 @@ class TestForm(FormPage):

number: NumberExample = 3
text: Annotated[str, Field(min_length=3, max_length=12)] = "Default text"
textArea: LongText = "Default text area"
textArea: LongText = "Text area default"
divider: Divider
label: Label = "Label"
hidden: Hidden = "Hidden"
# When there are > 3 choices a dropdown will be rendered
dropdown: DropdownChoices = "2"
# When there are <= 3 choices a radio group will be rendered
radio: RadioChoices = "3"
checkbox: bool = True
# checkbox: bool = True TODO: Fix validation errors on this

# When there are <= 5 choices in a list a set of checkboxes are rendered
multicheckbox: choice_list(MultiCheckBoxChoices) = ["1", "2"]
list: choice_list(ListChoices) = [0, 1]
# multicheckbox: choice_list(MultiCheckBoxChoices, min_items=3) = ["1", "2"]
# list: choice_list(ListChoices) = [0, 1]

person: Person

form_data_1 = yield TestForm

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

Adds object field
14 changes: 10 additions & 4 deletions frontend/apps/example/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
'use client';

import { PydanticForm, PydanticFormFieldType } from 'pydantic-forms';
import {
PydanticForm,
PydanticFormFieldFormat,
PydanticFormFieldType,
} from 'pydantic-forms';
import type {
PydanticComponentMatcher,
PydanticFormApiProvider,
Expand Down Expand Up @@ -60,19 +64,21 @@ export default function Home() {
const componentMatcher = (
currentMatchers: PydanticComponentMatcher[],
): PydanticComponentMatcher[] => {
return currentMatchers;
return [
...currentMatchers,
{
id: 'textarea',
ElementMatch: {
Element: TextArea,
isControlledElement: true,
},
matcher(field) {
return field.type === PydanticFormFieldType.STRING;
return (
field.type === PydanticFormFieldType.STRING &&
field.format === PydanticFormFieldFormat.LONG
);
},
},
...currentMatchers,
];
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useForm } from 'react-hook-form';

import { z } from 'zod';

import defaultComponentMatchers from '@/components/defaultComponentMatchers';
import { TextField } from '@/components/fields';
import type {
ElementMatch,
Properties,
PydanticComponentMatcher,
PydanticFormComponents,
PydanticFormField,
PydanticFormsContextConfig,
} from '@/types';

export const getMatcher = (
customComponentMatcher: PydanticFormsContextConfig['componentMatcher'],
) => {
const componentMatchers = customComponentMatcher
? customComponentMatcher(defaultComponentMatchers)
: defaultComponentMatchers;

return (field: PydanticFormField): PydanticComponentMatcher | undefined => {
return componentMatchers.find(({ matcher }) => {
return matcher(field);
});
};
};

export const getClientSideValidationRule = (
field: PydanticFormField,
rhf?: ReturnType<typeof useForm>,
customComponentMatcher?: PydanticFormsContextConfig['componentMatcher'],
) => {
const matcher = getMatcher(customComponentMatcher);

const componentMatch = matcher(field);

let validationRule = componentMatch?.validator?.(field, rhf) ?? z.string();

if (!field.required) {
validationRule = validationRule.optional();
}

if (field.validations.isNullable) {
validationRule = validationRule.nullable();
}

return validationRule;
};

export const componentMatcher = (
properties: Properties,
customComponentMatcher: PydanticFormsContextConfig['componentMatcher'],
): PydanticFormComponents => {
const matcher = getMatcher(customComponentMatcher);

const components: PydanticFormComponents = Object.entries(properties).map(
([, pydanticFormField]) => {
const matchedComponent = matcher(pydanticFormField);

const ElementMatch: ElementMatch = matchedComponent
? matchedComponent.ElementMatch
: {
Element: TextField,
isControlledElement: true,
};

// Defaults to textField when there are no matches
return {
Element: ElementMatch,
pydanticFormField: pydanticFormField,
};
},
);

return components;
};
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import {
PydanticFormFieldType,
} from '@/types';

import { zodValidationPresets } from './zodValidations';
import { ObjectField } from './fields/ObjectField';
import { zodValidationPresets } from './zodValidationsPresets';

const defaultComponentMatchers: PydanticComponentMatcher[] = [
{
Expand Down Expand Up @@ -152,6 +153,16 @@ const defaultComponentMatchers: PydanticComponentMatcher[] = [
},
validator: zodValidationPresets.array,
},
{
id: 'object',
ElementMatch: {
Element: ObjectField,
isControlledElement: false,
},
matcher(field) {
return field.type === PydanticFormFieldType.OBJECT;
},
},
];

// If nothing matches, it defaults to Text field in the mapToComponent function
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';

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

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

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

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

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

const components = componentMatcher(
pydanticFormField.properties || {},
config?.componentMatcher,
);

return (
<div>
<h1>{pydanticFormField.title}</h1>
<RenderFields components={components} />
</div>
);
};
11 changes: 1 addition & 10 deletions frontend/packages/pydantic-forms/src/components/form/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/
import React from 'react';

import RenderReactHookFormErrors from '@/components/render/RenderReactHookFormErrors';
import { usePydanticFormContext } from '@/core';

const Footer = () => {
Expand All @@ -20,17 +19,9 @@ const Footer = () => {
allowUntouchedSubmit,
} = usePydanticFormContext();

const hasErrors = !!Object.keys(rhf.formState.errors).length;

return (
<div style={{ height: '200px' }}>
{(!!footerComponent || hasErrors) && (
<div>
{footerComponent}

{<RenderReactHookFormErrors />}
</div>
)}
{footerComponent && <div>{footerComponent}</div>}{' '}
<div>
{resetButtonAlternative ?? (
<button
Expand Down
43 changes: 0 additions & 43 deletions frontend/packages/pydantic-forms/src/components/render/Fields.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import React from 'react';

import { RenderFields, RenderSections } from '@/components/render';
import { getFieldBySection } from '@/core/helper';
import type { PydanticFormData, FormRenderer as Renderer } from '@/types';

export const FormRenderer: Renderer = ({
pydanticFormData,
}: {
pydanticFormData: PydanticFormData;
}) => {
const formSections = getFieldBySection(pydanticFormData.fields);
import type { FormRenderer as Renderer } from '@/types';

export const FormRenderer: Renderer = ({ pydanticFormComponents }) => {
const formSections = getFieldBySection(pydanticFormComponents);
const sections = formSections.map((section) => (
<RenderSections section={section} key={section.id}>
{({ fields }) => (
<div>
<RenderFields fields={fields} />
</div>
)}
<RenderSections
section={section}
key={section.id}
components={pydanticFormComponents}
>
{({ components }) => <RenderFields components={components} />}
</RenderSections>
));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Pydantic Forms
*
* This component will render all the fields based on the
* config in the pydanticFormContext
*/
import React from 'react';

import { WrapFieldElement } from '@/core/WrapFieldElement';
import { PydanticFormComponents, PydanticFormField } from '@/types';

interface RenderFieldsProps {
components: PydanticFormComponents;
}

export function RenderFields({ components }: RenderFieldsProps) {
return components.map((component) => {
const { Element, isControlledElement } = component.Element;
const field: PydanticFormField = component.pydanticFormField;

if (!Element) {
return undefined;
}

if (isControlledElement) {
return (
<div key={field.id}>
<WrapFieldElement
PydanticFormControlledElement={Element}
pydanticFormField={field}
/>
</div>
);
} else {
return <Element pydanticFormField={field} key={field.id} />;
}
});
}
Loading

0 comments on commit 298d91b

Please sign in to comment.