diff --git a/demo/forms.py b/demo/forms.py index 886ae77e..ff9cb010 100644 --- a/demo/forms.py +++ b/demo/forms.py @@ -141,7 +141,10 @@ class SizeModel(BaseModel): class BigModel(BaseModel): name: str | None = Field( - None, description='This field is not required, it must start with a capital letter if provided' + None, + max_length=10, + min_length=2, + description='This field is not required, it must start with a capital letter if provided, and have length 2-10', ) info: Annotated[str | None, Textarea(rows=5)] = Field(None, description='Optional free text information about you.') profile_pic: Annotated[UploadFile, FormFile(accept='image/*', max_size=16_000)] = Field( @@ -154,6 +157,9 @@ class BigModel(BaseModel): human: bool | None = Field( None, title='Is human', description='Are you human?', json_schema_extra={'mode': 'switch'} ) + number: int | None = Field( + None, title='Number', ge=0, le=10, multiple_of=2, description='This is a number should be 0-10 and step with 2' + ) size: SizeModel position: tuple[ diff --git a/src/npm-fastui/src/components/FormField.tsx b/src/npm-fastui/src/components/FormField.tsx index d1b30c78..41034235 100644 --- a/src/npm-fastui/src/components/FormField.tsx +++ b/src/npm-fastui/src/components/FormField.tsx @@ -24,7 +24,8 @@ interface FormFieldInputProps extends FormFieldInput { } export const FormFieldInputComp: FC = (props) => { - const { name, placeholder, required, htmlType, locked, autocomplete } = props + const { name, placeholder, required, htmlType, locked, autocomplete, maxLength, minLength, ge, le, multipleOf } = + props return (
@@ -40,6 +41,11 @@ export const FormFieldInputComp: FC = (props) => { placeholder={placeholder} autoComplete={autocomplete} aria-describedby={descId(props)} + maxLength={maxLength} + minLength={minLength} + min={ge} + max={le} + step={multipleOf} />
diff --git a/src/npm-fastui/src/models.d.ts b/src/npm-fastui/src/models.d.ts index a0ca91cd..35f78643 100644 --- a/src/npm-fastui/src/models.d.ts +++ b/src/npm-fastui/src/models.d.ts @@ -353,6 +353,13 @@ export interface FormFieldInput { initial?: string | number placeholder?: string autocomplete?: string + maxLength?: number + minLength?: number + ge?: number | number + le?: number | number + gt?: number | number + lt?: number | number + multipleOf?: number | number type: 'FormFieldInput' } export interface FormFieldTextarea { diff --git a/src/python-fastui/fastui/components/forms.py b/src/python-fastui/fastui/components/forms.py index a18a9c4c..64ba0f9a 100644 --- a/src/python-fastui/fastui/components/forms.py +++ b/src/python-fastui/fastui/components/forms.py @@ -32,6 +32,13 @@ class FormFieldInput(BaseFormField): initial: _t.Union[str, float, None] = None placeholder: _t.Union[str, None] = None autocomplete: _t.Union[str, None] = None + max_length: _t.Union[int, None] = pydantic.Field(default=None, serialization_alias='maxLength') + min_length: _t.Union[int, None] = pydantic.Field(default=None, serialization_alias='minLength') + ge: _t.Union[int, float, None] = None + le: _t.Union[int, float, None] = None + gt: _t.Union[int, float, None] = None + lt: _t.Union[int, float, None] = None + multiple_of: _t.Union[int, float, None] = pydantic.Field(default=None, serialization_alias='multipleOf') type: _t.Literal['FormFieldInput'] = 'FormFieldInput' diff --git a/src/python-fastui/fastui/json_schema.py b/src/python-fastui/fastui/json_schema.py index 49492796..fea4492d 100644 --- a/src/python-fastui/fastui/json_schema.py +++ b/src/python-fastui/fastui/json_schema.py @@ -47,6 +47,8 @@ class JsonSchemaString(JsonSchemaBase): type: _ta.Required[_t.Literal['string']] default: str format: _t.Literal['date', 'date-time', 'time', 'email', 'uri', 'uuid', 'password'] + maxLength: int + minLength: int class JsonSchemaStringEnum(JsonSchemaBase, total=False): @@ -197,6 +199,13 @@ def json_schema_field_to_field( initial=schema.get('default'), autocomplete=schema.get('autocomplete'), description=schema.get('description'), + max_length=schema.get('maxLength'), + min_length=schema.get('minLength'), + ge=schema.get('minimum'), + le=schema.get('maximum'), + gt=schema.get('exclusiveMinimum'), + lt=schema.get('exclusiveMaximum'), + multiple_of=schema.get('multipleOf'), ) diff --git a/src/python-fastui/tests/test_forms.py b/src/python-fastui/tests/test_forms.py index b0919fad..ba949e61 100644 --- a/src/python-fastui/tests/test_forms.py +++ b/src/python-fastui/tests/test_forms.py @@ -6,7 +6,7 @@ from fastapi import HTTPException from fastui import components from fastui.forms import FormFile, Textarea, fastui_form -from pydantic import BaseModel +from pydantic import BaseModel, Field from starlette.datastructures import FormData, Headers, UploadFile from typing_extensions import Annotated @@ -16,6 +16,11 @@ class SimpleForm(BaseModel): size: int = 4 +class FormWithConstraints(BaseModel): + name: str = Field(..., max_length=10, min_length=2, description='This field is required, it must have length 2-10') + size: int = Field(4, ge=0, le=10, multiple_of=2, description='size with range 0-10 and step with 2') + + class FakeRequest: """ TODO replace this with httpx or similar maybe, perhaps this is sufficient @@ -89,6 +94,42 @@ def test_inline_form_fields(): } +def test_form_with_constraints_fields(): + m = components.ModelForm(model=FormWithConstraints, submit_url='/foobar/') + + assert m.model_dump(by_alias=True, exclude_none=True) == { + 'submitUrl': '/foobar/', + 'method': 'POST', + 'type': 'ModelForm', + 'formFields': [ + { + 'name': 'name', + 'title': ['Name'], + 'required': True, + 'locked': False, + 'htmlType': 'text', + 'type': 'FormFieldInput', + 'description': 'This field is required, it must have length 2-10', + 'maxLength': 10, + 'minLength': 2, + }, + { + 'name': 'size', + 'title': ['Size'], + 'initial': 4, + 'required': False, + 'locked': False, + 'htmlType': 'number', + 'type': 'FormFieldInput', + 'description': 'size with range 0-10 and step with 2', + 'le': 10, + 'ge': 0, + 'multipleOf': 2, + }, + ], + } + + async def test_simple_form_submit(): form_dep = fastui_form(SimpleForm)