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

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING, Any 

4 

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) 

15 

16if TYPE_CHECKING: 

17 from lexigram.ui.core.base import Component 

18 

19 

20class TextField(Field): 

21 """Single-line text input field.""" 

22 

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 

28 

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 ) 

79 

80 

81class IntegerField(Field): 

82 """Numeric integer input field.""" 

83 

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 

96 

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 ) 

111 

112 

113class TextAreaField(Field): 

114 """Multi-line textarea input field.""" 

115 

116 def __init__(self, label: str | None = None, rows: int = 4, **kwargs: Any) -> None: 

117 super().__init__(label=label, **kwargs) 

118 self.rows = rows 

119 

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 ) 

132 

133 

134class SelectField(Field): 

135 """Dropdown select field with single or multiple selection.""" 

136 

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 

154 

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 ) 

167 

168 

169class BooleanField(Field): 

170 """Boolean checkbox or toggle switch field.""" 

171 

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 

181 

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 ) 

202 

203 

204class DateField(Field): 

205 """Date picker input field.""" 

206 

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 )