fh-pydantic-form 0.2.0__py3-none-any.whl → 0.2.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/field_renderers.py +237 -92
- fh_pydantic_form/form_parser.py +78 -45
- fh_pydantic_form/form_renderer.py +132 -119
- fh_pydantic_form/list_path.py +145 -0
- fh_pydantic_form/ui_style.py +78 -47
- {fh_pydantic_form-0.2.0.dist-info → fh_pydantic_form-0.2.2.dist-info}/METADATA +4 -14
- fh_pydantic_form-0.2.2.dist-info/RECORD +14 -0
- fh_pydantic_form-0.2.0.dist-info/RECORD +0 -13
- {fh_pydantic_form-0.2.0.dist-info → fh_pydantic_form-0.2.2.dist-info}/WHEEL +0 -0
- {fh_pydantic_form-0.2.0.dist-info → fh_pydantic_form-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -3,6 +3,7 @@ from datetime import date, time
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import (
|
|
5
5
|
Any,
|
|
6
|
+
List,
|
|
6
7
|
Optional,
|
|
7
8
|
get_args,
|
|
8
9
|
get_origin,
|
|
@@ -61,6 +62,8 @@ class BaseFieldRenderer:
|
|
|
61
62
|
disabled: bool = False,
|
|
62
63
|
label_color: Optional[str] = None,
|
|
63
64
|
spacing: SpacingValue = SpacingTheme.NORMAL,
|
|
65
|
+
field_path: Optional[List[str]] = None,
|
|
66
|
+
form_name: Optional[str] = None,
|
|
64
67
|
):
|
|
65
68
|
"""
|
|
66
69
|
Initialize the field renderer
|
|
@@ -73,17 +76,61 @@ class BaseFieldRenderer:
|
|
|
73
76
|
disabled: Whether the field should be rendered as disabled
|
|
74
77
|
label_color: Optional CSS color value for the field label
|
|
75
78
|
spacing: Spacing theme to use for layout ("normal", "compact", or SpacingTheme enum)
|
|
79
|
+
field_path: Path segments from root to this field (for nested list support)
|
|
80
|
+
form_name: Explicit form name (used for nested list URLs)
|
|
76
81
|
"""
|
|
77
82
|
self.field_name = f"{prefix}{field_name}" if prefix else field_name
|
|
78
83
|
self.original_field_name = field_name
|
|
79
84
|
self.field_info = field_info
|
|
80
85
|
self.value = value
|
|
81
86
|
self.prefix = prefix
|
|
87
|
+
self.field_path: List[str] = field_path or []
|
|
88
|
+
self.explicit_form_name: Optional[str] = form_name
|
|
82
89
|
self.is_optional = _is_optional_type(field_info.annotation)
|
|
83
90
|
self.disabled = disabled
|
|
84
91
|
self.label_color = label_color
|
|
85
92
|
self.spacing = _normalize_spacing(spacing)
|
|
86
93
|
|
|
94
|
+
def _is_inline_color(self, color: str) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Determine if a color should be applied as an inline style or CSS class.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
color: The color value to check
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if the color should be applied as inline style, False if as CSS class
|
|
103
|
+
"""
|
|
104
|
+
# Check if it's a hex color value (starts with #) or basic HTML color name
|
|
105
|
+
return color.startswith("#") or color in [
|
|
106
|
+
"red",
|
|
107
|
+
"blue",
|
|
108
|
+
"green",
|
|
109
|
+
"yellow",
|
|
110
|
+
"orange",
|
|
111
|
+
"purple",
|
|
112
|
+
"pink",
|
|
113
|
+
"cyan",
|
|
114
|
+
"magenta",
|
|
115
|
+
"brown",
|
|
116
|
+
"black",
|
|
117
|
+
"white",
|
|
118
|
+
"gray",
|
|
119
|
+
"grey",
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
def _get_color_class(self, color: str) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Get the appropriate CSS class for a color.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
color: The color name
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
The CSS class string for the color
|
|
131
|
+
"""
|
|
132
|
+
return f"text-{color}-600"
|
|
133
|
+
|
|
87
134
|
def render_label(self) -> FT:
|
|
88
135
|
"""
|
|
89
136
|
Render label for the field
|
|
@@ -118,13 +165,12 @@ class BaseFieldRenderer:
|
|
|
118
165
|
|
|
119
166
|
# Apply color styling if specified
|
|
120
167
|
if self.label_color:
|
|
121
|
-
|
|
122
|
-
if self.label_color.replace("-", "").replace("#", "").isalnum():
|
|
123
|
-
# Looks like a CSS class, add it to the class list
|
|
124
|
-
cls_attr = f"block text-sm font-medium {self.label_color} {label_gap_class}".strip()
|
|
125
|
-
else:
|
|
168
|
+
if self._is_inline_color(self.label_color):
|
|
126
169
|
# Treat as color value
|
|
127
170
|
label_attrs["style"] = f"color: {self.label_color};"
|
|
171
|
+
else:
|
|
172
|
+
# Treat as CSS class (includes Tailwind colors like emerald, amber, rose, teal, indigo, lime, violet, etc.)
|
|
173
|
+
cls_attr = f"block text-sm font-medium {self._get_color_class(self.label_color)} {label_gap_class}".strip()
|
|
128
174
|
|
|
129
175
|
# Create and return the label - using standard fh.Label with appropriate styling
|
|
130
176
|
return fh.Label(
|
|
@@ -147,49 +193,38 @@ class BaseFieldRenderer:
|
|
|
147
193
|
|
|
148
194
|
def render(self) -> FT:
|
|
149
195
|
"""
|
|
150
|
-
Render the complete field (label + input) with spacing
|
|
196
|
+
Render the complete field (label + input) with spacing
|
|
197
|
+
|
|
198
|
+
For compact spacing: renders label and input side-by-side
|
|
199
|
+
For normal spacing: renders label above input (traditional)
|
|
151
200
|
|
|
152
201
|
Returns:
|
|
153
|
-
A FastHTML component
|
|
202
|
+
A FastHTML component containing the complete field
|
|
154
203
|
"""
|
|
155
|
-
# 1. Get the
|
|
204
|
+
# 1. Get the label component
|
|
156
205
|
label_component = self.render_label()
|
|
157
206
|
|
|
158
|
-
#
|
|
159
|
-
if self.label_color and isinstance(label_component, fh.FT):
|
|
160
|
-
if "style" in label_component.attrs:
|
|
161
|
-
label_component.attrs["style"] += f" color: {self.label_color};"
|
|
162
|
-
else:
|
|
163
|
-
label_component.attrs["style"] = f"color: {self.label_color};"
|
|
164
|
-
|
|
165
|
-
# 2. Render the input field that will be the accordion content
|
|
207
|
+
# 2. Render the input field
|
|
166
208
|
input_component = self.render_input()
|
|
167
209
|
|
|
168
|
-
# 3.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
id=accordion_id, # ID for the accordion container (ul)
|
|
187
|
-
multiple=True, # Allow multiple open (though only one exists)
|
|
188
|
-
collapsible=True, # Allow toggling
|
|
189
|
-
cls=f"{spacing('accordion_divider', self.spacing)} {spacing('accordion_content', self.spacing)}".strip(),
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
return accordion_container
|
|
210
|
+
# 3. Choose layout based on spacing theme
|
|
211
|
+
if self.spacing == SpacingTheme.COMPACT:
|
|
212
|
+
# Horizontal layout for compact mode
|
|
213
|
+
return fh.Div(
|
|
214
|
+
fh.Div(
|
|
215
|
+
label_component,
|
|
216
|
+
input_component,
|
|
217
|
+
cls=f"flex {spacing('horizontal_gap', self.spacing)} {spacing('label_align', self.spacing)} w-full",
|
|
218
|
+
),
|
|
219
|
+
cls=f"{spacing('outer_margin', self.spacing)} w-full",
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
# Vertical layout for normal mode (existing behavior)
|
|
223
|
+
return fh.Div(
|
|
224
|
+
label_component,
|
|
225
|
+
input_component,
|
|
226
|
+
cls=spacing("outer_margin", self.spacing),
|
|
227
|
+
)
|
|
193
228
|
|
|
194
229
|
|
|
195
230
|
# ---- Specific Field Renderers ----
|
|
@@ -301,6 +336,29 @@ class BooleanFieldRenderer(BaseFieldRenderer):
|
|
|
301
336
|
|
|
302
337
|
return mui.CheckboxX(**checkbox_attrs)
|
|
303
338
|
|
|
339
|
+
def render(self) -> FT:
|
|
340
|
+
"""
|
|
341
|
+
Render the complete field (label + input) with spacing, placing the checkbox next to the label.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
A FastHTML component containing the complete field
|
|
345
|
+
"""
|
|
346
|
+
# Get the label component
|
|
347
|
+
label_component = self.render_label()
|
|
348
|
+
|
|
349
|
+
# Get the checkbox component
|
|
350
|
+
checkbox_component = self.render_input()
|
|
351
|
+
|
|
352
|
+
# Create a flex container to place label and checkbox side by side
|
|
353
|
+
return fh.Div(
|
|
354
|
+
fh.Div(
|
|
355
|
+
label_component,
|
|
356
|
+
checkbox_component,
|
|
357
|
+
cls="flex items-center gap-2 w-full", # Use flexbox to align items horizontally with a small gap
|
|
358
|
+
),
|
|
359
|
+
cls=f"{spacing('outer_margin', self.spacing)} w-full",
|
|
360
|
+
)
|
|
361
|
+
|
|
304
362
|
|
|
305
363
|
class DateFieldRenderer(BaseFieldRenderer):
|
|
306
364
|
"""Renderer for date fields"""
|
|
@@ -559,26 +617,36 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
559
617
|
Returns:
|
|
560
618
|
A FastHTML component (mui.Accordion) containing the accordion structure.
|
|
561
619
|
"""
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
620
|
+
|
|
621
|
+
# Extract the label text and apply color styling
|
|
622
|
+
label_text = self.original_field_name.replace("_", " ").title()
|
|
623
|
+
|
|
624
|
+
# Create the title component with proper color styling
|
|
625
|
+
if self.label_color:
|
|
626
|
+
if self._is_inline_color(self.label_color):
|
|
627
|
+
# Color value - apply as inline style
|
|
628
|
+
title_component = fh.Span(
|
|
629
|
+
label_text,
|
|
630
|
+
style=f"color: {self.label_color};",
|
|
631
|
+
cls="text-sm font-medium",
|
|
632
|
+
)
|
|
633
|
+
else:
|
|
634
|
+
# CSS class - apply as Tailwind class (includes emerald, amber, rose, teal, indigo, lime, violet, etc.)
|
|
635
|
+
title_component = fh.Span(
|
|
636
|
+
label_text,
|
|
637
|
+
cls=f"text-sm font-medium {self._get_color_class(self.label_color)}",
|
|
638
|
+
)
|
|
576
639
|
else:
|
|
577
|
-
#
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
640
|
+
# No color specified - use default styling
|
|
641
|
+
title_component = fh.Span(
|
|
642
|
+
label_text, cls="text-sm font-medium text-gray-700"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Add tooltip if description is available
|
|
646
|
+
description = getattr(self.field_info, "description", None)
|
|
647
|
+
if description:
|
|
648
|
+
title_component.attrs["uk-tooltip"] = description
|
|
649
|
+
title_component.attrs["title"] = description
|
|
582
650
|
|
|
583
651
|
# 2. Render the nested input fields that will be the accordion content
|
|
584
652
|
input_component = self.render_input()
|
|
@@ -588,13 +656,8 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
588
656
|
accordion_id = f"{self.field_name}_accordion"
|
|
589
657
|
|
|
590
658
|
# 4. Create the AccordionItem using the MonsterUI component
|
|
591
|
-
# - Pass label_content as the title.
|
|
592
|
-
# - Pass input_component as the content.
|
|
593
|
-
# - Set 'open=True' to be expanded by default.
|
|
594
|
-
# - Pass item_id via li_kwargs.
|
|
595
|
-
# - Add 'mb-4' class for bottom margin spacing.
|
|
596
659
|
accordion_item = mui.AccordionItem(
|
|
597
|
-
|
|
660
|
+
title_component, # Title component with proper color styling
|
|
598
661
|
input_component, # Content component (the Card with nested fields)
|
|
599
662
|
open=True, # Open by default
|
|
600
663
|
li_kwargs={"id": item_id}, # Pass the specific ID for the <li>
|
|
@@ -609,7 +672,7 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
609
672
|
id=accordion_id, # ID for the accordion container (ul)
|
|
610
673
|
multiple=True, # Allow multiple open (though only one exists)
|
|
611
674
|
collapsible=True, # Allow toggling
|
|
612
|
-
cls=f"{spacing('accordion_divider', self.spacing)} {spacing('accordion_content', self.spacing)}".strip(),
|
|
675
|
+
cls=f"{spacing('accordion_divider', self.spacing)} {spacing('accordion_content', self.spacing)} w-full".strip(),
|
|
613
676
|
)
|
|
614
677
|
|
|
615
678
|
return accordion_container
|
|
@@ -695,6 +758,8 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
695
758
|
prefix=nested_prefix,
|
|
696
759
|
disabled=self.disabled, # Propagate disabled state to nested fields
|
|
697
760
|
spacing=self.spacing, # Propagate spacing to nested fields
|
|
761
|
+
field_path=self.field_path + [nested_field_name], # Propagate path
|
|
762
|
+
form_name=self.explicit_form_name, # Propagate form name
|
|
698
763
|
)
|
|
699
764
|
|
|
700
765
|
nested_inputs.append(renderer.render())
|
|
@@ -729,6 +794,54 @@ class BaseModelFieldRenderer(BaseFieldRenderer):
|
|
|
729
794
|
class ListFieldRenderer(BaseFieldRenderer):
|
|
730
795
|
"""Renderer for list fields containing any type"""
|
|
731
796
|
|
|
797
|
+
def _container_id(self) -> str:
|
|
798
|
+
"""
|
|
799
|
+
Return a DOM-unique ID for the list's <ul> / <div> wrapper.
|
|
800
|
+
|
|
801
|
+
Format: <formname>_<hierarchy>_items_container
|
|
802
|
+
Example: main_form_compact_tags_items_container
|
|
803
|
+
"""
|
|
804
|
+
base = "_".join(self.field_path) # tags or main_address_tags
|
|
805
|
+
if self._form_name: # already resolved in property
|
|
806
|
+
return f"{self._form_name}_{base}_items_container"
|
|
807
|
+
return f"{base}_items_container" # fallback (shouldn't happen)
|
|
808
|
+
|
|
809
|
+
@property
|
|
810
|
+
def _form_name(self) -> str:
|
|
811
|
+
"""Get form name - prefer explicit form name if provided"""
|
|
812
|
+
if self.explicit_form_name:
|
|
813
|
+
return self.explicit_form_name
|
|
814
|
+
|
|
815
|
+
# Fallback to extracting from prefix (for backward compatibility)
|
|
816
|
+
# The prefix always starts with the form name followed by underscore
|
|
817
|
+
# e.g., "main_form_compact_" or "main_form_compact_main_address_tags_"
|
|
818
|
+
# We need to extract just "main_form_compact"
|
|
819
|
+
if self.prefix:
|
|
820
|
+
# For backward compatibility with existing non-nested lists
|
|
821
|
+
# Split by underscore and rebuild the form name by removing known field components
|
|
822
|
+
parts = self.prefix.rstrip("_").split("_")
|
|
823
|
+
|
|
824
|
+
# For a simple heuristic: form names typically have 2-3 parts (main_form_compact)
|
|
825
|
+
# Field paths are at the end, so we find where the form name ends
|
|
826
|
+
# This is imperfect but works for most cases
|
|
827
|
+
if len(parts) >= 3 and parts[1] == "form":
|
|
828
|
+
# Standard pattern: main_form_compact
|
|
829
|
+
form_name = "_".join(parts[:3])
|
|
830
|
+
elif len(parts) >= 2:
|
|
831
|
+
# Fallback: take first 2 parts
|
|
832
|
+
form_name = "_".join(parts[:2])
|
|
833
|
+
else:
|
|
834
|
+
# Single part
|
|
835
|
+
form_name = parts[0] if parts else ""
|
|
836
|
+
|
|
837
|
+
return form_name
|
|
838
|
+
return ""
|
|
839
|
+
|
|
840
|
+
@property
|
|
841
|
+
def _list_path(self) -> str:
|
|
842
|
+
"""Get the hierarchical path for this list field"""
|
|
843
|
+
return "/".join(self.field_path)
|
|
844
|
+
|
|
732
845
|
def render(self) -> FT:
|
|
733
846
|
"""
|
|
734
847
|
Render the complete field (label + input) with spacing, adding a refresh icon for list fields.
|
|
@@ -738,13 +851,42 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
738
851
|
A FastHTML component containing the complete field with refresh icon
|
|
739
852
|
"""
|
|
740
853
|
# Extract form name from prefix (removing trailing underscore if present)
|
|
741
|
-
form_name = self.prefix.rstrip("_") if self.prefix else None
|
|
854
|
+
# form_name = self.prefix.rstrip("_") if self.prefix else None
|
|
855
|
+
form_name = self._form_name or None
|
|
856
|
+
|
|
857
|
+
# Create the label text with proper color styling
|
|
858
|
+
label_text = self.original_field_name.replace("_", " ").title()
|
|
859
|
+
|
|
860
|
+
# Create the styled label span
|
|
861
|
+
if self.label_color:
|
|
862
|
+
if self._is_inline_color(self.label_color):
|
|
863
|
+
# Color value - apply as inline style
|
|
864
|
+
label_span = fh.Span(
|
|
865
|
+
label_text,
|
|
866
|
+
style=f"color: {self.label_color};",
|
|
867
|
+
cls=f"block text-sm font-medium {spacing('label_gap', self.spacing)}",
|
|
868
|
+
)
|
|
869
|
+
else:
|
|
870
|
+
# CSS class - apply as Tailwind class (includes emerald, amber, rose, teal, indigo, lime, violet, etc.)
|
|
871
|
+
label_span = fh.Span(
|
|
872
|
+
label_text,
|
|
873
|
+
cls=f"block text-sm font-medium {self._get_color_class(self.label_color)} {spacing('label_gap', self.spacing)}",
|
|
874
|
+
)
|
|
875
|
+
else:
|
|
876
|
+
# No color specified - use default styling
|
|
877
|
+
label_span = fh.Span(
|
|
878
|
+
label_text,
|
|
879
|
+
cls=f"block text-sm font-medium text-gray-700 {spacing('label_gap', self.spacing)}",
|
|
880
|
+
)
|
|
742
881
|
|
|
743
|
-
#
|
|
744
|
-
|
|
882
|
+
# Add tooltip if description is available
|
|
883
|
+
description = getattr(self.field_info, "description", None)
|
|
884
|
+
if description:
|
|
885
|
+
label_span.attrs["uk-tooltip"] = description
|
|
886
|
+
label_span.attrs["title"] = description
|
|
745
887
|
|
|
746
888
|
# Construct the container ID that will be generated by render_input()
|
|
747
|
-
container_id =
|
|
889
|
+
container_id = self._container_id()
|
|
748
890
|
|
|
749
891
|
# Only add refresh icon if we have a form name
|
|
750
892
|
if form_name:
|
|
@@ -761,21 +903,23 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
761
903
|
hx_post=f"/form/{form_name}/refresh",
|
|
762
904
|
hx_target=f"#{form_name}-inputs-wrapper",
|
|
763
905
|
hx_swap="innerHTML",
|
|
764
|
-
hx_include=
|
|
906
|
+
hx_include="closest form", # ← key change
|
|
765
907
|
uk_tooltip="Refresh form display to update list summaries",
|
|
908
|
+
# Prevent 'toggleListItems' on the parent from firing
|
|
909
|
+
onclick="event.stopPropagation();",
|
|
766
910
|
)
|
|
767
911
|
|
|
768
912
|
# Combine label and icon
|
|
769
913
|
label_with_icon = fh.Div(
|
|
770
|
-
|
|
914
|
+
label_span, # Use the properly styled label span
|
|
771
915
|
refresh_icon_trigger,
|
|
772
916
|
cls="flex items-center cursor-pointer", # Added cursor-pointer
|
|
773
917
|
onclick=f"toggleListItems('{container_id}'); return false;", # Add click handler
|
|
774
918
|
)
|
|
775
919
|
else:
|
|
776
|
-
# If no form name, just use the
|
|
920
|
+
# If no form name, just use the styled label but still make it clickable
|
|
777
921
|
label_with_icon = fh.Div(
|
|
778
|
-
|
|
922
|
+
label_span, # Use the properly styled label span
|
|
779
923
|
cls="flex items-center cursor-pointer", # Added cursor-pointer
|
|
780
924
|
onclick=f"toggleListItems('{container_id}'); return false;", # Add click handler
|
|
781
925
|
uk_tooltip="Click to toggle all items open/closed",
|
|
@@ -785,7 +929,7 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
785
929
|
return fh.Div(
|
|
786
930
|
label_with_icon,
|
|
787
931
|
self.render_input(),
|
|
788
|
-
cls=spacing(
|
|
932
|
+
cls=f"{spacing('outer_margin', self.spacing)} w-full",
|
|
789
933
|
)
|
|
790
934
|
|
|
791
935
|
def render_input(self) -> FT:
|
|
@@ -839,8 +983,8 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
839
983
|
)
|
|
840
984
|
)
|
|
841
985
|
|
|
842
|
-
# Container for list items
|
|
843
|
-
container_id =
|
|
986
|
+
# Container for list items using hierarchical field path
|
|
987
|
+
container_id = self._container_id()
|
|
844
988
|
|
|
845
989
|
# Use mui.Accordion component
|
|
846
990
|
accordion = mui.Accordion(
|
|
@@ -854,13 +998,10 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
854
998
|
# Empty state message if no items
|
|
855
999
|
empty_state = ""
|
|
856
1000
|
if not items:
|
|
857
|
-
#
|
|
858
|
-
form_name = self.prefix.rstrip("_") if self.prefix else None
|
|
859
|
-
|
|
860
|
-
# Check if it's a simple type or BaseModel
|
|
1001
|
+
# Use hierarchical path for URL
|
|
861
1002
|
add_url = (
|
|
862
|
-
f"/form/{
|
|
863
|
-
if
|
|
1003
|
+
f"/form/{self._form_name}/list/add/{self._list_path}"
|
|
1004
|
+
if self._form_name
|
|
864
1005
|
else f"/list/add/{self.field_name}"
|
|
865
1006
|
)
|
|
866
1007
|
|
|
@@ -913,9 +1054,6 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
913
1054
|
item_id = f"{self.field_name}_{idx}"
|
|
914
1055
|
item_card_id = f"{item_id}_card"
|
|
915
1056
|
|
|
916
|
-
# Extract form name from prefix if available
|
|
917
|
-
form_name = self.prefix.rstrip("_") if self.prefix else None
|
|
918
|
-
|
|
919
1057
|
# Check if it's a simple type or BaseModel
|
|
920
1058
|
is_model = hasattr(item_type, "model_fields")
|
|
921
1059
|
|
|
@@ -1060,6 +1198,11 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
1060
1198
|
prefix=name_prefix,
|
|
1061
1199
|
disabled=self.disabled, # Propagate disabled state
|
|
1062
1200
|
spacing=self.spacing, # Propagate spacing
|
|
1201
|
+
field_path=self.field_path
|
|
1202
|
+
+ [
|
|
1203
|
+
str(idx),
|
|
1204
|
+
nested_field_name,
|
|
1205
|
+
], # Propagate path with index
|
|
1063
1206
|
)
|
|
1064
1207
|
|
|
1065
1208
|
# Add rendered field to valid fields
|
|
@@ -1095,21 +1238,23 @@ class ListFieldRenderer(BaseFieldRenderer):
|
|
|
1095
1238
|
prefix=self.prefix, # Correct: Provide the form prefix
|
|
1096
1239
|
disabled=self.disabled, # Propagate disabled state
|
|
1097
1240
|
spacing=self.spacing, # Propagate spacing
|
|
1241
|
+
field_path=self.field_path
|
|
1242
|
+
+ [str(idx)], # Propagate path with index
|
|
1098
1243
|
)
|
|
1099
1244
|
input_element = simple_renderer.render_input()
|
|
1100
1245
|
item_content_elements.append(fh.Div(input_element))
|
|
1101
1246
|
|
|
1102
1247
|
# --- Create action buttons with form-specific URLs ---
|
|
1103
|
-
# Generate HTMX endpoints
|
|
1248
|
+
# Generate HTMX endpoints using hierarchical paths
|
|
1104
1249
|
delete_url = (
|
|
1105
|
-
f"/form/{
|
|
1106
|
-
if
|
|
1250
|
+
f"/form/{self._form_name}/list/delete/{self._list_path}"
|
|
1251
|
+
if self._form_name
|
|
1107
1252
|
else f"/list/delete/{self.field_name}"
|
|
1108
1253
|
)
|
|
1109
1254
|
|
|
1110
1255
|
add_url = (
|
|
1111
|
-
f"/form/{
|
|
1112
|
-
if
|
|
1256
|
+
f"/form/{self._form_name}/list/add/{self._list_path}"
|
|
1257
|
+
if self._form_name
|
|
1113
1258
|
else f"/list/add/{self.field_name}"
|
|
1114
1259
|
)
|
|
1115
1260
|
|