Coverage for src / lexigram / admin / ui / organisms / form_registry.py: 51%
59 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""Registry for form field renderers."""
3from __future__ import annotations
5from typing import TYPE_CHECKING, Any, Protocol
7from lexigram.ui import DateInput, NumberInput, RichSelect, Switch, TextArea, TextInput
9if TYPE_CHECKING:
10 from lexigram.admin.forms import AbstractField, FieldType
13class FormFieldRendererProtocol(Protocol):
14 """Protocol for form field renderers."""
16 def can_render(self, field_type: FieldType) -> bool: ...
17 def render(self, field: AbstractField, current_value: Any) -> Any: ...
20class TextAreaRenderer:
21 def can_render(self, field_type: FieldType) -> bool:
22 from lexigram.admin.forms import FieldType
24 return field_type == FieldType.TEXTAREA
26 def render(self, field: AbstractField, current_value: Any) -> Any:
27 return TextArea(
28 name=field.name,
29 label=field.label,
30 value=str(current_value) if current_value else "",
31 placeholder=field.placeholder,
32 error=getattr(field, "errors", [None])[0]
33 if getattr(field, "errors", [])
34 else None,
35 rows=4,
36 disabled=not getattr(field, "editable", True),
37 )
40class SelectRenderer:
41 def can_render(self, field_type: FieldType) -> bool:
42 from lexigram.admin.forms import FieldType
44 return field_type == FieldType.SELECT
46 def render(self, field: AbstractField, current_value: Any) -> Any:
47 options_with_selection = [
48 {**opt, "selected": str(current_value) == str(opt["value"])}
49 for opt in field.options # type: ignore[attr-defined]
50 ]
51 return RichSelect(
52 label=field.label, # type: ignore[arg-type]
53 name=field.name,
54 options=options_with_selection,
55 value=current_value,
56 error=getattr(field, "errors", [None])[0]
57 if getattr(field, "errors", [])
58 else None,
59 disabled=not getattr(field, "editable", True),
60 )
63class CheckboxRenderer:
64 def can_render(self, field_type: FieldType) -> bool:
65 from lexigram.admin.forms import FieldType
67 return field_type == FieldType.CHECKBOX
69 def render(self, field: AbstractField, current_value: Any) -> Any:
70 return Switch(
71 label=field.label, # type: ignore[arg-type]
72 name=field.name,
73 value=bool(current_value) if current_value else False,
74 description=field.help_text,
75 error=getattr(field, "errors", [None])[0]
76 if getattr(field, "errors", [])
77 else None,
78 disabled=not getattr(field, "editable", True),
79 )
82class BelongsToFormRenderer:
83 def can_render(self, field_type: FieldType) -> bool:
84 from lexigram.admin.forms import FieldType
86 return field_type == FieldType.BELONGS_TO
88 def render(self, field: AbstractField, current_value: Any) -> Any:
89 from lexigram.ui.atoms.inputs.selection.relational import BelongsTo
91 return BelongsTo(
92 name=field.name,
93 resource=getattr(field, "metadata", {}).get("related_resource", ""),
94 value=current_value,
95 label=getattr(field, "label", field.name.title()),
96 ).render()
99class HasManyFormRenderer:
100 def can_render(self, field_type: FieldType) -> bool:
101 from lexigram.admin.forms import FieldType
103 return field_type == FieldType.HAS_MANY
105 def render(self, field: AbstractField, current_value: Any) -> Any:
106 from lexigram.ui.atoms.inputs.selection.choice import MultiSelect
108 choices = [
109 (o.get("value"), o.get("label")) for o in getattr(field, "choices", [])
110 ]
111 return MultiSelect(
112 name=field.name,
113 choices=choices,
114 value=current_value if isinstance(current_value, list) else [],
115 label=getattr(field, "label", field.name.title()),
116 ).render()
119class InputRenderer:
120 """Renderer for standard and specific input types."""
122 def __init__(self) -> None:
123 from lexigram.admin.forms import FieldType
125 self.type_map = {
126 FieldType.TEXT: (TextInput, "text"),
127 FieldType.EMAIL: (TextInput, "email"),
128 FieldType.PASSWORD: (TextInput, "password"),
129 FieldType.NUMBER: (NumberInput, None),
130 FieldType.DATE: (DateInput, None),
131 }
133 def can_render(self, field_type: FieldType) -> bool:
134 return field_type in self.type_map or True # Fallback
136 def render(self, field: AbstractField, current_value: Any) -> Any:
137 atom_class, input_type = self.type_map.get(field.type, (TextInput, "text")) # type: ignore[attr-defined]
139 props = {
140 "name": field.name,
141 "label": field.label,
142 "value": str(current_value) if current_value is not None else "",
143 "placeholder": field.placeholder,
144 "error": field.error, # type: ignore[attr-defined]
145 "disabled": not getattr(field, "editable", True),
146 }
147 if input_type:
148 props["type"] = input_type
150 return atom_class(**props)
153class FormFieldRegistry:
154 """Registry for form field renderers."""
156 def __init__(self) -> None:
157 self._renderers: list[FormFieldRendererProtocol] = [
158 TextAreaRenderer(),
159 SelectRenderer(),
160 CheckboxRenderer(),
161 BelongsToFormRenderer(),
162 HasManyFormRenderer(),
163 InputRenderer(), # Catch-all
164 ]
166 def get_renderer(self, field_type: FieldType) -> FormFieldRendererProtocol:
167 for renderer in self._renderers:
168 if renderer.can_render(field_type):
169 return renderer
170 return InputRenderer()
173_form_field_registry = FormFieldRegistry()