Coverage for src/lexigram/admin/ui/organisms/dynamic_form.py: 94%
31 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""Dynamic Form Component.
3Renders a FormSchema into HTML using htpy.
4"""
6from __future__ import annotations
8from typing import TYPE_CHECKING, Any
10import htpy as h
12from lexigram.admin.schema import BooleanField
13from lexigram.ui import Button, Component, Form
15if TYPE_CHECKING:
16 from lexigram.admin.forms import FormSchema
19class DynamicForm(Component):
20 """Renders a form based on a schema."""
22 def __init__(
23 self,
24 schema: FormSchema,
25 action: str,
26 method: str = "POST",
27 submit_text: str = "Submit",
28 hx_post: str | None = None,
29 hx_target: str | None = None,
30 hx_swap: str = "outerHTML",
31 ):
32 self.schema = schema
33 self.action = action
34 self.method = method
35 self.submit_text = submit_text
36 self.hx_post = hx_post or action if hx_post else None
37 self.hx_target = hx_target
38 self.hx_swap = hx_swap
40 def render(self) -> Any:
41 # We wrap the content in a list of htpy nodes
42 form_content: list[Any] = []
44 # Render each field via its schema render_form
45 for field in self.schema.fields:
46 # Skip fields hidden from forms
47 if not field.visible_in_form:
48 continue
50 form_content.append(field.render_form(field.get_default()))
52 if field.help_text and not isinstance(field, BooleanField):
53 form_content.append(
54 h.p(
55 class_="mt-1 text-xs text-muted-foreground mb-4 -mt-4",
56 )[field.help_text],
57 )
59 # Submit Button
60 form_content.append(
61 h.div(class_="flex justify-end pt-4")[
62 Button(self.submit_text, type="submit", color="primary")
63 ],
64 )
66 # Determine attributes for the generic Form wrapper
67 # The Wrapper handles CSRF injection automatically via the logic we added earlier
68 form_attrs = {
69 "action": self.action,
70 "method": self.method,
71 "class_": "space-y-4 bg-card p-6 rounded-lg shadow",
72 }
73 if self.hx_post:
74 form_attrs["hx_post"] = self.hx_post
75 if self.hx_target:
76 form_attrs["hx_target"] = self.hx_target
77 if self.hx_swap:
78 form_attrs["hx_swap"] = self.hx_swap
80 return Form(
81 children=[
82 h.h2(class_="text-lg font-medium text-foreground mb-4")[
83 self.schema.title
84 ],
85 form_content,
86 ],
87 **form_attrs, # type: ignore[arg-type]
88 )