@@ -111,6 +111,32 @@ def attrs(self, **attrs) -> Element:
111111 self ._attrs .update (attrs )
112112 return self
113113
114+ def on (self , event : str , handler : Callable ) -> Self :
115+ """
116+ Binds a python function to a DOM event.
117+ Serializes the function name to a data attribute.
118+ """
119+ if not hasattr (handler , "__name__" ):
120+ raise ValueError ("Event handler must be a named function" )
121+
122+ # We store it as a special attribute for the client runtime to find
123+ self ._attrs [f"data-on-{ event } " ] = handler .__name__
124+ return self
125+
126+ def has_bindings (self ) -> bool :
127+ """Checks if this element or any child has a Python event binding."""
128+ # Check self
129+ for key in self ._attrs :
130+ if key .startswith ("data-on-" ):
131+ return True
132+
133+ # Check children
134+ for child in self ._content :
135+ if isinstance (child , Element ) and child .has_bindings ():
136+ return True
137+
138+ return False
139+
114140 def _render (self , fp , indent : int ):
115141 parts = [self ._tag ]
116142
@@ -243,6 +269,18 @@ class StyleResource:
243269 inline : bool = False
244270
245271
272+ @dataclass
273+ class ScriptResource :
274+ """
275+ Represents a JS script resource.
276+ """
277+
278+ src : str | None = None
279+ content : str | None = None
280+ defer : bool = False
281+ module : bool = False
282+
283+
246284class Document (Markup ):
247285 def __init__ (self , lang : str = "en" , ** head_kwargs ) -> None :
248286 self .lang = lang
@@ -274,6 +312,20 @@ def style(
274312
275313 return self
276314
315+ def script (
316+ self ,
317+ src : str | None = None ,
318+ content : str | None = None ,
319+ defer : bool = False ,
320+ module : bool = False ,
321+ ) -> Document :
322+ self .head .scripts .append (
323+ ScriptResource (
324+ src , textwrap .dedent (content ) if content else None , defer , module
325+ )
326+ )
327+ return self
328+
277329 def _render (self , fp , indent : int ):
278330 self ._write_line (fp , "<!DOCTYPE html>" )
279331 self ._write_line (fp , f'<html lang="{ self .lang } ">' )
@@ -287,6 +339,7 @@ def __init__(self, charset: str = "UTF-8", title: str = "") -> None:
287339 self .charset = charset
288340 self .title = title
289341 self .styles : list [StyleResource ] = []
342+ self .scripts : list [ScriptResource ] = []
290343
291344 def _render (self , fp , indent : int ):
292345 self ._write_line (fp , "<head>" , indent )
@@ -301,19 +354,36 @@ def _render(self, fp, indent: int):
301354 )
302355 self ._write_line (fp , f"<title>{ self .title } </title>" , indent + 1 )
303356
304- for resource in self .styles :
305- if resource .inline and resource .sheet :
357+ for style in self .styles :
358+ if style .inline and style .sheet :
306359 # Render Inline
307360 self ._write_line (fp , "<style>" , indent + 1 )
308- resource . sheet .render (fp )
361+ self . _write_line ( fp , style . sheet .render (), indent + 1 )
309362 self ._write_line (fp , "</style>" , indent + 1 )
310363
311- elif resource .href :
364+ elif style .href :
312365 # Render Link
313366 self ._write_line (
314- fp , f'<link rel="stylesheet" href="{ resource .href } ">' , indent + 1
367+ fp , f'<link rel="stylesheet" href="{ style .href } ">' , indent + 1
315368 )
316369
370+ for script in self .scripts :
371+ if script .src :
372+ # External script
373+ attrs = f'src="{ script .src } "'
374+ if script .defer :
375+ attrs += " defer"
376+ if script .module :
377+ attrs += ' type="module"'
378+ self ._write_line (fp , f"<script { attrs } ></script>" , indent + 1 )
379+
380+ elif script .content :
381+ # Inline script
382+ attrs = ' type="module"' if script .module else ""
383+ self ._write_line (fp , f"<script{ attrs } >" , indent + 1 )
384+ self ._write_line (fp , script .content , indent + 1 )
385+ self ._write_line (fp , "</script>" , indent + 1 )
386+
317387 self ._write_line (fp , "</head>" , indent )
318388
319389
0 commit comments