Source code for animaid.html_button
"""HTMLButton - A clickable button widget."""
from __future__ import annotations
import html
import threading
from collections.abc import Callable
from typing import Self
[docs]
class HTMLButton:
"""A clickable button widget for use with App.
Buttons support click callbacks and styled presets for common use cases.
Examples:
>>> button = HTMLButton("Click Me")
>>> button = HTMLButton("Submit").primary()
>>> button = HTMLButton("Delete").danger().on_click(handle_delete)
# With App
>>> with App() as app:
... def on_click():
... print("Button clicked!")
... app.add(HTMLButton("Click").on_click(on_click))
"""
def __init__(self, label: str) -> None:
"""Create a button with the given label.
Args:
label: The text to display on the button.
"""
self._label = label
self._on_click: Callable[[], None] | None = None
self._styles: dict[str, str] = {}
self._css_classes: list[str] = ["anim-button"]
self._anim_id: str | None = None
self._lock = threading.Lock()
# For compatibility with HTMLInput interface
self._value = None
self._on_change = None
@property
def label(self) -> str:
"""Get the button label."""
return self._label
[docs]
def on_click(self, callback: Callable[[], None]) -> "HTMLButton":
"""Register a callback for button clicks.
The callback is called whenever the button is clicked in the browser.
The callback takes no arguments.
Args:
callback: A function to call when the button is clicked.
Returns:
Self for method chaining.
Examples:
>>> def handle_click():
... print("Button was clicked!")
>>> button = HTMLButton("Click Me").on_click(handle_click)
"""
self._on_click = callback
return self
[docs]
def render(self) -> str:
"""Return HTML representation of this button.
Returns:
A string containing valid HTML for the button.
"""
attrs = self._build_attributes()
escaped_label = html.escape(self._label)
# Add data-anim-id for event handling
anim_id_attr = ""
if self._anim_id:
anim_id_attr = f' data-anim-id="{html.escape(self._anim_id)}"'
if attrs:
return f"<button {attrs}{anim_id_attr}>{escaped_label}</button>"
return f"<button{anim_id_attr}>{escaped_label}</button>"
def __html__(self) -> str:
"""Jinja2 auto-escaping protocol."""
return self.render()
[docs]
def styled(self, **styles: str) -> "HTMLButton":
"""Return a copy with additional inline styles.
Style names use Python convention (underscores) and are
converted to CSS convention (hyphens) automatically.
Args:
**styles: CSS property-value pairs, e.g., font_size="16px"
Returns:
A new instance with the combined styles.
"""
new_button = HTMLButton(self._label)
new_button._on_click = self._on_click
new_button._anim_id = self._anim_id
new_button._styles = dict(self._styles)
new_button._css_classes = list(self._css_classes)
# Add new styles, converting underscores to hyphens
for key, value in styles.items():
css_key = key.replace("_", "-")
new_button._styles[css_key] = value
return new_button
[docs]
def add_class(self, *class_names: str) -> "HTMLButton":
"""Return a copy with additional CSS classes.
Args:
*class_names: CSS class names to add.
Returns:
A new instance with the additional classes.
"""
new_button = HTMLButton(self._label)
new_button._on_click = self._on_click
new_button._anim_id = self._anim_id
new_button._styles = dict(self._styles)
new_button._css_classes = list(self._css_classes) + list(class_names)
return new_button
def _build_style_string(self) -> str:
"""Convert internal styles dict to CSS style attribute value."""
if not self._styles:
return ""
return "; ".join(f"{k}: {v}" for k, v in self._styles.items())
def _build_class_string(self) -> str:
"""Convert internal classes list to CSS class attribute value."""
if not self._css_classes:
return ""
return " ".join(self._css_classes)
def _build_attributes(self) -> str:
"""Build the complete HTML attributes string."""
parts = []
class_str = self._build_class_string()
if class_str:
parts.append(f'class="{class_str}"')
style_str = self._build_style_string()
if style_str:
parts.append(f'style="{style_str}"')
return " ".join(parts)
# Styled presets
[docs]
def primary(self) -> "HTMLButton":
"""Return a copy styled as a primary button (blue).
Returns:
A new instance with primary styling.
"""
return self.add_class("primary")
[docs]
def success(self) -> "HTMLButton":
"""Return a copy styled as a success button (green).
Returns:
A new instance with success styling.
"""
return self.add_class("success")
[docs]
def danger(self) -> "HTMLButton":
"""Return a copy styled as a danger button (red).
Returns:
A new instance with danger styling.
"""
return self.add_class("danger")
[docs]
def warning(self) -> "HTMLButton":
"""Return a copy styled as a warning button (orange).
Returns:
A new instance with warning styling.
"""
return self.add_class("warning")
[docs]
def large(self) -> "HTMLButton":
"""Return a copy styled with larger text and padding.
Returns:
A new instance with large styling.
"""
return self.styled(font_size="18px", padding="14px 28px")
[docs]
def small(self) -> "HTMLButton":
"""Return a copy styled with smaller text and padding.
Returns:
A new instance with small styling.
"""
return self.styled(font_size="12px", padding="6px 12px")