bootstrap-python 0.1.0__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.
- bootstrap_python-0.1.0.dist-info/METADATA +226 -0
- bootstrap_python-0.1.0.dist-info/RECORD +15 -0
- bootstrap_python-0.1.0.dist-info/WHEEL +5 -0
- bootstrap_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstrap_python-0.1.0.dist-info/top_level.txt +1 -0
- bootstrap_ui/__init__.py +162 -0
- bootstrap_ui/constants.py +85 -0
- bootstrap_ui/content.py +562 -0
- bootstrap_ui/data.py +411 -0
- bootstrap_ui/forms.py +489 -0
- bootstrap_ui/layout.py +260 -0
- bootstrap_ui/nav.py +356 -0
- bootstrap_ui/overlays.py +199 -0
- bootstrap_ui/templates/page.jinja2 +37 -0
- bootstrap_ui/types.py +236 -0
bootstrap_ui/forms.py
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
"""表单组件:Form / FormGroup / Label / Input / Textarea / Select / Checkbox / RadioGroup /
|
|
2
|
+
InputGroup / Fieldset / Hidden。
|
|
3
|
+
|
|
4
|
+
用法上分成两层:控件只渲染自身(``<input>``、``<select>`` …),
|
|
5
|
+
由 :class:`FormGroup` 负责「标签 + 控件 + 帮助文本」的组合,
|
|
6
|
+
与 Bootstrap 文档的结构一一对应。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from typing import Any, ClassVar, Literal
|
|
13
|
+
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
|
|
16
|
+
from .constants import Color, Size, value as enum_value
|
|
17
|
+
from .content import Button
|
|
18
|
+
from .types import Node, NodeLike, escape, render_attrs, render_children
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Form",
|
|
22
|
+
"FormGroup",
|
|
23
|
+
"Label",
|
|
24
|
+
"Input",
|
|
25
|
+
"Textarea",
|
|
26
|
+
"Select",
|
|
27
|
+
"Checkbox",
|
|
28
|
+
"RadioGroup",
|
|
29
|
+
"InputGroup",
|
|
30
|
+
"Fieldset",
|
|
31
|
+
"Hidden",
|
|
32
|
+
"field",
|
|
33
|
+
"field_id",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def field_id(name: str) -> str:
|
|
38
|
+
"""把字段名转成合法的 HTML id,例如 ``user[name]`` → ``field-user-name``。"""
|
|
39
|
+
return "field-" + re.sub(r"[^\w-]+", "-", name).strip("-")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _normalize_options(options: list[Any] | None):
|
|
43
|
+
"""把各种选项写法统一成 ``(label, value, disabled)``,支持 optgroup。
|
|
44
|
+
|
|
45
|
+
- ``"男"`` → label=value="男"
|
|
46
|
+
- ``("男", 1)`` / ``["男", 1]`` → label="男", value=1
|
|
47
|
+
- ``{"label": "男", "value": 1, "disabled": True}``
|
|
48
|
+
- ``{"label": "分组", "options": [...]}`` → optgroup
|
|
49
|
+
"""
|
|
50
|
+
for option in options or []:
|
|
51
|
+
if isinstance(option, dict):
|
|
52
|
+
if "options" in option:
|
|
53
|
+
yield ("group", option.get("label", ""), list(option.get("options") or []))
|
|
54
|
+
continue
|
|
55
|
+
label = option.get("label", option.get("value"))
|
|
56
|
+
value = option.get("value", label)
|
|
57
|
+
yield (label, value, bool(option.get("disabled", False)))
|
|
58
|
+
elif isinstance(option, (list, tuple)):
|
|
59
|
+
values = list(option)
|
|
60
|
+
label = values[0] if values else ""
|
|
61
|
+
value = values[1] if len(values) > 1 else label
|
|
62
|
+
yield (label, value, bool(values[2]) if len(values) > 2 else False)
|
|
63
|
+
else:
|
|
64
|
+
yield (option, option, False)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class Form(Node):
|
|
68
|
+
"""表单。"""
|
|
69
|
+
|
|
70
|
+
TAG: ClassVar[str | None] = "form"
|
|
71
|
+
|
|
72
|
+
action: str = ""
|
|
73
|
+
method: Literal["get", "post"] = "post"
|
|
74
|
+
enctype: str | None = None
|
|
75
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
76
|
+
inline: bool = False
|
|
77
|
+
novalidate: bool = False
|
|
78
|
+
submit_label: str | None = "提交"
|
|
79
|
+
submit_color: Color = Color.primary
|
|
80
|
+
reset_label: str | None = None
|
|
81
|
+
|
|
82
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
83
|
+
attrs = super().html_attrs()
|
|
84
|
+
attrs["action"] = self.action
|
|
85
|
+
attrs["method"] = self.method
|
|
86
|
+
if self.enctype:
|
|
87
|
+
attrs["enctype"] = self.enctype
|
|
88
|
+
if self.novalidate:
|
|
89
|
+
attrs["novalidate"] = True
|
|
90
|
+
return attrs
|
|
91
|
+
|
|
92
|
+
def css_classes(self) -> list[str]:
|
|
93
|
+
return ["row", "row-cols-lg-auto", "g-3", "align-items-center"] if self.inline else []
|
|
94
|
+
|
|
95
|
+
def render_content(self) -> str:
|
|
96
|
+
parts = [render_children(self.body)]
|
|
97
|
+
if self.submit_label or self.reset_label:
|
|
98
|
+
buttons: list[str] = []
|
|
99
|
+
if self.submit_label:
|
|
100
|
+
buttons.append(
|
|
101
|
+
Button(text=self.submit_label, type="submit", color=self.submit_color).render()
|
|
102
|
+
)
|
|
103
|
+
if self.reset_label:
|
|
104
|
+
buttons.append(
|
|
105
|
+
Button(text=self.reset_label, type="reset", color=Color.secondary, outline=True).render()
|
|
106
|
+
)
|
|
107
|
+
parts.append(f'<div class="d-flex gap-2 mt-4">{"".join(buttons)}</div>')
|
|
108
|
+
return "".join(parts)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Label(Node):
|
|
112
|
+
"""表单标签。"""
|
|
113
|
+
|
|
114
|
+
TAG: ClassVar[str | None] = "label"
|
|
115
|
+
|
|
116
|
+
text: str = ""
|
|
117
|
+
for_: str | None = Field(default=None, alias="for")
|
|
118
|
+
required: bool = False
|
|
119
|
+
|
|
120
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
121
|
+
attrs = super().html_attrs()
|
|
122
|
+
if self.for_:
|
|
123
|
+
attrs["for"] = self.for_
|
|
124
|
+
return attrs
|
|
125
|
+
|
|
126
|
+
def css_classes(self) -> list[str]:
|
|
127
|
+
return ["form-label"]
|
|
128
|
+
|
|
129
|
+
def render_content(self) -> str:
|
|
130
|
+
star = '<span class="text-danger ms-1">*</span>' if self.required else ""
|
|
131
|
+
return escape(self.text) + star
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class _Control(Node):
|
|
135
|
+
"""表单控件的公共行为:id 推导、校验反馈。"""
|
|
136
|
+
|
|
137
|
+
TAG: ClassVar[str | None] = None
|
|
138
|
+
|
|
139
|
+
name: str = ""
|
|
140
|
+
invalid: bool = False
|
|
141
|
+
valid: bool = False
|
|
142
|
+
invalid_feedback: str | None = None
|
|
143
|
+
valid_feedback: str | None = None
|
|
144
|
+
|
|
145
|
+
def control_id(self) -> str:
|
|
146
|
+
return self.id or field_id(self.name)
|
|
147
|
+
|
|
148
|
+
def validation_classes(self) -> list[str]:
|
|
149
|
+
classes: list[str] = []
|
|
150
|
+
if self.invalid:
|
|
151
|
+
classes.append("is-invalid")
|
|
152
|
+
elif self.valid:
|
|
153
|
+
classes.append("is-valid")
|
|
154
|
+
return classes
|
|
155
|
+
|
|
156
|
+
def feedback(self) -> str:
|
|
157
|
+
parts: list[str] = []
|
|
158
|
+
if self.invalid_feedback:
|
|
159
|
+
parts.append(f'<div class="invalid-feedback">{escape(self.invalid_feedback)}</div>')
|
|
160
|
+
if self.valid_feedback:
|
|
161
|
+
parts.append(f'<div class="valid-feedback">{escape(self.valid_feedback)}</div>')
|
|
162
|
+
return "".join(parts)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class Input(_Control):
|
|
166
|
+
"""单行输入框。"""
|
|
167
|
+
|
|
168
|
+
TAG: ClassVar[str | None] = "input"
|
|
169
|
+
|
|
170
|
+
type: str = "text"
|
|
171
|
+
value: Any = None
|
|
172
|
+
placeholder: str | None = None
|
|
173
|
+
size: Size | None = None
|
|
174
|
+
required: bool = False
|
|
175
|
+
disabled: bool = False
|
|
176
|
+
readonly: bool = False
|
|
177
|
+
autocomplete: str | None = None
|
|
178
|
+
|
|
179
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
180
|
+
attrs = super().html_attrs()
|
|
181
|
+
attrs["type"] = self.type
|
|
182
|
+
attrs["name"] = self.name
|
|
183
|
+
attrs.setdefault("id", self.control_id())
|
|
184
|
+
if self.value is not None:
|
|
185
|
+
attrs["value"] = self.value
|
|
186
|
+
if self.placeholder:
|
|
187
|
+
attrs["placeholder"] = self.placeholder
|
|
188
|
+
if self.required:
|
|
189
|
+
attrs["required"] = True
|
|
190
|
+
if self.disabled:
|
|
191
|
+
attrs["disabled"] = True
|
|
192
|
+
if self.readonly:
|
|
193
|
+
attrs["readonly"] = True
|
|
194
|
+
if self.autocomplete:
|
|
195
|
+
attrs["autocomplete"] = self.autocomplete
|
|
196
|
+
return attrs
|
|
197
|
+
|
|
198
|
+
def css_classes(self) -> list[str]:
|
|
199
|
+
classes = ["form-control"]
|
|
200
|
+
if self.size:
|
|
201
|
+
classes.append(f"form-control-{enum_value(self.size)}")
|
|
202
|
+
classes.extend(self.validation_classes())
|
|
203
|
+
return classes
|
|
204
|
+
|
|
205
|
+
def render(self) -> str:
|
|
206
|
+
return super().render() + self.feedback()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Textarea(_Control):
|
|
210
|
+
"""多行文本输入框。"""
|
|
211
|
+
|
|
212
|
+
TAG: ClassVar[str | None] = "textarea"
|
|
213
|
+
|
|
214
|
+
value: str | None = None
|
|
215
|
+
rows: int = 3
|
|
216
|
+
placeholder: str | None = None
|
|
217
|
+
required: bool = False
|
|
218
|
+
disabled: bool = False
|
|
219
|
+
readonly: bool = False
|
|
220
|
+
|
|
221
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
222
|
+
attrs = super().html_attrs()
|
|
223
|
+
attrs["name"] = self.name
|
|
224
|
+
attrs.setdefault("id", self.control_id())
|
|
225
|
+
attrs["rows"] = self.rows
|
|
226
|
+
if self.placeholder:
|
|
227
|
+
attrs["placeholder"] = self.placeholder
|
|
228
|
+
if self.required:
|
|
229
|
+
attrs["required"] = True
|
|
230
|
+
if self.disabled:
|
|
231
|
+
attrs["disabled"] = True
|
|
232
|
+
if self.readonly:
|
|
233
|
+
attrs["readonly"] = True
|
|
234
|
+
return attrs
|
|
235
|
+
|
|
236
|
+
def css_classes(self) -> list[str]:
|
|
237
|
+
return ["form-control", *self.validation_classes()]
|
|
238
|
+
|
|
239
|
+
def render_content(self) -> str:
|
|
240
|
+
return escape(self.value or "")
|
|
241
|
+
|
|
242
|
+
def render(self) -> str:
|
|
243
|
+
return super().render() + self.feedback()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
class Select(_Control):
|
|
247
|
+
"""下拉选择框。"""
|
|
248
|
+
|
|
249
|
+
TAG: ClassVar[str | None] = "select"
|
|
250
|
+
|
|
251
|
+
options: list[Any] = Field(default_factory=list)
|
|
252
|
+
value: Any = None
|
|
253
|
+
multiple: bool = False
|
|
254
|
+
size: Size | None = None
|
|
255
|
+
placeholder: str | None = None
|
|
256
|
+
required: bool = False
|
|
257
|
+
disabled: bool = False
|
|
258
|
+
|
|
259
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
260
|
+
attrs = super().html_attrs()
|
|
261
|
+
attrs["name"] = self.name
|
|
262
|
+
attrs.setdefault("id", self.control_id())
|
|
263
|
+
if self.multiple:
|
|
264
|
+
attrs["multiple"] = True
|
|
265
|
+
if self.required:
|
|
266
|
+
attrs["required"] = True
|
|
267
|
+
if self.disabled:
|
|
268
|
+
attrs["disabled"] = True
|
|
269
|
+
return attrs
|
|
270
|
+
|
|
271
|
+
def css_classes(self) -> list[str]:
|
|
272
|
+
classes = ["form-select"]
|
|
273
|
+
if self.size:
|
|
274
|
+
classes.append(f"form-select-{enum_value(self.size)}")
|
|
275
|
+
classes.extend(self.validation_classes())
|
|
276
|
+
return classes
|
|
277
|
+
|
|
278
|
+
def _options_html(self) -> str:
|
|
279
|
+
selected = self.value if isinstance(self.value, (list, tuple, set)) else [self.value]
|
|
280
|
+
selected_values = {str(item) for item in selected if item is not None}
|
|
281
|
+
parts: list[str] = []
|
|
282
|
+
|
|
283
|
+
if self.placeholder and not self.multiple and not selected_values:
|
|
284
|
+
parts.append(f'<option value="" disabled selected>{escape(self.placeholder)}</option>')
|
|
285
|
+
|
|
286
|
+
for label, value, disabled in _normalize_options(self.options):
|
|
287
|
+
if label == "group":
|
|
288
|
+
group_options = "".join(
|
|
289
|
+
self._option_html(*item, selected_values) for item in _normalize_options(value)
|
|
290
|
+
)
|
|
291
|
+
parts.append(f'<optgroup label="{escape(value)}">{group_options}</optgroup>')
|
|
292
|
+
else:
|
|
293
|
+
parts.append(self._option_html(label, value, disabled, selected_values))
|
|
294
|
+
return "".join(parts)
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _option_html(label: Any, value: Any, disabled: bool, selected_values: set[str]) -> str:
|
|
298
|
+
attrs: list[str] = [f'value="{escape(value)}"']
|
|
299
|
+
if disabled:
|
|
300
|
+
attrs.append("disabled")
|
|
301
|
+
if str(value) in selected_values:
|
|
302
|
+
attrs.append("selected")
|
|
303
|
+
return f'<option {" ".join(attrs)}>{escape(label)}</option>'
|
|
304
|
+
|
|
305
|
+
def render_content(self) -> str:
|
|
306
|
+
return self._options_html()
|
|
307
|
+
|
|
308
|
+
def render(self) -> str:
|
|
309
|
+
return super().render() + self.feedback()
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class Checkbox(Node):
|
|
313
|
+
"""复选框 / 开关。"""
|
|
314
|
+
|
|
315
|
+
TAG: ClassVar[str | None] = None
|
|
316
|
+
|
|
317
|
+
name: str = ""
|
|
318
|
+
label: str | None = None
|
|
319
|
+
value: str = "on"
|
|
320
|
+
checked: bool = False
|
|
321
|
+
switch: bool = False
|
|
322
|
+
inline: bool = False
|
|
323
|
+
disabled: bool = False
|
|
324
|
+
required: bool = False
|
|
325
|
+
|
|
326
|
+
def render(self) -> str:
|
|
327
|
+
checkbox_id = self.id or field_id(self.name)
|
|
328
|
+
input_classes = ["form-check-input"]
|
|
329
|
+
wrapper_classes = ["form-check"]
|
|
330
|
+
if self.switch:
|
|
331
|
+
wrapper_classes.append("form-switch")
|
|
332
|
+
if self.inline:
|
|
333
|
+
wrapper_classes.append("form-check-inline")
|
|
334
|
+
|
|
335
|
+
attrs: dict[str, Any] = {
|
|
336
|
+
"class": " ".join(input_classes),
|
|
337
|
+
"type": "checkbox",
|
|
338
|
+
"id": checkbox_id,
|
|
339
|
+
"name": self.name,
|
|
340
|
+
"value": self.value,
|
|
341
|
+
}
|
|
342
|
+
if self.checked:
|
|
343
|
+
attrs["checked"] = True
|
|
344
|
+
if self.disabled:
|
|
345
|
+
attrs["disabled"] = True
|
|
346
|
+
if self.required:
|
|
347
|
+
attrs["required"] = True
|
|
348
|
+
|
|
349
|
+
label = (
|
|
350
|
+
f'<label class="form-check-label" for="{checkbox_id}">{escape(self.label)}</label>'
|
|
351
|
+
if self.label
|
|
352
|
+
else ""
|
|
353
|
+
)
|
|
354
|
+
return f'<div class="{" ".join(wrapper_classes)}"><input{render_attrs(attrs)}>{label}</div>'
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class RadioGroup(Node):
|
|
358
|
+
"""单选组。"""
|
|
359
|
+
|
|
360
|
+
TAG: ClassVar[str | None] = None
|
|
361
|
+
|
|
362
|
+
name: str = ""
|
|
363
|
+
options: list[Any] = Field(default_factory=list)
|
|
364
|
+
value: Any = None
|
|
365
|
+
inline: bool = True
|
|
366
|
+
disabled: bool = False
|
|
367
|
+
required: bool = False
|
|
368
|
+
|
|
369
|
+
def render(self) -> str:
|
|
370
|
+
parts: list[str] = []
|
|
371
|
+
for index, (label, value, disabled) in enumerate(_normalize_options(self.options)):
|
|
372
|
+
radio_id = f"{field_id(self.name)}-{index}"
|
|
373
|
+
attrs: dict[str, Any] = {
|
|
374
|
+
"class": "form-check-input",
|
|
375
|
+
"type": "radio",
|
|
376
|
+
"id": radio_id,
|
|
377
|
+
"name": self.name,
|
|
378
|
+
"value": value,
|
|
379
|
+
}
|
|
380
|
+
if self.value is not None and str(value) == str(self.value):
|
|
381
|
+
attrs["checked"] = True
|
|
382
|
+
if disabled or self.disabled:
|
|
383
|
+
attrs["disabled"] = True
|
|
384
|
+
if self.required:
|
|
385
|
+
attrs["required"] = True
|
|
386
|
+
wrapper = "form-check form-check-inline" if self.inline else "form-check"
|
|
387
|
+
parts.append(
|
|
388
|
+
f'<div class="{wrapper}"><input{render_attrs(attrs)}>'
|
|
389
|
+
f'<label class="form-check-label" for="{radio_id}">{escape(label)}</label></div>'
|
|
390
|
+
)
|
|
391
|
+
return "".join(parts)
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class InputGroup(Node):
|
|
395
|
+
"""输入框组合(前置 / 后置附加内容)。"""
|
|
396
|
+
|
|
397
|
+
control: NodeLike | None = None
|
|
398
|
+
prepend: NodeLike | None = None
|
|
399
|
+
append: NodeLike | None = None
|
|
400
|
+
size: Size | None = None
|
|
401
|
+
|
|
402
|
+
def css_classes(self) -> list[str]:
|
|
403
|
+
classes = ["input-group"]
|
|
404
|
+
if self.size:
|
|
405
|
+
classes.append(f"input-group-{enum_value(self.size)}")
|
|
406
|
+
return classes
|
|
407
|
+
|
|
408
|
+
def render_content(self) -> str:
|
|
409
|
+
parts: list[str] = []
|
|
410
|
+
if self.prepend is not None:
|
|
411
|
+
parts.append(f'<span class="input-group-text">{render_children(self.prepend)}</span>')
|
|
412
|
+
parts.append(render_children(self.control))
|
|
413
|
+
if self.append is not None:
|
|
414
|
+
parts.append(f'<span class="input-group-text">{render_children(self.append)}</span>')
|
|
415
|
+
return "".join(parts)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class Fieldset(Node):
|
|
419
|
+
"""字段分组。"""
|
|
420
|
+
|
|
421
|
+
TAG: ClassVar[str | None] = "fieldset"
|
|
422
|
+
|
|
423
|
+
legend: str | None = None
|
|
424
|
+
body: list[NodeLike] = Field(default_factory=list)
|
|
425
|
+
disabled: bool = False
|
|
426
|
+
|
|
427
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
428
|
+
attrs = super().html_attrs()
|
|
429
|
+
if self.disabled:
|
|
430
|
+
attrs["disabled"] = True
|
|
431
|
+
return attrs
|
|
432
|
+
|
|
433
|
+
def css_classes(self) -> list[str]:
|
|
434
|
+
return ["mb-3"]
|
|
435
|
+
|
|
436
|
+
def render_content(self) -> str:
|
|
437
|
+
legend = f"<legend>{escape(self.legend)}</legend>" if self.legend else ""
|
|
438
|
+
return legend + render_children(self.body)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
class Hidden(Node):
|
|
442
|
+
"""隐藏字段。"""
|
|
443
|
+
|
|
444
|
+
TAG: ClassVar[str | None] = "input"
|
|
445
|
+
|
|
446
|
+
name: str = ""
|
|
447
|
+
value: Any = None
|
|
448
|
+
|
|
449
|
+
def html_attrs(self) -> dict[str, Any]:
|
|
450
|
+
attrs = super().html_attrs()
|
|
451
|
+
attrs["type"] = "hidden"
|
|
452
|
+
attrs["name"] = self.name
|
|
453
|
+
if self.value is not None:
|
|
454
|
+
attrs["value"] = self.value
|
|
455
|
+
return attrs
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class FormGroup(Node):
|
|
459
|
+
"""「标签 + 控件 + 帮助文本」的组合。"""
|
|
460
|
+
|
|
461
|
+
label: str | None = None
|
|
462
|
+
control: NodeLike | None = None
|
|
463
|
+
help: str | None = None
|
|
464
|
+
required: bool = False
|
|
465
|
+
|
|
466
|
+
def css_classes(self) -> list[str]:
|
|
467
|
+
return ["mb-3"]
|
|
468
|
+
|
|
469
|
+
def _label_for(self) -> str | None:
|
|
470
|
+
control = self.control
|
|
471
|
+
if isinstance(control, _Control):
|
|
472
|
+
return control.control_id()
|
|
473
|
+
if isinstance(control, Checkbox):
|
|
474
|
+
return control.id or field_id(control.name)
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
def render_content(self) -> str:
|
|
478
|
+
parts: list[str] = []
|
|
479
|
+
if self.label:
|
|
480
|
+
parts.append(Label(text=self.label, for_=self._label_for(), required=self.required).render())
|
|
481
|
+
parts.append(render_children(self.control))
|
|
482
|
+
if self.help:
|
|
483
|
+
parts.append(f'<div class="form-text">{escape(self.help)}</div>')
|
|
484
|
+
return "".join(parts)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def field(label: str, control: Node | str, help: str | None = None, required: bool = False) -> FormGroup:
|
|
488
|
+
"""``FormGroup(label=..., control=...)`` 的简写。"""
|
|
489
|
+
return FormGroup(label=label, control=control, help=help, required=required)
|