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

1"""Dynamic Form Component. 

2 

3Renders a FormSchema into HTML using htpy. 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import TYPE_CHECKING, Any 

9 

10import htpy as h 

11 

12from lexigram.admin.schema import BooleanField 

13from lexigram.ui import Button, Component, Form 

14 

15if TYPE_CHECKING: 

16 from lexigram.admin.forms import FormSchema 

17 

18 

19class DynamicForm(Component): 

20 """Renders a form based on a schema.""" 

21 

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 

39 

40 def render(self) -> Any: 

41 # We wrap the content in a list of htpy nodes 

42 form_content: list[Any] = [] 

43 

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 

49 

50 form_content.append(field.render_form(field.get_default())) 

51 

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 ) 

58 

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 ) 

65 

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 

79 

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 )