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

1from __future__ import annotations 

2 

3from typing import Any 

4 

5from lexigram.ui.atoms.button import Button 

6from lexigram.ui.core.base import Component, el 

7 

8 

9class Form(Component): 

10 """ 

11 A container for form fields with HTMX submission support. 

12 """ 

13 

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 

45 

46 def render(self) -> Any: 

47 attrs = { 

48 "method": self.method, 

49 "class": "space-y-6", 

50 } 

51 

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()" 

68 

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}" 

73 

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" 

78 

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}" 

82 

83 # If autosave is enabled, we use a different endpoint for the FORM's automatic pulses 

84 attrs["hx-post"] = pulse_url 

85 

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 ) 

92 

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 ) 

112 

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 

130 

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 ) 

137 

138 return el("form", csrf_input, *self.children, footer, **attrs)