Coverage for src / lexigram / ui / htmx / helpers.py: 35%
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
1"""HTMX performance optimization patterns and helpers.
3This module provides utilities for optimizing HTMX-based interactions with
4techniques like selective swaps, prefetching, and efficient DOM updates.
5"""
7from __future__ import annotations
9from typing import TYPE_CHECKING, Any
11if TYPE_CHECKING:
12 from htpy import Element
15def hx_swap_oob(*elements: tuple[str, Element]) -> list[Element]:
16 """Create out-of-band swap elements for multiple updates.
18 Out-of-band (OOB) swaps allow updating multiple parts of the page
19 with a single HTMX request, improving performance.
21 Args:
22 *elements: Tuples of (target_id, element) to swap
24 Returns:
25 List of elements with hx-swap-oob attributes
27 Example:
28 ```python
29 from htpy import div
31 # Update multiple elements at once
32 return [
33 div["Main content"], # Normal swap
34 *hx_swap_oob(
35 ("notifications", Badge("3 new messages")),
36 ("user-menu", UserMenu(user)),
37 )
38 ]
39 ```
40 """
41 from htpy import div
43 result = []
44 for target_id, element in elements:
45 # Wrap in div with hx-swap-oob attribute
46 wrapped = div[element][{"hx-swap-oob": f"innerHTML:#{target_id}"}]
47 result.append(wrapped)
48 return result
51def hx_prefetch(
52 url: str,
53 trigger: str = "mouseenter",
54 threshold: str = "200ms",
55) -> dict[str, str]:
56 """Create attributes for HTMX prefetching.
58 Prefetch content before the user clicks, making navigation instant.
60 Args:
61 url: URL to prefetch
62 trigger: Event that triggers prefetch (default: mouseenter)
63 threshold: Delay before prefetching (default: 200ms)
65 Returns:
66 Dictionary of HTMX attributes
68 Example:
69 ```python
70 # Prefetch on hover
71 a["View Details"][
72 {
73 "href": "/users/123",
74 **hx_prefetch("/users/123"),
75 }
76 ]
77 ```
78 """
79 return {
80 "hx-get": url,
81 "hx-trigger": f"{trigger} throttle:{threshold}",
82 "hx-swap": "none", # Don't swap on prefetch
83 }
86def hx_lazy_load(
87 url: str,
88 trigger: str = "revealed",
89 threshold: str = "0px",
90 placeholder: str | None = None,
91) -> dict[str, str]:
92 """Create attributes for lazy loading content when scrolled into view.
94 Args:
95 url: URL to load content from
96 trigger: Trigger event (default: revealed)
97 threshold: Distance from viewport to trigger (default: 0px)
98 placeholder: Placeholder text while loading
100 Returns:
101 Dictionary of HTMX attributes
103 Example:
104 ```python
105 # Load when scrolled into view
106 div["Loading..."][
107 {
108 **hx_lazy_load("/api/heavy-content"),
109 }
110 ]
111 ```
112 """
113 attrs = {
114 "hx-get": url,
115 "hx-trigger": f"{trigger} threshold:{threshold}",
116 "hx-swap": "outerHTML",
117 }
118 if placeholder:
119 attrs["hx-indicator"] = placeholder
120 return attrs
123def hx_morph(target: str | None = None) -> dict[str, str]:
124 """Create attributes for morphing (minimal DOM updates).
126 Morphing intelligently updates only changed parts of the DOM,
127 preserving focus, scroll position, and component state.
129 Args:
130 target: Optional target selector
132 Returns:
133 Dictionary of HTMX attributes
135 Example:
136 ```python
137 # Morph instead of replacing entire element
138 div["Content"][
139 {
140 **hx_morph(),
141 "hx-get": "/api/update",
142 }
143 ]
144 ```
145 """
146 attrs = {"hx-swap": "morph"}
147 if target:
148 attrs["hx-target"] = target
149 return attrs
152def hx_preserve(*selectors: str) -> dict[str, str]:
153 """Create attributes to preserve elements during swap.
155 Useful for maintaining state of inputs, scroll position, etc.
157 Args:
158 *selectors: CSS selectors of elements to preserve
160 Returns:
161 Dictionary of HTMX attributes
163 Example:
164 ```python
165 # Preserve input values during update
166 form["..."][
167 {
168 **hx_preserve("input", "textarea"),
169 }
170 ]
171 ```
172 """
173 return {"hx-preserve": ",".join(selectors)}
176def hx_boost(
177 enable: bool = True,
178 target: str | None = None,
179 swap: str = "innerHTML",
180) -> dict[str, str]:
181 """Create attributes for boosting standard links/forms.
183 Boosting converts normal links and forms to use HTMX, making
184 the site feel like a SPA without rewriting existing code.
186 Args:
187 enable: Whether to enable boosting
188 target: Target selector for swaps
189 swap: Swap strategy
191 Returns:
192 Dictionary of HTMX attributes
194 Example:
195 ```python
196 # Boost all links in navigation
197 nav["..."][
198 {
199 **hx_boost(target="#content"),
200 }
201 ]
202 ```
203 """
204 attrs = {"hx-boost": "true" if enable else "false"}
205 if target:
206 attrs["hx-target"] = target
207 if swap != "innerHTML":
208 attrs["hx-swap"] = swap
209 return attrs
212def hx_debounce(delay: str = "500ms") -> dict[str, str]:
213 """Create attributes for debouncing requests.
215 Debouncing prevents sending too many requests during rapid user input.
217 Args:
218 delay: Debounce delay (e.g., "500ms", "1s")
220 Returns:
221 Dictionary of HTMX attributes
223 Example:
224 ```python
225 # Debounce search input
226 input_[
227 {
228 "type": "search",
229 "hx-get": "/search",
230 "hx-trigger": "keyup changed",
231 **hx_debounce("300ms"),
232 }
233 ]
234 ```
235 """
236 return {"hx-trigger": f"keyup changed delay:{delay}"}
239def hx_optimistic(
240 indicator: str | None = None,
241 settle_delay: str = "0ms",
242) -> dict[str, str]:
243 """Create attributes for optimistic UI updates.
245 Shows immediate feedback while request is processing.
247 Args:
248 indicator: Loading indicator selector
249 settle_delay: Delay before settling (default: 0ms for instant)
251 Returns:
252 Dictionary of HTMX attributes
254 Example:
255 ```python
256 # Show immediate update, then settle
257 button["Like"][
258 {
259 **hx_optimistic(indicator="#spinner"),
260 "hx-post": "/like",
261 }
262 ]
263 ```
264 """
265 attrs = {"hx-swap": f"innerHTML settle:{settle_delay}"}
266 if indicator:
267 attrs["hx-indicator"] = indicator
268 return attrs
271def hx_polling(
272 interval: str = "2s",
273 url: str | None = None,
274) -> dict[str, str]:
275 """Create attributes for polling updates.
277 Args:
278 interval: Polling interval (e.g., "2s", "5000ms")
279 url: Optional URL to poll (uses current if not specified)
281 Returns:
282 Dictionary of HTMX attributes
284 Example:
285 ```python
286 # Poll for updates every 5 seconds
287 div["Status: ..."][
288 {
289 **hx_polling("5s", "/api/status"),
290 }
291 ]
292 ```
293 """
294 attrs = {"hx-trigger": f"every {interval}"}
295 if url:
296 attrs["hx-get"] = url
297 return attrs
300def hx_websocket(url: str) -> dict[str, str]:
301 """Create attributes for WebSocket connection.
303 Args:
304 url: WebSocket URL
306 Returns:
307 Dictionary of HTMX attributes
309 Example:
310 ```python
311 # Connect to WebSocket
312 div["Messages"][
313 {
314 **hx_websocket("ws://localhost:8000/ws"),
315 }
316 ]
317 ```
318 """
319 return {"hx-ws": f"connect:{url}"}
322def hx_sse(url: str, swap: str = "message") -> dict[str, str]:
323 """Create attributes for Server-Sent Events.
325 Args:
326 url: SSE endpoint URL
327 swap: Event name to swap on (default: message)
329 Returns:
330 Dictionary of HTMX attributes
332 Example:
333 ```python
334 # Listen for SSE updates
335 div["Live Updates"][
336 {
337 **hx_sse("/events", swap="update"),
338 }
339 ]
340 ```
341 """
342 return {
343 "hx-sse": f"connect:{url}",
344 "hx-trigger": f"sse:{swap}",
345 }
348def optimistic_update(
349 target: str,
350 content: str,
351 trigger: str = "click",
352 **kwargs: Any,
353) -> dict[str, Any]:
354 """Optimistic UI update via hx-on::before-request.
356 Args:
357 target: CSS selector for the target element.
358 content: HTML content to swap in immediately.
359 trigger: HTMX trigger event. Defaults to "click".
361 Returns:
362 HTMX attributes dict for optimistic update.
363 """
364 return {
365 "hx-on::before-request": f"document.querySelector('{target}').innerHTML = '{content}'",
366 **kwargs,
367 }
370def hx_optimistic_swap(target: str, html_snippet: str) -> dict[str, Any]:
371 """Optimistic swap on click via hx-on attribute.
373 Note: this is distinct from `hx_optimistic` above, which produces a
374 settle-delay indicator pattern. `hx_optimistic_swap` swaps content
375 immediately into a target on click — useful for instant placeholders.
377 Args:
378 target: CSS selector for the target element.
379 html_snippet: HTML content to swap in (single quotes escaped).
381 Returns:
382 HTMX attributes dict for optimistic swap.
383 """
384 safe_snippet = html_snippet.replace("'", "\\'")
385 return {
386 "hx-on-click": f"document.querySelector('{target}').innerHTML = '{safe_snippet}'",
387 }