Coverage for src / lexigram / ui / molecules / form_field.py: 15%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""FieldSchema molecule component - wrapper for form inputs with label and error."""
3from __future__ import annotations
5from typing import Any
7from lexigram.ui.core.base import Component, el
10class FormField(Component):
11 """Form field wrapper with label, input, error message, and help text."""
13 """Form field wrapper with label, input, error message, and help text."""
15 def __init__(
16 self,
17 input_component: Component,
18 label: str | None = None,
19 error: str | None = None,
20 help_text: str | None = None,
21 hint: str | None = None,
22 required: bool = False,
23 hidden: bool = False,
24 visible_condition: str | None = None,
25 **props,
26 ) -> None:
27 super().__init__(
28 input_component=input_component,
29 label=label,
30 error=error,
31 help_text=help_text,
32 hint=hint,
33 required=required,
34 hidden=hidden,
35 visible_condition=visible_condition,
36 **props,
37 )
38 self.input_component = input_component
39 self.label = label
40 self.error = error
41 self.help_text = help_text
42 self.hint = hint
43 self.required = required
44 self.hidden = hidden
45 self.visible_condition = visible_condition
47 def render(self) -> Any:
48 # Build container attributes
49 container_attrs: dict[str, Any] = {"class": "mb-6"}
50 if self.error:
51 error_id = f"{getattr(self.input_component, 'id', None) or getattr(self.input_component, 'name', None) or 'field'}-error"
52 container_attrs["aria_describedby"] = error_id
53 container_attrs["aria_invalid"] = "true"
55 # Handle hidden state
56 if self.hidden:
57 container_attrs["style"] = "display: none"
59 # Handle conditional visibility (Alpine.js)
60 if self.visible_condition:
61 container_attrs["x-show"] = self.visible_condition
62 container_attrs["x-cloak"] = True
64 elements = []
66 # Header (Label + Hint)
67 if self.label or self.hint:
68 header_parts = []
70 if self.label:
71 label_text = self.label
73 # Dynamic requirement asterisk
74 required_if = self.props.get("required_if")
75 if required_if:
76 header_parts.append(
77 el(
78 "label",
79 label_text,
80 el(
81 "span",
82 "*",
83 class_="text-destructive ml-1",
84 **{"x-show": required_if},
85 ),
86 for_=getattr(self.input_component, "id", None)
87 or getattr(self.input_component, "name", None),
88 class_="block text-sm font-medium text-foreground",
89 ),
90 )
91 elif self.required:
92 header_parts.append(
93 el(
94 "label",
95 label_text
96 + el("span", "*", class_="text-destructive ml-1"),
97 for_=getattr(self.input_component, "id", None)
98 or getattr(self.input_component, "name", None),
99 class_="block text-sm font-medium text-foreground",
100 ),
101 )
102 else:
103 header_parts.append(
104 el(
105 "label",
106 label_text,
107 for_=getattr(self.input_component, "id", None)
108 or getattr(self.input_component, "name", None),
109 class_="block text-sm font-medium text-foreground",
110 ),
111 )
113 if self.hint:
114 header_parts.append(
115 el(
116 "span",
117 self.hint,
118 class_="text-xs text-muted-foreground italic",
119 title=self.hint,
120 ),
121 )
123 elements.append(
124 el(
125 "div",
126 *header_parts,
127 class_="flex items-center justify-between mb-2",
128 ),
129 )
131 # Input
132 try:
133 rendered_input = self.input_component.render()
134 elements.append(rendered_input)
135 except (AttributeError, ValueError, TypeError):
136 # Fail-safe: avoid bubbling render errors to outer HTML rendering
137 from lexigram.logging import get_logger
139 logger = get_logger(__name__)
140 logger.exception(
141 "Error rendering input component %s",
142 getattr(self.input_component, "__class__", None),
143 )
144 elements.append(
145 el(
146 "div",
147 "Error rendering field",
148 class_="mb-2 p-2 rounded bg-destructive/10 text-destructive",
149 ),
150 )
152 # Error message
153 if self.error:
154 error_id = f"{getattr(self.input_component, 'id', None) or getattr(self.input_component, 'name', None) or 'field'}-error"
155 elements.append(
156 el(
157 "p",
158 self.error,
159 id=error_id,
160 class_="mt-2 text-sm text-destructive font-medium",
161 ),
162 )
164 # Help text
165 if self.help_text and not self.error:
166 elements.append(
167 el(
168 "p",
169 self.help_text,
170 class_="mt-2 text-sm text-muted-foreground",
171 ),
172 )
174 return el("div", *elements, **container_attrs)
177FieldSchema = FormField