Coverage for src / lexigram / admin / forms / fields / _text.py: 36%
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
1from __future__ import annotations
3from typing import TYPE_CHECKING, Any
5from lexigram.admin.forms.fields._base import Field
6from lexigram.ui import (
7 Checkbox,
8 DateInput,
9 NumberInput,
10 Select,
11 Switch,
12 TextArea,
13 TextInput,
14)
16if TYPE_CHECKING:
17 from lexigram.ui.core.base import Component
20class TextField(Field):
21 """Single-line text input field."""
23 def __init__(
24 self, label: str | None = None, field_type: str = "text", **kwargs: Any
25 ) -> None:
26 super().__init__(label=label, **kwargs)
27 self.type = field_type
29 def render(self, **kwargs) -> Component:
30 htmx_attrs = {}
31 if self.realtime_validate:
32 htmx_attrs.update(
33 {
34 "hx_post": f"/admin//forms/validate/{self.name}",
35 "hx_trigger": "blur, change delay:500ms",
36 "hx_target": f"#{self.name}-error",
37 "hx_swap": "innerHTML",
38 },
39 )
40 if self.autocomplete_source:
41 htmx_attrs.update(
42 {
43 "hx_get": f"/admin//forms/autocomplete/{self.name}",
44 "hx_trigger": "input changed delay:300ms",
45 "hx_target": f"#{self.name}-suggestions",
46 "hx_swap": "innerHTML",
47 },
48 )
49 display_value = self.format_value(self.value)
50 if self.mask_pattern and isinstance(display_value, str):
51 display_value = self.apply_mask(display_value)
52 prefill_attrs = {}
53 if self.prefill_from:
54 prefill_attrs.update(
55 {
56 "data-prefill-from": self.prefill_from,
57 "data-field-name": self.name,
58 },
59 )
60 return TextInput(
61 name=self.name,
62 value=display_value,
63 label=self.label,
64 placeholder=self.placeholder,
65 type=self.type,
66 error=str(self.errors[0]) if self.errors else None,
67 required=self.required,
68 disabled=self.disabled,
69 prefix=self.prefix,
70 suffix=self.suffix,
71 aria_label=self.aria_label,
72 aria_describedby=self.aria_describedby,
73 aria_invalid="true" if self.errors else "false",
74 aria_errormessage=self.aria_errormessage if self.errors else None,
75 **htmx_attrs,
76 **prefill_attrs,
77 **kwargs,
78 )
81class IntegerField(Field):
82 """Numeric integer input field."""
84 def __init__(
85 self,
86 label: str | None = None,
87 min_value: int | None = None,
88 max_value: int | None = None,
89 step: int | None = None,
90 **kwargs: Any,
91 ) -> None:
92 super().__init__(label=label, **kwargs)
93 self.min = min_value
94 self.max = max_value
95 self.step = step
97 def render(self, **kwargs) -> Component:
98 return NumberInput(
99 name=self.name,
100 value=self.value,
101 label=self.label,
102 placeholder=self.placeholder,
103 error=str(self.errors[0]) if self.errors else None,
104 required=self.required,
105 disabled=self.disabled,
106 min=self.min,
107 max=self.max,
108 step=self.step,
109 **kwargs,
110 )
113class TextAreaField(Field):
114 """Multi-line textarea input field."""
116 def __init__(self, label: str | None = None, rows: int = 4, **kwargs: Any) -> None:
117 super().__init__(label=label, **kwargs)
118 self.rows = rows
120 def render(self, **kwargs) -> Component:
121 return TextArea(
122 name=self.name,
123 value=self.value,
124 label=self.label,
125 placeholder=self.placeholder,
126 error=str(self.errors[0]) if self.errors else None,
127 required=self.required,
128 disabled=self.disabled,
129 rows=self.rows,
130 **kwargs,
131 )
134class SelectField(Field):
135 """Dropdown select field with single or multiple selection."""
137 def __init__(
138 self,
139 label: str | None = None,
140 options: list[tuple[str, str]] | list[str] | None = None,
141 multiple: bool = False,
142 **kwargs: Any,
143 ) -> None:
144 if options is None:
145 options = []
146 super().__init__(label=label, **kwargs)
147 self.options: list[tuple[str, str]] = []
148 for opt in options:
149 if isinstance(opt, (tuple, list)) and len(opt) == 2:
150 self.options.append((str(opt[0]), str(opt[1])))
151 else:
152 self.options.append((str(opt), str(opt)))
153 self.multiple = multiple
155 def render(self, **kwargs) -> Component:
156 return Select(
157 name=self.name,
158 choices=self.options,
159 value=self.value,
160 label=self.label,
161 error=str(self.errors[0]) if self.errors else None,
162 required=self.required,
163 disabled=self.disabled,
164 multiple=self.multiple,
165 **kwargs,
166 )
169class BooleanField(Field):
170 """Boolean checkbox or toggle switch field."""
172 def __init__(
173 self,
174 label: str | None = None,
175 widget: str = "checkbox",
176 required: bool = False,
177 **kwargs: Any,
178 ) -> None:
179 super().__init__(label=label, required=required, **kwargs)
180 self.widget = widget
182 def render(self, **kwargs) -> Component:
183 if self.widget == "switch":
184 return Switch(
185 name=self.name,
186 value=bool(self.value),
187 label=self.label, # type: ignore[arg-type]
188 description=self.help_text,
189 error=str(self.errors[0]) if self.errors else None,
190 disabled=self.disabled,
191 **kwargs,
192 )
193 return Checkbox(
194 name=self.name,
195 checked=bool(self.value),
196 label=self.label,
197 error=str(self.errors[0]) if self.errors else None,
198 required=self.required,
199 disabled=self.disabled,
200 **kwargs,
201 )
204class DateField(Field):
205 """Date picker input field."""
207 def render(self, **kwargs) -> Component:
208 return DateInput(
209 name=self.name,
210 value=self.value,
211 label=self.label,
212 error=str(self.errors[0]) if self.errors else None,
213 required=self.required,
214 disabled=self.disabled,
215 **kwargs,
216 )