fh-pydantic-form 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of fh-pydantic-form might be problematic. Click here for more details.
- fh_pydantic_form/__init__.py +90 -0
- fh_pydantic_form/field_renderers.py +1033 -0
- fh_pydantic_form/form_parser.py +537 -0
- fh_pydantic_form/form_renderer.py +713 -0
- fh_pydantic_form/py.typed +0 -0
- fh_pydantic_form/registry.py +145 -0
- fh_pydantic_form/type_helpers.py +42 -0
- fh_pydantic_form-0.1.2.dist-info/METADATA +327 -0
- fh_pydantic_form-0.1.2.dist-info/RECORD +11 -0
- fh_pydantic_form-0.1.2.dist-info/WHEEL +4 -0
- fh_pydantic_form-0.1.2.dist-info/licenses/LICENSE +13 -0
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
from logging import getLogger
|
|
2
|
+
from typing import (
|
|
3
|
+
Any,
|
|
4
|
+
ClassVar,
|
|
5
|
+
Dict,
|
|
6
|
+
List,
|
|
7
|
+
Optional,
|
|
8
|
+
Tuple,
|
|
9
|
+
Type,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from pydantic.fields import FieldInfo
|
|
13
|
+
|
|
14
|
+
from fh_pydantic_form.type_helpers import _get_underlying_type_if_optional
|
|
15
|
+
|
|
16
|
+
logger = getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FieldRendererRegistry:
|
|
20
|
+
"""
|
|
21
|
+
Registry for field renderers with support for type and predicate-based registration
|
|
22
|
+
|
|
23
|
+
This registry manages:
|
|
24
|
+
- Type-specific renderers (e.g., for str, int, bool)
|
|
25
|
+
- Type-name-specific renderers (by class name)
|
|
26
|
+
- Predicate-based renderers (e.g., for Literal fields)
|
|
27
|
+
- List item renderers for specialized list item rendering
|
|
28
|
+
|
|
29
|
+
It uses a singleton pattern to ensure consistent registration across the app.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
_instance = None # Add class attribute to hold the single instance
|
|
33
|
+
|
|
34
|
+
# Use ClassVar for all registry storage
|
|
35
|
+
_type_renderers: ClassVar[Dict[Type, Any]] = {}
|
|
36
|
+
_type_name_renderers: ClassVar[Dict[str, Any]] = {}
|
|
37
|
+
_predicate_renderers: ClassVar[List[Tuple[Any, Any]]] = []
|
|
38
|
+
_list_item_renderers: ClassVar[Dict[Type, Any]] = {}
|
|
39
|
+
|
|
40
|
+
def __new__(cls, *args, **kwargs):
|
|
41
|
+
if cls._instance is None:
|
|
42
|
+
logger.debug("Creating new FieldRendererRegistry singleton instance.")
|
|
43
|
+
cls._instance = super().__new__(cls)
|
|
44
|
+
else:
|
|
45
|
+
logger.debug("Returning existing FieldRendererRegistry singleton instance.")
|
|
46
|
+
return cls._instance
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def register_type_renderer(cls, field_type: Type, renderer_cls: Any) -> None:
|
|
50
|
+
"""Register a renderer for a field type"""
|
|
51
|
+
cls._type_renderers[field_type] = renderer_cls
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def register_type_name_renderer(
|
|
55
|
+
cls, field_type_name: str, renderer_cls: Any
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Register a renderer for a specific field type name"""
|
|
58
|
+
cls._type_name_renderers[field_type_name] = renderer_cls
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def register_type_renderer_with_predicate(cls, predicate_func, renderer_cls):
|
|
62
|
+
"""
|
|
63
|
+
Register a renderer with a predicate function
|
|
64
|
+
|
|
65
|
+
The predicate function should accept a field_info parameter and return
|
|
66
|
+
True if the renderer should be used for that field.
|
|
67
|
+
"""
|
|
68
|
+
cls._predicate_renderers.append((predicate_func, renderer_cls))
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def register_list_item_renderer(cls, item_type: Type, renderer_cls: Any) -> None:
|
|
72
|
+
"""Register a renderer for list items of a specific type"""
|
|
73
|
+
cls._list_item_renderers[item_type] = renderer_cls
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def get_renderer(cls, field_name: str, field_info: FieldInfo) -> Any:
|
|
77
|
+
"""
|
|
78
|
+
Get the appropriate renderer for a field
|
|
79
|
+
|
|
80
|
+
The selection algorithm:
|
|
81
|
+
1. Check exact type matches
|
|
82
|
+
2. Check predicate renderers (for special cases like Literal fields)
|
|
83
|
+
3. Check for subclass relationships
|
|
84
|
+
4. Fall back to string renderer
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
field_name: The name of the field being rendered
|
|
88
|
+
field_info: The FieldInfo for the field
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
A renderer class appropriate for the field
|
|
92
|
+
"""
|
|
93
|
+
# Get the field type (unwrap Optional if present)
|
|
94
|
+
original_annotation = field_info.annotation
|
|
95
|
+
field_type = _get_underlying_type_if_optional(original_annotation)
|
|
96
|
+
|
|
97
|
+
# 1. Check exact type matches first
|
|
98
|
+
if field_type in cls._type_renderers:
|
|
99
|
+
return cls._type_renderers[field_type]
|
|
100
|
+
|
|
101
|
+
# 2. Check predicates second
|
|
102
|
+
for predicate, renderer in cls._predicate_renderers:
|
|
103
|
+
if predicate(field_info):
|
|
104
|
+
return renderer
|
|
105
|
+
|
|
106
|
+
# 3. Check for subclass relationships
|
|
107
|
+
if isinstance(field_type, type):
|
|
108
|
+
for typ, renderer in cls._type_renderers.items():
|
|
109
|
+
try:
|
|
110
|
+
if isinstance(typ, type) and issubclass(field_type, typ):
|
|
111
|
+
return renderer
|
|
112
|
+
except TypeError:
|
|
113
|
+
# Handle non-class types
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
# 4. Fall back to string renderer
|
|
117
|
+
from_imports = globals()
|
|
118
|
+
return from_imports.get("StringFieldRenderer", None)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def get_list_item_renderer(cls, item_type: Type) -> Optional[Any]:
|
|
122
|
+
"""
|
|
123
|
+
Get renderer for summarizing list items of a given type
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
item_type: The type of the list items
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A renderer class for list items, or None if none is registered
|
|
130
|
+
"""
|
|
131
|
+
# Check for exact type match
|
|
132
|
+
if item_type in cls._list_item_renderers:
|
|
133
|
+
return cls._list_item_renderers[item_type]
|
|
134
|
+
|
|
135
|
+
# Check for subclass matches
|
|
136
|
+
for registered_type, renderer in cls._list_item_renderers.items():
|
|
137
|
+
try:
|
|
138
|
+
if isinstance(registered_type, type) and issubclass(
|
|
139
|
+
item_type, registered_type
|
|
140
|
+
):
|
|
141
|
+
return renderer
|
|
142
|
+
except TypeError:
|
|
143
|
+
continue
|
|
144
|
+
|
|
145
|
+
return None
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Any, Literal, Union, get_args, get_origin
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _is_optional_type(annotation: Any) -> bool:
|
|
5
|
+
"""
|
|
6
|
+
Check if an annotation is Optional[T] (Union[T, None]).
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
annotation: The type annotation to check
|
|
10
|
+
|
|
11
|
+
Returns:
|
|
12
|
+
True if the annotation is Optional[T], False otherwise
|
|
13
|
+
"""
|
|
14
|
+
origin = get_origin(annotation)
|
|
15
|
+
if origin is Union:
|
|
16
|
+
args = get_args(annotation)
|
|
17
|
+
# Check if NoneType is one of the args and there are exactly two args
|
|
18
|
+
return len(args) == 2 and type(None) in args
|
|
19
|
+
return False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _get_underlying_type_if_optional(annotation: Any) -> Any:
|
|
23
|
+
"""
|
|
24
|
+
Extract the type T from Optional[T], otherwise return the original annotation.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
annotation: The type annotation, potentially Optional[T]
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The underlying type if Optional, otherwise the original annotation
|
|
31
|
+
"""
|
|
32
|
+
if _is_optional_type(annotation):
|
|
33
|
+
args = get_args(annotation)
|
|
34
|
+
# Return the non-None type
|
|
35
|
+
return args[0] if args[1] is type(None) else args[1]
|
|
36
|
+
return annotation
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_literal_type(annotation: Any) -> bool:
|
|
40
|
+
"""Check if the underlying type of an annotation is Literal."""
|
|
41
|
+
underlying_type = _get_underlying_type_if_optional(annotation)
|
|
42
|
+
return get_origin(underlying_type) is Literal
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fh-pydantic-form
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: a library to turn any pydantic BaseModel object into a fasthtml/monsterui input form
|
|
5
|
+
Project-URL: Homepage, https://github.com/Marcura/fh-pydantic-form
|
|
6
|
+
Project-URL: Repository, https://github.com/Marcura/fh-pydantic-form
|
|
7
|
+
Project-URL: Documentation, https://github.com/Marcura/fh-pydantic-form
|
|
8
|
+
Author-email: Oege Dijk <o.dijk@marcura.com>
|
|
9
|
+
Maintainer-email: Oege Dijk <o.dijk@marcura.com>
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: fasthtml,forms,monsterui,pydantic,ui,web
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: Content Management System
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: monsterui>=1.0.19
|
|
22
|
+
Requires-Dist: pydantic>=2.0
|
|
23
|
+
Requires-Dist: python-fasthtml>=0.12.12
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# fh-pydantic-form
|
|
27
|
+
|
|
28
|
+
**Generate HTML forms from Pydantic models for your FastHTML applications.**
|
|
29
|
+
|
|
30
|
+
`fh-pydantic-form` simplifies creating web forms for [FastHTML](https://github.com/AnswerDotAI/fasthtml) by automatically generating the necessary HTML input elements based on your Pydantic model definitions. It integrates seamlessly with and leverages [MonsterUI](https://github.com/AnswerDotAI/monsterui) components for styling.
|
|
31
|
+
|
|
32
|
+
<details >
|
|
33
|
+
<summary>show demo screen recording</summary>
|
|
34
|
+
<video src="https://private-user-images.githubusercontent.com/27999937/436237879-feabf388-22af-43e6-b054-f103b8a1b6e6.mp4" controls="controls" style="max-width: 730px;">
|
|
35
|
+
</video>
|
|
36
|
+
</details>
|
|
37
|
+
|
|
38
|
+
## Purpose
|
|
39
|
+
|
|
40
|
+
- **Reduce Boilerplate:** Automatically render form inputs (text, number, checkbox, select, date, time, etc.) based on Pydantic field types and annotations.
|
|
41
|
+
- **Data Validation:** Leverage Pydantic's validation rules directly from form submissions.
|
|
42
|
+
- **Nested Structures:** Support for nested Pydantic models and lists of models/simple types.
|
|
43
|
+
- **Dynamic Lists:** Built-in HTMX endpoints and JavaScript for adding, deleting, and reordering items in lists within the form.
|
|
44
|
+
- **Customization:** Easily register custom renderers for specific Pydantic types or fields.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
You can install `fh-pydantic-form` using either `pip` or `uv`.
|
|
49
|
+
|
|
50
|
+
**Using pip:**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install fh-pydantic-form
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Using uv:
|
|
57
|
+
```bash
|
|
58
|
+
uv add fh-pydantic-form
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This will also install necessary dependencies like `pydantic`, `python-fasthtml`, and `monsterui`.
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Basic Usage
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
|
|
69
|
+
# examples/simple_example.py
|
|
70
|
+
import fasthtml.common as fh
|
|
71
|
+
import monsterui.all as mui
|
|
72
|
+
from pydantic import BaseModel, ValidationError
|
|
73
|
+
|
|
74
|
+
# 1. Import the form renderer
|
|
75
|
+
from fh_pydantic_form import PydanticForm
|
|
76
|
+
|
|
77
|
+
app, rt = fh.fast_app(
|
|
78
|
+
hdrs=[
|
|
79
|
+
mui.Theme.blue.headers(),
|
|
80
|
+
# Add list_manipulation_js() if using list fields
|
|
81
|
+
# from fh_pydantic_form import list_manipulation_js
|
|
82
|
+
# list_manipulation_js(),
|
|
83
|
+
],
|
|
84
|
+
pico=False, # Using MonsterUI, not PicoCSS
|
|
85
|
+
live=True, # Enable live reload for development
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# 2. Define your Pydantic model
|
|
89
|
+
class SimpleModel(BaseModel):
|
|
90
|
+
"""Model representing a simple form"""
|
|
91
|
+
name: str = "Default Name"
|
|
92
|
+
age: int
|
|
93
|
+
is_active: bool = True
|
|
94
|
+
|
|
95
|
+
# 3. Create a form renderer instance
|
|
96
|
+
# - 'my_form': Unique name for the form (used for prefixes and routes)
|
|
97
|
+
# - SimpleModel: The Pydantic model class
|
|
98
|
+
form_renderer = PydanticForm("my_form", SimpleModel)
|
|
99
|
+
|
|
100
|
+
# (Optional) Register list manipulation routes if your model has List fields
|
|
101
|
+
# form_renderer.register_routes(app)
|
|
102
|
+
|
|
103
|
+
# 4. Define routes
|
|
104
|
+
@rt("/")
|
|
105
|
+
def get():
|
|
106
|
+
"""Display the form"""
|
|
107
|
+
return fh.Div(
|
|
108
|
+
mui.Container(
|
|
109
|
+
mui.Card(
|
|
110
|
+
mui.CardHeader("Simple Pydantic Form"),
|
|
111
|
+
mui.CardBody(
|
|
112
|
+
# Use MonsterUI Form component for structure
|
|
113
|
+
mui.Form(
|
|
114
|
+
# Render the inputs using the renderer
|
|
115
|
+
form_renderer.render_inputs(),
|
|
116
|
+
# Add standard form buttons
|
|
117
|
+
mui.Button("Submit", type="submit", cls=mui.ButtonT.primary),
|
|
118
|
+
# HTMX attributes for form submission
|
|
119
|
+
hx_post="/submit_form",
|
|
120
|
+
hx_target="#result", # Target div for response
|
|
121
|
+
hx_swap="innerHTML",
|
|
122
|
+
# Set a unique ID for the form itself for refresh/reset inclusion
|
|
123
|
+
id=f"{form_renderer.name}-form",
|
|
124
|
+
)
|
|
125
|
+
),
|
|
126
|
+
),
|
|
127
|
+
# Div to display validation results
|
|
128
|
+
fh.Div(id="result"),
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
@rt("/submit_form")
|
|
133
|
+
async def post_submit_form(req):
|
|
134
|
+
"""Handle form submission and validation"""
|
|
135
|
+
try:
|
|
136
|
+
# 5. Validate the request data against the model
|
|
137
|
+
validated_data: SimpleModel = await form_renderer.model_validate_request(req)
|
|
138
|
+
|
|
139
|
+
# Success: Display the validated data
|
|
140
|
+
return mui.Card(
|
|
141
|
+
mui.CardHeader(fh.H3("Validation Successful")),
|
|
142
|
+
mui.CardBody(
|
|
143
|
+
fh.Pre(
|
|
144
|
+
validated_data.model_dump_json(indent=2),
|
|
145
|
+
)
|
|
146
|
+
),
|
|
147
|
+
cls="mt-4",
|
|
148
|
+
)
|
|
149
|
+
except ValidationError as e:
|
|
150
|
+
# Validation Error: Display the errors
|
|
151
|
+
return mui.Card(
|
|
152
|
+
mui.CardHeader(fh.H3("Validation Error", cls="text-red-500")),
|
|
153
|
+
mui.CardBody(
|
|
154
|
+
fh.Pre(
|
|
155
|
+
e.json(indent=2),
|
|
156
|
+
)
|
|
157
|
+
),
|
|
158
|
+
cls="mt-4",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if __name__ == "__main__":
|
|
162
|
+
fh.serve()
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
## Key Features
|
|
166
|
+
|
|
167
|
+
- **Automatic Field Rendering:** Handles `str`, `int`, `float`, `bool`, `date`, `time`, `Optional`, `Literal`, nested `BaseModel`s, and `List`s out-of-the-box.
|
|
168
|
+
- **Sensible Defaults:** Uses appropriate HTML5 input types (`text`, `number`, `date`, `time`, `checkbox`, `select`).
|
|
169
|
+
- **Labels & Placeholders:** Generates labels from field names (converting snake_case to Title Case) and basic placeholders.
|
|
170
|
+
- **Descriptions as Tooltips:** Uses `Field(description=...)` from Pydantic to create tooltips (`uk-tooltip` via UIkit).
|
|
171
|
+
- **Required Fields:** Automatically adds the `required` attribute based on field definitions (considering `Optional` and defaults).
|
|
172
|
+
- **Disabled Fields:** Disable the whole form with `disabled=True` or disable specific fields with `disabled_fields`
|
|
173
|
+
- **Collapsible Nested Models:** Renders nested Pydantic models in collapsible details/summary elements for better form organization and space management.
|
|
174
|
+
- **List Manipulation:**
|
|
175
|
+
- Renders lists of simple types or models in accordion-style cards with an enhanced UI.
|
|
176
|
+
- Provides HTMX endpoints (registered via `register_routes`) for adding and deleting list items.
|
|
177
|
+
- Includes JavaScript (`list_manipulation_js()`) for client-side reordering (moving items up/down).
|
|
178
|
+
- **Form Refresh & Reset:**
|
|
179
|
+
- Provides HTMX-powered "Refresh" and "Reset" buttons (`form_renderer.refresh_button()`, `form_renderer.reset_button()`).
|
|
180
|
+
- Refresh updates list item summaries or other dynamic parts without full page reload.
|
|
181
|
+
- Reset reverts the form to its initial values.
|
|
182
|
+
- **Custom Renderers:** Register your own `BaseFieldRenderer` subclasses for specific Pydantic types or complex field logic using `FieldRendererRegistry` or by passing `custom_renderers` during `PydanticForm` initialization.
|
|
183
|
+
- **Form Data Parsing:** Includes logic (`form_renderer.parse` and `form_renderer.model_validate_request`) to correctly parse submitted form data (handling prefixes, list indices, nested structures, boolean checkboxes, etc.) back into a dictionary suitable for Pydantic validation.
|
|
184
|
+
|
|
185
|
+
## disabled fields
|
|
186
|
+
|
|
187
|
+
You can disable the full form with `PydanticForm("my_form", FormModel, disabled=True)` or disable specific fields with `PydanticForm("my_form", FormModel, disabled_fields=["field1", "field3"])`.
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
## Manipulating lists fields
|
|
191
|
+
|
|
192
|
+
When you have `BaseModels` with fields that are e.g. `List[str]` or even `List[BaseModel]` you want to be able to easily edit the list by adding, deleting and moving items. For this we need a little bit of javascript and register some additional routes:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from fh_pydantic_form import PydanticForm, list_manipulation_js
|
|
196
|
+
|
|
197
|
+
app, rt = fh.fast_app(
|
|
198
|
+
hdrs=[
|
|
199
|
+
mui.Theme.blue.headers(),
|
|
200
|
+
list_manipulation_js(),
|
|
201
|
+
],
|
|
202
|
+
pico=False,
|
|
203
|
+
live=True,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class ListModel(BaseModel):
|
|
208
|
+
name: str = ""
|
|
209
|
+
tags: List[str] = Field(["tag1", "tag2"])
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
form_renderer = PydanticForm("list_model", ListModel)
|
|
213
|
+
form_renderer.register_routes(app)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Refreshing and resetting the form
|
|
217
|
+
|
|
218
|
+
You can set the initial values of the form by passing an instantiated BaseModel:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
form_renderer = PydanticForm("my_form", ListModel, initial_values=ListModel(name="John", tags=["happy", "joy"]))
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
You can reset the form back to these initial values by adding a `form_render.reset_button()` to your UI:
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
mui.Form(
|
|
228
|
+
form_renderer.render_inputs(),
|
|
229
|
+
fh.Div(
|
|
230
|
+
mui.Button("Validate and Show JSON",cls=mui.ButtonT.primary,),
|
|
231
|
+
form_renderer.refresh_button(),
|
|
232
|
+
form_renderer.reset_button(),
|
|
233
|
+
),
|
|
234
|
+
hx_post="/submit_form",
|
|
235
|
+
hx_target="#result",
|
|
236
|
+
hx_swap="innerHTML",
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The refresh button 🔄 refreshes the list item labels. These are rendered initially to summarize the underlying item, but do not automatically update after editing unless refreshed. You can also use the 🔄 icon next to the list field label.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Custom renderers
|
|
244
|
+
|
|
245
|
+
The library is extensible by adding your own input renderers for your types. This can be used to override e.g. the default BaseModelFieldRenderer for nested BaseModels, but also to register types that are not (yet) supported (but submit a PR then as well!)
|
|
246
|
+
|
|
247
|
+
You can register a renderer based on type, type str, or a predicate function:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from fh_pydantic_form import FieldRendererRegistry
|
|
251
|
+
|
|
252
|
+
from fh_pydantic_form.field_renderers import BaseFieldRenderer
|
|
253
|
+
|
|
254
|
+
class CustomDetail(BaseModel):
|
|
255
|
+
value: str = "Default value"
|
|
256
|
+
confidence: Literal["HIGH", "MEDIUM", "LOW"] = "MEDIUM"
|
|
257
|
+
|
|
258
|
+
def __str__(self) -> str:
|
|
259
|
+
return f"{self.value} ({self.confidence})"
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class CustomDetailFieldRenderer(BaseFieldRenderer):
|
|
263
|
+
"""display value input and dropdown side by side"""
|
|
264
|
+
|
|
265
|
+
def render_input(self):
|
|
266
|
+
value_input = fh.Div(
|
|
267
|
+
mui.Input(
|
|
268
|
+
value=self.value.get("value", ""),
|
|
269
|
+
id=f"{self.field_name}_value",
|
|
270
|
+
name=f"{self.field_name}_value",
|
|
271
|
+
placeholder=f"Enter {self.original_field_name.replace('_', ' ')} value",
|
|
272
|
+
cls="uk-input w-full",
|
|
273
|
+
),
|
|
274
|
+
cls="flex-grow", # apply some custom css
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
confidence_options_ft = [
|
|
278
|
+
fh.Option(
|
|
279
|
+
opt, value=opt, selected=(opt == self.value.get("confidence", "MEDIUM"))
|
|
280
|
+
)
|
|
281
|
+
for opt in ["HIGH", "MEDIUM", "LOW"]
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
confidence_select = mui.Select(
|
|
285
|
+
*confidence_options_ft,
|
|
286
|
+
id=f"{self.field_name}_confidence",
|
|
287
|
+
name=f"{self.field_name}_confidence",
|
|
288
|
+
cls_wrapper="w-[110px] min-w-[110px] flex-shrink-0", # apply some custom css
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
return fh.Div(
|
|
292
|
+
value_input,
|
|
293
|
+
confidence_select,
|
|
294
|
+
cls="flex items-start gap-2 w-full", # apply some custom css
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
# these are all equivalent. You can either register the type directly
|
|
299
|
+
FieldRendererRegistry.register_type_renderer(CustomDetail, CustomDetailFieldRender)
|
|
300
|
+
# or just by the name of the type
|
|
301
|
+
FieldRendererRegistry.register_type_name_renderer("CustomDetail", CustomDetailFieldRender)
|
|
302
|
+
# or register I predicate function
|
|
303
|
+
FieldRendererRegistry.register_type_renderer_with_predicate(lambda: x: isinstance(x, CustomDetail), CustomDetailFieldRender)
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
You can also pass these directly to the `PydanticForm` with the custom_renderers argument:
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
|
|
310
|
+
form_renderer = PydanticForm(
|
|
311
|
+
form_name="main_form",
|
|
312
|
+
model_class=ComplexSchema,
|
|
313
|
+
initial_values=initial_values,
|
|
314
|
+
custom_renderers=[
|
|
315
|
+
(CustomDetail, CustomDetailFieldRenderer)
|
|
316
|
+
], # Register Detail renderer
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Contributing
|
|
321
|
+
|
|
322
|
+
Contributions are welcome! Please feel free to open an issue or submit a pull request.
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
fh_pydantic_form/__init__.py,sha256=oifnGdBOS1XaO9Q8gcZ1SJ98Xzz9TfbZH9kvHg9_beo,3056
|
|
2
|
+
fh_pydantic_form/field_renderers.py,sha256=zgGEgR09Oop_-lyYvcHw52L7Uz4X26dWcxyFqUCBSN0,39461
|
|
3
|
+
fh_pydantic_form/form_parser.py,sha256=3EGy4YHbLskH76V0ieTG8zmQ0b02ty8bOZf9AEg89ZY,20629
|
|
4
|
+
fh_pydantic_form/form_renderer.py,sha256=U4njGebyysKEy88wd2oSf756nndXm1qYc0Dxm_ltmH8,27709
|
|
5
|
+
fh_pydantic_form/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
fh_pydantic_form/registry.py,sha256=sufK-85ST3rc3Vu0XmjjjdTqTAqgHr_ZbMGU0xRgTK8,4996
|
|
7
|
+
fh_pydantic_form/type_helpers.py,sha256=ZCU8m4xxFk_ofMsAwxb8CunZhsDBsrEFjpJdtncreT0,1322
|
|
8
|
+
fh_pydantic_form-0.1.2.dist-info/METADATA,sha256=D6xJkgYCio3dhL4m1JIwa2L26kSRIo9MrR3dPYZXAKY,12555
|
|
9
|
+
fh_pydantic_form-0.1.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
fh_pydantic_form-0.1.2.dist-info/licenses/LICENSE,sha256=AOi2eNK3D2aDycRHfPRiuACZ7WPBsKHTV2tTYNl7cls,577
|
|
11
|
+
fh_pydantic_form-0.1.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Copyright 2025 Marcura
|
|
2
|
+
|
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License.
|
|
5
|
+
You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
See the License for the specific language governing permissions and
|
|
13
|
+
limitations under the License.
|