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

1"""HTMX performance optimization patterns and helpers. 

2 

3This module provides utilities for optimizing HTMX-based interactions with 

4techniques like selective swaps, prefetching, and efficient DOM updates. 

5""" 

6 

7from __future__ import annotations 

8 

9from typing import TYPE_CHECKING, Any 

10 

11if TYPE_CHECKING: 

12 from htpy import Element 

13 

14 

15def hx_swap_oob(*elements: tuple[str, Element]) -> list[Element]: 

16 """Create out-of-band swap elements for multiple updates. 

17 

18 Out-of-band (OOB) swaps allow updating multiple parts of the page 

19 with a single HTMX request, improving performance. 

20 

21 Args: 

22 *elements: Tuples of (target_id, element) to swap 

23 

24 Returns: 

25 List of elements with hx-swap-oob attributes 

26 

27 Example: 

28 ```python 

29 from htpy import div 

30 

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 

42 

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 

49 

50 

51def hx_prefetch( 

52 url: str, 

53 trigger: str = "mouseenter", 

54 threshold: str = "200ms", 

55) -> dict[str, str]: 

56 """Create attributes for HTMX prefetching. 

57 

58 Prefetch content before the user clicks, making navigation instant. 

59 

60 Args: 

61 url: URL to prefetch 

62 trigger: Event that triggers prefetch (default: mouseenter) 

63 threshold: Delay before prefetching (default: 200ms) 

64 

65 Returns: 

66 Dictionary of HTMX attributes 

67 

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 } 

84 

85 

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. 

93 

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 

99 

100 Returns: 

101 Dictionary of HTMX attributes 

102 

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 

121 

122 

123def hx_morph(target: str | None = None) -> dict[str, str]: 

124 """Create attributes for morphing (minimal DOM updates). 

125 

126 Morphing intelligently updates only changed parts of the DOM, 

127 preserving focus, scroll position, and component state. 

128 

129 Args: 

130 target: Optional target selector 

131 

132 Returns: 

133 Dictionary of HTMX attributes 

134 

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 

150 

151 

152def hx_preserve(*selectors: str) -> dict[str, str]: 

153 """Create attributes to preserve elements during swap. 

154 

155 Useful for maintaining state of inputs, scroll position, etc. 

156 

157 Args: 

158 *selectors: CSS selectors of elements to preserve 

159 

160 Returns: 

161 Dictionary of HTMX attributes 

162 

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

174 

175 

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. 

182 

183 Boosting converts normal links and forms to use HTMX, making 

184 the site feel like a SPA without rewriting existing code. 

185 

186 Args: 

187 enable: Whether to enable boosting 

188 target: Target selector for swaps 

189 swap: Swap strategy 

190 

191 Returns: 

192 Dictionary of HTMX attributes 

193 

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 

210 

211 

212def hx_debounce(delay: str = "500ms") -> dict[str, str]: 

213 """Create attributes for debouncing requests. 

214 

215 Debouncing prevents sending too many requests during rapid user input. 

216 

217 Args: 

218 delay: Debounce delay (e.g., "500ms", "1s") 

219 

220 Returns: 

221 Dictionary of HTMX attributes 

222 

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

237 

238 

239def hx_optimistic( 

240 indicator: str | None = None, 

241 settle_delay: str = "0ms", 

242) -> dict[str, str]: 

243 """Create attributes for optimistic UI updates. 

244 

245 Shows immediate feedback while request is processing. 

246 

247 Args: 

248 indicator: Loading indicator selector 

249 settle_delay: Delay before settling (default: 0ms for instant) 

250 

251 Returns: 

252 Dictionary of HTMX attributes 

253 

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 

269 

270 

271def hx_polling( 

272 interval: str = "2s", 

273 url: str | None = None, 

274) -> dict[str, str]: 

275 """Create attributes for polling updates. 

276 

277 Args: 

278 interval: Polling interval (e.g., "2s", "5000ms") 

279 url: Optional URL to poll (uses current if not specified) 

280 

281 Returns: 

282 Dictionary of HTMX attributes 

283 

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 

298 

299 

300def hx_websocket(url: str) -> dict[str, str]: 

301 """Create attributes for WebSocket connection. 

302 

303 Args: 

304 url: WebSocket URL 

305 

306 Returns: 

307 Dictionary of HTMX attributes 

308 

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

320 

321 

322def hx_sse(url: str, swap: str = "message") -> dict[str, str]: 

323 """Create attributes for Server-Sent Events. 

324 

325 Args: 

326 url: SSE endpoint URL 

327 swap: Event name to swap on (default: message) 

328 

329 Returns: 

330 Dictionary of HTMX attributes 

331 

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 } 

346 

347 

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. 

355 

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

360 

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 } 

368 

369 

370def hx_optimistic_swap(target: str, html_snippet: str) -> dict[str, Any]: 

371 """Optimistic swap on click via hx-on attribute. 

372 

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. 

376 

377 Args: 

378 target: CSS selector for the target element. 

379 html_snippet: HTML content to swap in (single quotes escaped). 

380 

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 }