Coverage for src / lexigram / ui / organisms / forms.py: 14%
51 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
1from __future__ import annotations
3from typing import Any
5from lexigram.ui.atoms.button import Button
6from lexigram.ui.core.base import Component, el
9class Form(Component):
10 """
11 A container for form fields with HTMX submission support.
12 """
14 def __init__(
15 self,
16 action_url: str | None = None,
17 method: str = "post",
18 submit_label: str = "Save",
19 hx_target: str = "#main-content",
20 hx_swap: str = "innerHTML",
21 autosave: bool = False,
22 form_id: str | None = None,
23 suppress_submit: bool = False,
24 **props,
25 ) -> None:
26 super().__init__(
27 action_url=action_url,
28 method=method,
29 submit_label=submit_label,
30 hx_target=hx_target,
31 hx_swap=hx_swap,
32 autosave=autosave,
33 form_id=form_id,
34 suppress_submit=suppress_submit,
35 **props,
36 )
37 self.action_url = action_url
38 self.method = method
39 self.submit_label = submit_label
40 self.hx_target = hx_target
41 self.hx_swap = hx_swap
42 self.autosave = autosave
43 self.form_id = form_id
44 self.suppress_submit = suppress_submit
46 def render(self) -> Any:
47 attrs = {
48 "method": self.method,
49 "class": "space-y-6",
50 }
52 submit_button_attrs = {}
53 if self.action_url:
54 if self.method.lower() == "get":
55 attrs["hx-get"] = self.action_url
56 elif self.autosave and self.form_id:
57 attrs["action"] = self.action_url
58 attrs["method"] = "post"
59 submit_button_attrs["type"] = "button"
60 submit_button_attrs["onclick"] = (
61 "var f=this.closest('form');f.submit();"
62 )
63 else:
64 attrs["action"] = self.action_url
65 attrs["method"] = "post"
66 submit_button_attrs["type"] = "button"
67 submit_button_attrs["onclick"] = "var f=this.closest('form');f.submit()"
69 # Handle Auto-save logic
70 autosave_indicator = ""
71 if self.autosave and self.form_id:
72 pulse_url = f"/api/forms/draft/{self.form_id}"
74 # Additional attributes for autosave
75 attrs["hx-target"] = self.hx_target
76 attrs["hx-swap"] = self.hx_swap
77 attrs["hx-trigger"] = "change delay:2s, autosave-pulse from:window"
79 # The pulse should target a specific indicator to show "Saving..."
80 indicator_id = f"autosave-status-{self.form_id}"
81 attrs["hx-indicator"] = f"#{indicator_id}"
83 # If autosave is enabled, we use a different endpoint for the FORM's automatic pulses
84 attrs["hx-post"] = pulse_url
86 autosave_indicator = el(
87 "div",
88 el("div", "", id=indicator_id, class_="htmx-indicator"),
89 el("span", "", id=f"{indicator_id}-result"),
90 class_="flex items-center gap-2",
91 )
93 footer = (
94 el(
95 "div",
96 el(
97 "div",
98 Button(
99 self.submit_label,
100 type=submit_button_attrs.get("type", "submit"),
101 class_="w-full sm:w-auto",
102 onclick=submit_button_attrs.get("onclick"),
103 ),
104 autosave_indicator,
105 class_="flex items-center justify-between w-full",
106 ),
107 class_="pt-4 border-t border-border mt-6",
108 )
109 if self.submit_label and not getattr(self, "suppress_submit", False)
110 else ""
111 )
113 # Inject CSRF token if available in request context
114 csrf_input = ""
115 try:
116 request = getattr(self, "_request", None)
117 if (
118 request
119 and hasattr(request, "state")
120 and hasattr(request.state, "csrf_token")
121 ):
122 csrf_input = el(
123 "input",
124 type="hidden",
125 name="csrf_token",
126 value=request.state.csrf_token,
127 )
128 except AttributeError as e:
129 from lexigram.logging import get_logger
131 logger = get_logger(__name__)
132 logger.debug(
133 "Failed to retrieve request/CSRF token from context: %s",
134 e,
135 exc_info=True,
136 )
138 return el("form", csrf_input, *self.children, footer, **attrs)