djhtmx 1.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,991 @@
1
+ Metadata-Version: 2.4
2
+ Name: djhtmx
3
+ Version: 1.2.6
4
+ Summary: Interactive UI Components for Django using HTMX
5
+ Project-URL: Homepage, https://github.com/edelvalle/djhtmx
6
+ Project-URL: Documentation, https://github.com/edelvalle/djhtmx#readme
7
+ Project-URL: Repository, https://github.com/edelvalle/djhtmx.git
8
+ Project-URL: Issues, https://github.com/edelvalle/djhtmx/issues
9
+ Project-URL: Changelog, https://github.com/edelvalle/djhtmx/blob/master/CHANGELOG.md
10
+ Author-email: Eddy Ernesto del Valle Pino <eddy@edelvalle.me>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: django,htmx,liveview,reactive,real-time,spa,websockets
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Framework :: Django
17
+ Classifier: Framework :: Django :: 4.1
18
+ Classifier: Framework :: Django :: 4.2
19
+ Classifier: Framework :: Django :: 5.0
20
+ Classifier: Framework :: Django :: 5.1
21
+ Classifier: Intended Audience :: Developers
22
+ Classifier: License :: OSI Approved :: MIT License
23
+ Classifier: Operating System :: OS Independent
24
+ Classifier: Programming Language :: Python
25
+ Classifier: Programming Language :: Python :: 3
26
+ Classifier: Programming Language :: Python :: 3.11
27
+ Classifier: Programming Language :: Python :: 3.12
28
+ Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Topic :: Internet :: WWW/HTTP
30
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
31
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
32
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
33
+ Requires-Python: >=3.11
34
+ Requires-Dist: channels>=4.1.0
35
+ Requires-Dist: django>=4.1
36
+ Requires-Dist: mmh3>=5.1.0
37
+ Requires-Dist: orjson>=3.10.7
38
+ Requires-Dist: pydantic<3,>=2
39
+ Requires-Dist: redis[hiredis]>=5.0.8
40
+ Requires-Dist: uuid6>=2024.7.10
41
+ Requires-Dist: xotl-tools>=3.1.1
42
+ Provides-Extra: logfire
43
+ Requires-Dist: logfire[django]>=3.8.0; extra == 'logfire'
44
+ Provides-Extra: sentry
45
+ Requires-Dist: sentry-sdk>=2.19; extra == 'sentry'
46
+ Description-Content-Type: text/markdown
47
+
48
+ # djhtmx
49
+
50
+ [![CI](https://github.com/edelvalle/djhtmx/actions/workflows/ci.yml/badge.svg)](https://github.com/edelvalle/djhtmx/actions/workflows/ci.yml)
51
+ [![codecov](https://codecov.io/gh/edelvalle/djhtmx/branch/master/graph/badge.svg)](https://codecov.io/gh/edelvalle/djhtmx)
52
+
53
+ Interactive UI Components for Django using [htmx](https://htmx.org)
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ uv add djhtmx
59
+ ```
60
+
61
+ or
62
+
63
+ ```bash
64
+ pip install djhtmx
65
+ ```
66
+
67
+ # Configuration
68
+
69
+ ## Requirements
70
+
71
+ djhtmx requires **Redis** to be running for session storage and component state management.
72
+
73
+ **Important**: Redis is not included with djhtmx and must be installed separately on your system. Make sure Redis is installed and accessible before using djhtmx.
74
+
75
+ ### Installing Redis
76
+
77
+ - **macOS**: `brew install redis`
78
+ - **Ubuntu/Debian**: `sudo apt-get install redis-server`
79
+ - **CentOS/RHEL**: `sudo yum install redis` or `sudo dnf install redis`
80
+ - **Docker**: `docker run -d -p 6379:6379 redis:alpine`
81
+ - **Windows**: Download from [Redis for Windows](https://github.com/microsoftarchive/redis/releases)
82
+
83
+ Add `djhtmx` to your `INSTALLED_APPS`.
84
+
85
+ ```python
86
+ INSTALLED_APPS = [
87
+ ...
88
+ "djhtmx",
89
+ ...
90
+ ]
91
+ ```
92
+
93
+ Install the Middleware as the last one of the list
94
+
95
+ ```python
96
+ MIDDLEWARE = [
97
+ ...,
98
+ "djhtmx.middleware",
99
+ ]
100
+ ```
101
+
102
+ Add `djhtmx.context.component_repo` to the list of context processors:
103
+
104
+ ```python
105
+ TEMPLATES = [
106
+ {
107
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
108
+ "DIRS": [],
109
+ "APP_DIRS": True,
110
+ "OPTIONS": {
111
+ "context_processors": [
112
+ ...,
113
+ "djhtmx.context.component_repo",
114
+ ],
115
+ },
116
+ },
117
+ ]
118
+
119
+ ```
120
+
121
+ Expose the HTTP endpoint in your `urls.py` as you wish, you can use any path you want.
122
+
123
+ ```python
124
+ from django.urls import path, include
125
+
126
+ urlpatterns = [
127
+ # ...
128
+ path("_htmx/", include("djhtmx.urls")),
129
+ # ...
130
+ ]
131
+ ```
132
+
133
+ ## Settings
134
+
135
+ djhtmx can be configured through Django settings:
136
+
137
+ ### Required Settings
138
+
139
+ - **`DJHTMX_REDIS_URL`** (default: `"redis://localhost/0"`): Redis connection URL for session storage and component state management.
140
+
141
+ ### Optional Settings
142
+
143
+ - **`DJHTMX_SESSION_TTL`** (default: `3600`): Session timeout in seconds. Can be an integer or a `datetime.timedelta` object.
144
+ - **`DJHTMX_DEFAULT_LAZY_TEMPLATE`** (default: `"htmx/lazy.html"`): Default template for lazy-loaded components.
145
+ - **`DJHTMX_ENABLE_SENTRY_TRACING`** (default: `True`): Enable Sentry tracing integration.
146
+ - **`DJHTMX_ENABLE_LOGFIRE_TRACING`** (default: `False`): Enable Logfire tracing integration.
147
+ - **`DJHTMX_STRICT_EVENT_HANDLER_CONSISTENCY_CHECK`** (default: `False`): Enable strict consistency checking for event handlers.
148
+ - **`DJHTMX_KEY_SIZE_ERROR_THRESHOLD`** (default: `0`): Threshold in bytes for session key size errors (0 = disabled).
149
+ - **`DJHTMX_KEY_SIZE_WARN_THRESHOLD`** (default: `51200`): Threshold in bytes for session key size warnings (50KB).
150
+ - **`DJHTMX_KEY_SIZE_SAMPLE_PROB`** (default: `0.1`): Probability for sampling session key size checks.
151
+
152
+ ### Example Configuration
153
+
154
+ ```python
155
+ # settings.py
156
+
157
+ # Redis connection (required)
158
+ DJHTMX_REDIS_URL = "redis://localhost:6379/0" # or redis://user:password@host:port/db
159
+
160
+ # Optional settings
161
+ DJHTMX_SESSION_TTL = 7200 # 2 hours
162
+ DJHTMX_DEFAULT_LAZY_TEMPLATE = "my_app/lazy_component.html"
163
+ DJHTMX_ENABLE_SENTRY_TRACING = True
164
+ DJHTMX_KEY_SIZE_WARN_THRESHOLD = 100 * 1024 # 100KB
165
+ ```
166
+
167
+ In your base template you need to load the necessary scripts to make this work
168
+
169
+ ```html
170
+ {% load htmx %}
171
+ <!doctype html>
172
+ <html>
173
+ <head>
174
+ {% htmx-headers %}
175
+ </head>
176
+ </html>
177
+ ```
178
+
179
+ ## Getting started
180
+
181
+ **Important**: djhtmx is a framework for building interactive components, not a component library. No pre-built components, templates, or behaviors are provided. You need to create your own components from scratch using the framework's base classes and conventions.
182
+
183
+ This library is opinionated about how to use HTMX with Django, but it is not opinionated about components, styling, or specific functionality. You have complete freedom to design and implement your components as needed for your application.
184
+
185
+ This app will look for `htmx.py` files in your app and registers all components found there, but if you load any module where you have components manually when Django boots up, that also works.
186
+
187
+ ### Component Organization
188
+
189
+ As of version 1.2.0, djhtmx supports both single file and directory-based component organization:
190
+
191
+ **Single file (traditional):**
192
+ ```
193
+ myapp/
194
+ ├── htmx.py # All components in one file
195
+ └── ...
196
+ ```
197
+
198
+ **Directory structure (new in v1.2.0):**
199
+ ```
200
+ myapp/
201
+ ├── htmx/
202
+ │ ├── __init__.py
203
+ │ ├── components.py # Basic components
204
+ │ ├── forms.py # Form components
205
+ │ └── widgets/
206
+ │ ├── __init__.py
207
+ │ ├── calendar.py # Calendar widgets
208
+ │ └── charts.py # Chart widgets
209
+ └── ...
210
+ ```
211
+
212
+ The autodiscovery system will recursively find and import all Python modules under `htmx/` directories, allowing you to organize your components in a structured way that scales with your project size.
213
+
214
+ ```python
215
+ from djhtmx.component import HtmxComponent
216
+
217
+
218
+ class Counter(HtmxComponent):
219
+ _template_name = "Counter.html"
220
+ counter: int = 0
221
+
222
+ def inc(self, amount: int = 1):
223
+ self.counter += amount
224
+ ```
225
+
226
+ The `inc` event handler is ready to be called from the front-end to respond to an event.
227
+
228
+ The `counter.html` would be:
229
+
230
+ ```html
231
+ {% load htmx %}
232
+ <div {% hx-tag %}>
233
+ {{ counter }}
234
+ <button {% on "inc" %}>+</button>
235
+ <button {% on "inc" amount=2 %}>+2</button>
236
+ </div>
237
+ ```
238
+
239
+ When the event is dispatched to the back-end the component state is reconstructed, the event handler called and then the full component is rendered back to the front-end.
240
+
241
+ Now use the component in any of your html templates, by passing attributes that are part of the component state:
242
+
243
+ ```html
244
+ {% load htmx %}
245
+
246
+ Counters: <br />
247
+ {% htmx "Counter" %} Counter with init value 3:<br />
248
+ {% htmx "Counter" counter=3 %}
249
+ ```
250
+
251
+ ## Doing more complicated stuff
252
+
253
+ ### Authentication
254
+
255
+ All components have a `self.user` representing the current logged in user or `None` in case the user is anonymous. If you wanna make sure your user is properly validated and enforced. You need to create a base component and annotate the right user:
256
+
257
+ ```python
258
+ from typing import Annotated
259
+ from pydantic import Field
260
+ from djhtmx.component import HtmxComponent
261
+
262
+ class BaseComponent(HtmxComponent, public=False):
263
+ user: Annotated[User, Field(exclude=True)]
264
+
265
+
266
+ class Counter(BaseComponent):
267
+ _template_name = "Counter.html"
268
+ counter: int = 0
269
+
270
+ def inc(self, amount: int = 1):
271
+ self.counter += amount
272
+ ```
273
+
274
+ ### Non-public components
275
+
276
+ These are components that can't be instantiated using `{% htmx "ComponentName" %}` because they are used to create some abstraction and reuse code.
277
+
278
+ Pass `public=False` in their declaration
279
+
280
+ ```python
281
+ class BaseComponent(HtmxComponent, public=False):
282
+ ...
283
+ ```
284
+
285
+ ## Component nesting
286
+
287
+ Components can contain components inside to decompose the behavior in more granular and specialized parts, for this you don't have to do anything but to a component inside the template of other component....
288
+
289
+ ```python
290
+ class Items(HtmxComponent):
291
+ _template_name = "Items.html"
292
+
293
+ def items(self):
294
+ return Item.objects.all()
295
+
296
+ class ItemEntry(HtmxComponent):
297
+ ...
298
+ item: Item
299
+ is_open: bool = False
300
+ ...
301
+ ```
302
+
303
+ `Items.html`:
304
+
305
+ ```html
306
+ {% load htmx %}
307
+
308
+ <ul {% hx-tag %}>
309
+ {% for item in items %}
310
+ {% htmx "ItemEntry" item=item %}
311
+ {% endfor %}
312
+ </ul>
313
+ ```
314
+
315
+ In this case every time there is a render of the parent component all children components will also be re-rendered.
316
+
317
+ How can you preserve the state in the child components if there were some of them that were already had `is_open = True`? The state that is not passed directly during instantiation to the component is retrieved from the session, but the component needs to have consistent id. To do this you have to pass an `id` to the component.
318
+
319
+ `Items.html`:
320
+
321
+ ```html
322
+ {% load htmx %}
323
+
324
+ <ul {% hx-tag %}>
325
+ {% for item in items %}
326
+ {% htmx "ItemEntry" id="item-"|add:item.id item=item %}
327
+ {% endfor %}
328
+ </ul>
329
+ ```
330
+
331
+ ## Lazy lading
332
+
333
+ If you want some component to load lazily, you pass `lazy=True` where it is being instantiated.
334
+
335
+
336
+ `Items.html`:
337
+
338
+ ```html
339
+ {% load htmx %}
340
+
341
+ <ul {% hx-tag %}>
342
+ {% for item in items %}
343
+ {% htmx "ItemEntry" id="item-"|add:item.id item=item lazy=True %}
344
+ {% endfor %}
345
+ </ul>
346
+ ```
347
+
348
+ This makes the component to be initialized, but instead of rendering the template in `_template_name` the template defined in `_template_name_lazy` will be rendered (you can override this). When the component arrives to the front-end it will trigger an event to render it self.
349
+
350
+
351
+ ## Implicit parameters
352
+
353
+ When sending an event to the back-end sometimes you can pass the parameters explicitly to the event handler, and sometimes these are inputs the user is typing stuff on. The value of those inputs are passed implicitly if they nave a `name="..."` attribute.
354
+
355
+ ```python
356
+ class Component(HtmxComponent):
357
+ ...
358
+
359
+ def create(self, name: str, is_active: bool = False):
360
+ Item.objects.create(name=name, is_active=is_active)
361
+
362
+ ```
363
+
364
+ ```html
365
+ {% load htmx %}
366
+
367
+ <form {% hx-tag %} {% on "submit" "create" %}>
368
+ <input type="text" name="name">
369
+ <input type="checkbox" name="is_active">
370
+ <button type="submit">Create!</button>
371
+ </form>
372
+ ```
373
+
374
+ The parameters of any event handler are always converted by pydantic to the annotated types. It's suggested to properly annotate the event handler parameter with the more restrictive types you can.
375
+
376
+ ### Data structures in implicit parameters
377
+
378
+ Suppose that you have a multiple choice list and you want to select multiple options, you can do this by suffixing the name with `[]` as in `choices[]`:
379
+
380
+ ```python
381
+ class DeleteSelection(HtmxComponent):
382
+
383
+ @property
384
+ def items(self):
385
+ return self.filter(owner=self.user)
386
+
387
+ def delete(self, selected: list[UUID] | None = None):
388
+ if selected:
389
+ self.items.filter(id__in=selected).delete()
390
+ ```
391
+
392
+ ```html
393
+ {% load htmx %}
394
+
395
+ <form {% hx-tag %} {% on "submit" "delete" %}>
396
+ <h1>Select items to be deleted</h1>
397
+ {% for item in items %}
398
+ <p>
399
+ <input
400
+ type="checkbox"
401
+ name="selected[]"
402
+ value="{{ item.id }}"
403
+ id="checkbox-{{ item.id }}"
404
+ />
405
+ <label for="checkbox-{{ item.id }}">{{ item.name}}</label>
406
+
407
+ </p>
408
+ {% endfor %}
409
+ <p><button type="submit">Delete selected</button></p>
410
+ </form>
411
+
412
+ ```
413
+
414
+
415
+ ## Commands
416
+
417
+ Each event handler in a component can yield commands for the library to execute. These are useful for skipping the default component render, redirecting the user, remove the component from the front-end, updating other components, and rendering components with custom context.
418
+
419
+ ### Redirects
420
+
421
+ Wanna redirect the user to some object url:
422
+ - If you have the url directly you can `yield Redirect(url)`.
423
+
424
+ - If you want Django to resolve the url automatically use: `yield Redirect.to(obj, *args, **kwargs)` as you would use `django.shortcuts.resolve_url`.
425
+
426
+ ```python
427
+ from djhtmx.component import HtmxComponent, Redirect
428
+
429
+
430
+ class Component(HtmxComponent):
431
+ ...
432
+
433
+ def create(self, name: str):
434
+ item = Item.objects.create(name=name)
435
+ yield Redirect.to(item)
436
+ ```
437
+
438
+ If you want to open the url in a new url use the `yield Open...` command with similar syntax to `Redirect`.
439
+
440
+ ### Remove the current component from the interface
441
+
442
+ Sometimes you want to remove the component when it responds to an event, for that you need to `yield Destroy(component_id: str)`. You can also use this to remove any other component if you know their id.
443
+
444
+ ```python
445
+ from djhtmx.component import HtmxComponent, Destroy
446
+
447
+
448
+ class Notification(HtmxComponent):
449
+ ...
450
+
451
+ def close(self):
452
+ yield Destroy(self.id)
453
+ ```
454
+
455
+ ### Skip renders
456
+
457
+ Sometimes when reacting to a front-end event is handy to skip the default render of the current component, to achieve this do:
458
+
459
+ ```python
460
+ from djhtmx.component import HtmxComponent, Redirect
461
+
462
+
463
+ class Component(HtmxComponent):
464
+ ...
465
+
466
+ def do_something(self):
467
+ ...
468
+ yield SkipRender(self)
469
+ ```
470
+
471
+ ### Partial Rendering
472
+
473
+ Sometimes you don't want to do a full component render, but a partial one. Specially if the user if typing somewhere to filter items and you don't wanna interfere with the user typing or focus. Here is the technique to do that:
474
+
475
+ ```python
476
+ from djhtmx.component import HtmxComponent, Render
477
+
478
+ class SmartFilter(HtmxComponent):
479
+ _template_name = "SmartFilter.html"
480
+ query: str = ""
481
+
482
+ @property
483
+ def items(self):
484
+ items = Item.objects.all()
485
+ if self.query:
486
+ items = items.filter(name__icontains=self.query)
487
+ return items
488
+
489
+ def filter(self, query: str):
490
+ self.query = query.trim()
491
+ yield Render(self, template="SmartFilter_list.html")
492
+ ```
493
+
494
+ `SmartFilter.html`:
495
+
496
+ ```html
497
+ {% load htmx %}
498
+
499
+ <div {% hx-tag %}>
500
+ <input type="text" name="query" value="{{ query }}">
501
+ {% include "SmartFilter_list.html" %}
502
+ </div>
503
+ ```
504
+
505
+ `SmartFilter_list.html`:
506
+
507
+ ```html
508
+ <ul {% oob "list" %}>
509
+ {% for item in items %}
510
+ <li><a href="{{ item.get_absolute_url }}">{{ item }}</a></li>
511
+ {% empty %}
512
+ <li>Nothing found!</li>
513
+ {% endfor %}
514
+ </ul>
515
+ ```
516
+
517
+ - Split the component in multiple templates, the main one and the partial ones.
518
+ - For readability prefix the name of the partials with the name of the parent.
519
+ - The partials need a single root HTML Element with an id and the `{% oob %}` tag next to it.
520
+ - When you wanna do the partial render you have to `yield Render(self, template=...)` with the name of the partial template, this will automatically skip the default full render and render the component with that partial template.
521
+
522
+ ### Rendering with Custom Context
523
+
524
+ Sometimes you need to render a component with custom context data that differs from the component's state. The `Render` command supports an optional `context` parameter that allows you to override the component's context:
525
+
526
+ ```python
527
+ from djhtmx.component import HtmxComponent, Render
528
+
529
+ class DataVisualization(HtmxComponent):
530
+ _template_name = "DataVisualization.html"
531
+
532
+ def show_filtered_data(self, filter_type: str):
533
+ # Get some custom data that's not part of component state
534
+ custom_data = self.get_filtered_data(filter_type)
535
+
536
+ # Render with custom context
537
+ yield Render(
538
+ self,
539
+ template="DataVisualization_filtered.html",
540
+ context={
541
+ "filtered_data": custom_data,
542
+ "filter_applied": filter_type,
543
+ "timestamp": datetime.now()
544
+ }
545
+ )
546
+ ```
547
+
548
+ When using custom context:
549
+ - The provided context overrides the component's default context
550
+ - Essential HTMX variables (`htmx_repo`, `hx_oob`, `this`) are preserved
551
+ - The component's state remains unchanged - only the rendering context is modified
552
+ - This is particularly useful for displaying computed data, temporary states, or external data that shouldn't be part of the component's persistent state
553
+
554
+ ## Query Parameters & State
555
+
556
+ Coming back to the previous example let's say that we want to persist the state of the `query` in the URL, so in case the user refreshes the page or shares the link the state of the component is partially restored. For do the following:
557
+
558
+ ```python
559
+ from typing import Annotated
560
+ from djhtmx.component import HtmxComponent
561
+ from djhtmx.query import Query
562
+
563
+
564
+ class SmartFilter(HtmxComponent):
565
+ ...
566
+ query: Annotated[str, Query("query")] = ""
567
+ ...
568
+ ```
569
+
570
+ Annotating with Query causes that if the state of the query is not explicitly passed to the component during instantiation it is taken from the query string of the current URL.
571
+
572
+ There can be multiple components subscribed to the same query parameter or to individual ones.
573
+
574
+ If you want now you can split this component in two, each with their own template:
575
+
576
+
577
+ ```python
578
+ from typing import Annotated
579
+ from djhtmx.component import HtmxComponent, SkipRender
580
+ from djhtmx.query import Query
581
+
582
+ class SmartFilter(HtmxComponent):
583
+ _template_name = "SmartFilter.html"
584
+ query: Annotated[str, Query("query")] = ""
585
+
586
+ def filter(self, query: str):
587
+ self.query = query.trim()
588
+ yield SkipRender(self)
589
+
590
+ class SmartList(HtmxComponent):
591
+ _template_name = "SmartList.html"
592
+ query: Annotated[str, Query("query")] = ""
593
+
594
+ @property
595
+ def items(self):
596
+ items = Item.objects.all()
597
+ if self.query:
598
+ items = items.filter(name__icontains=self.query)
599
+ return items
600
+ ```
601
+
602
+ Instantiate next to each other:
603
+
604
+
605
+ ```html
606
+ <div>
607
+ ...
608
+ {% htmx "SmartFilter" %}
609
+ {% htmx "SmartList" %}
610
+ ...
611
+ </div>
612
+ ```
613
+
614
+ When the filter mutates the `query`, the URL is updated and the `SmartList` is awaken because the both point to the same query parameter, and will be re-rendered.
615
+
616
+ ## Signals
617
+
618
+ Sometimes you modify a model and you want not just the current component to react to this, but also trigger re-renders of other components that are not directly related to the current one. For this signals are very convenient. These are strings that represent topics you can subscribe a component to and make sure it is rendered in case any of the topics it subscribed to is triggered.
619
+
620
+ Signal formats:
621
+ - `app_label.modelname`: Some mutation happened to a model instance of this kind
622
+ - `app_label.modelname.instance_pk`: Some mutation happened to this precise instance of model
623
+ - `app_label.modelname.instance_pk.created`: This instance was created
624
+ - `app_label.modelname.instance_pk.updated`: This instance was updated
625
+ - `app_label.modelname.instance_pk.deleted`: This instance was deleted
626
+
627
+ When an instance is modified the mode specific and not so specific signals are triggered.
628
+ Together with them some other signals to related models are triggered.
629
+
630
+ Example: if we have a Todo list app with the models:
631
+
632
+
633
+ ```python
634
+ class TodoList(Model):
635
+ ...
636
+
637
+ class Item(Model):
638
+ todo_list = ForeignKey(TodoList, related_name="items")
639
+ ```
640
+
641
+ And from the list with id `932` you take a item with id `123` and update it all this signals will be triggered:
642
+
643
+ - `todoapp.item`
644
+ - `todoapp.item.123`
645
+ - `todoapp.item.123.updated`
646
+ - `todoapp.todolist.932.items`
647
+ - `todoapp.todolist.932.items.updated`
648
+
649
+
650
+ ### How to subscribe to signals
651
+
652
+ Let's say you wanna count how many items there are in certain Todo list, but your component does not receive an update when the list is updated because it is out of it. You can do this.
653
+
654
+ ```python
655
+ from djhtmx.component import HtmxComponent
656
+
657
+ class ItemCounter(HtmxComponent):
658
+ todo_list: TodoList
659
+
660
+ def subscriptions(self):
661
+ return {
662
+ f"todoapp.todolist.{self.todo_list.id}.items.deleted",
663
+ f"todoapp.todolist.{self.todo_list.id}.items.created",
664
+ }
665
+
666
+ def count(self):
667
+ return self.todo_list.items.count()
668
+ ```
669
+
670
+ This will make this component re-render every time an item is added or removed from the list `todo_list`.
671
+
672
+ ## Dispatching Events between components
673
+
674
+ Sometimes is handy to notify components in the same session that something changed and they need to perform the corresponding update and `Query()` nor Signals are very convenient for this. In this case you can `Emit` events and listen to them.
675
+
676
+ Find here an implementation of `SmartFilter` and `SmartItem` using this mechanism:
677
+
678
+ ```python
679
+ from dataclasses import dataclass
680
+ from djhtmx.component import HtmxComponent, SkipRender, Emit
681
+
682
+
683
+ @dataclass(slots=True)
684
+ class QueryChanged:
685
+ query: str
686
+
687
+
688
+ class SmartFilter(HtmxComponent):
689
+ _template_name = "SmartFilter.html"
690
+ query: str = ""
691
+
692
+ def filter(self, query: str):
693
+ self.query = query.trim()
694
+ yield Emit(QueryChanged(query))
695
+ yield SkipRender(self)
696
+
697
+ class SmartList(HtmxComponent):
698
+ _template_name = "SmartList.html"
699
+ query: str = ""
700
+
701
+ def _handle_event(self, event: QueryChanged):
702
+ self.query = event.query
703
+
704
+ @property
705
+ def items(self):
706
+ items = Item.objects.all()
707
+ if self.query:
708
+ items = items.filter(name__icontains=self.query)
709
+ return items
710
+ ```
711
+
712
+ The library will look in all components if they define `_handle_event(event: ...)` and based on the annotation of `event` subscribe them to those events. This annotation can be a single type or a `Union` with multiple even types.
713
+
714
+ ## Inserting a component somewhere
715
+
716
+ Let's say that we are making the TODO list app and we want that when a new item is added to the list there is not a full re-render of the whole list, just that the Component handling a single Item is added to the list.
717
+
718
+ ```python
719
+ from djhtmx.component import HtmxComponent, SkipRender, BuildAndRender
720
+
721
+ class TodoListComponent(HtmxComponent):
722
+ _template_name = "TodoListComponent.html"
723
+ todo_list: TodoList
724
+
725
+ def create(self, name: str):
726
+ item = self.todo_list.items.create(name=name)
727
+ yield BuildAndRender.prepend(
728
+ f"#{self.id} .list",
729
+ ItemComponent,
730
+ id=f"item-{item.id}",
731
+ item=item,
732
+ )
733
+ yield SkipRender(self)
734
+
735
+ class ItemComponent(HtmxComponent):
736
+ ...
737
+ item: Item
738
+ ...
739
+ ```
740
+
741
+ `TodoListComponent.html`:
742
+
743
+ ```html
744
+ {% load htmx %}
745
+ <div {% hx-tag %}>
746
+ <form {% on "submit" "create" %}>
747
+ <input type="text" name="name">
748
+ </form>
749
+ <ul class="list">
750
+ {% for item in items %}
751
+ {% htmx "ItemComponent" id="item-"|add:item.id item=item %}
752
+ {% endfor %}
753
+ </ul>
754
+ </div>
755
+ ```
756
+
757
+ Use the `BuildAndRender.<helper>(target: str, ...)` to send a component to be inserted somewhere or updated.
758
+
759
+ ### Cascade Deletion
760
+
761
+ You can establish parent-child relationships so that when a parent component is destroyed, all its children are automatically destroyed recursively. This prevents memory leaks in complex component hierarchies.
762
+
763
+ #### Using BuildAndRender with parent_id
764
+
765
+ ```python
766
+ class TodoListComponent(HtmxComponent):
767
+ def create(self, name: str):
768
+ item = self.todo_list.items.create(name=name)
769
+ # Child component that will be automatically destroyed when parent is destroyed
770
+ yield BuildAndRender.append(
771
+ "#todo-items",
772
+ ItemComponent,
773
+ parent_id=self.id, # Establishes parent-child relationship
774
+ id=f"item-{item.id}",
775
+ item=item
776
+ )
777
+
778
+ class Dashboard(HtmxComponent):
779
+ def show_modal(self):
780
+ # Modal becomes child of dashboard - destroyed when dashboard is destroyed
781
+ yield BuildAndRender.prepend("body", SettingsModal, parent_id=self.id)
782
+ ```
783
+
784
+ #### Template Tag Automatic Tracking
785
+
786
+ When you use `{% htmx "ComponentName" %}` inside another component's template, parent-child relationships are automatically established:
787
+
788
+ ```html
789
+ <!-- In TodoList.html template -->
790
+ {% load htmx %}
791
+ <div {% hx-tag %}>
792
+ {% for item in items %}
793
+ <!-- Each TodoItem automatically becomes a child of this TodoList -->
794
+ {% htmx "ItemComponent" id="item-"|add:item.id item=item %}
795
+ {% endfor %}
796
+ </div>
797
+ ```
798
+
799
+ When the parent TodoList is destroyed, all child ItemComponent instances are automatically cleaned up.
800
+
801
+ #### Updating Components
802
+
803
+ Use `BuildAndRender.update()` to update existing components (preserves existing parent-child relationships):
804
+
805
+ ```python
806
+ # Update existing component without changing relationships
807
+ yield BuildAndRender.update(SidebarWidget, data=sidebar_data)
808
+ ```
809
+
810
+
811
+ ## Focusing an item after render
812
+
813
+ Let's say we want to put the focus in an input that inside the new ItemComponent rendered, for this use `yield Focus(target)`
814
+
815
+ ```python
816
+ from djhtmx.component import HtmxComponent, SkipRender, BuildAndRender, Focus
817
+
818
+ class TodoListComponent(HtmxComponent):
819
+ _template_name = "TodoListComponent.html"
820
+ todo_list: TodoList
821
+
822
+ def create(self, name: str):
823
+ item = self.todo_list.items.create(name=name)
824
+ item_id = f"item-{item.id}"
825
+ yield BuildAndRender.prepend(
826
+ f"{self.id} .list",
827
+ ItemComponent,
828
+ id=item_id,
829
+ item=item,
830
+ )
831
+ yield Focus(f"#{item_id} input")
832
+ yield SkipRender(self)
833
+ ```
834
+
835
+ ## Sending Events to the DOM
836
+
837
+ Suppose you have a rich JavaScript library (graphs, maps, or anything...) in the front-end and you want to communicate something to it because it is subscribed to some dome event. For that you can use `yield DispatchDOMEvent(target, event, detail, ....)`
838
+
839
+
840
+ ```python
841
+ from djhtmx.component import HtmxComponent, DispatchDOMEvent
842
+
843
+ class TodoListComponent(HtmxComponent):
844
+ _template_name = "TodoListComponent.html"
845
+ todo_list: TodoList
846
+
847
+ def create(self, name: str):
848
+ item = self.todo_list.items.create(name=name)
849
+ yield DispatchDOMEvent(
850
+ "#leaflet-map",
851
+ "new-item",
852
+ {"id": item.id, "name": item.name, "geojson": item.geojson}
853
+ )
854
+ ```
855
+
856
+ This will trigger that event in the front-end when the request arrives allowing rich JavaScript components to react accordingly without full re-render.
857
+
858
+ ## Template Tags you should know about
859
+
860
+ - `{% htmx-headers %}`: put it inside your `<header></header>` to load the right scripts and configuration.
861
+
862
+ ```html
863
+ <header>
864
+ {% htmx-headers %}
865
+ </header>
866
+ ```
867
+
868
+ - `{% htmx <ComponentName: str> **kwargs %}`: instantiates and inserts the result of rendering that component with those initialization parameters.
869
+
870
+ ```html
871
+ <div>
872
+ {% htmx 'Button' document=document name='Save Document' is_primary=True %}
873
+ </div>
874
+ ```
875
+
876
+
877
+ - `{% hx-tag %}`: goes in the root HTML Element of a component template, it sets the component `id` and some other basic configuration details of the component.
878
+
879
+ ```html
880
+ <button {% hx-tag %}>
881
+ ...
882
+ </button>
883
+ ```
884
+
885
+
886
+ - `{% oob <LocalId: str> %}`: goes in the root HTML Element of an element that will used for partial render (swapped Out Of Band). It sets the id of the element to a concatenation of the current component id and whatever you pass to it, and sets the right [hx-swap-oob](https://htmx.org/attributes/hx-swap-oob/) strategy.
887
+
888
+ ```html
889
+ <div {% oob "dropdown" %} class="dropdown">
890
+ ...
891
+ </div>
892
+ ```
893
+
894
+ - `{% on <EventName: str> <EventHandler: str> **kwargs %}` binds the event handler using [hx-trigger](https://htmx.org/attributes/hx-trigger/) to an event handler in the component with certain explicit parameters. Implicit parameters are passed from anything that has a name attribute defined inside the component.
895
+
896
+ ```html
897
+ <button {% on "click" "save" %}>
898
+ ...
899
+ </button>
900
+ ```
901
+
902
+ - `{% class <ClassName: str>: <BooleanExpr: bool>[, ...] %}` used inside of any html tag to set the class attribute, activating certain classes when corresponding boolean expression is `True`.
903
+
904
+
905
+ ```html
906
+ <button {% class "btn": True, "btn-primary": is_primary %}>
907
+ ...
908
+ </button>
909
+ ```
910
+
911
+ ## Testing
912
+
913
+ This library provides the class `djhtmx.testing.Htmx` which implements a very basic a dumb runtime for testing components. How to use:
914
+
915
+
916
+ ```python
917
+ from django.test import Client, TestCase
918
+ from djhtmx.testing import Htmx
919
+
920
+ from .models import Item
921
+
922
+ class TestNormalRendering(TestCase):
923
+ def setUp(self):
924
+ Item.objects.create(text="First task")
925
+ Item.objects.create(text="Second task")
926
+ self.htmx = Htmx(Client())
927
+
928
+ def test_todo_app(self):
929
+ self.htmx.navigate_to("/todo")
930
+
931
+ [a, b] = self.htmx.select('[hx-name="TodoItem"] label')
932
+ self.assertEqual(a.text_content(), "First task")
933
+ self.assertEqual(b.text_content(), "Second task")
934
+
935
+ [count] = self.htmx.select(".todo-count")
936
+ self.assertEqual(count.text_content(), "2 items left")
937
+
938
+ # Add new item
939
+ self.htmx.type("input.new-todo", "3rd task")
940
+ self.htmx.trigger("input.new-todo")
941
+
942
+ [count] = self.htmx.select(".todo-count")
943
+ self.assertEqual(count.text_content(), "3 items left")
944
+
945
+ [a, b, c] = self.htmx.select('[hx-name="TodoItem"] label')
946
+ self.assertEqual(a.text_content(), "First task")
947
+ self.assertEqual(b.text_content(), "Second task")
948
+ self.assertEqual(c.text_content(), "3rd task")
949
+ ```
950
+
951
+ ### API
952
+
953
+ `Htmx(client: Client)`: pass a Django test client, that can be authenticated if that's required.
954
+
955
+ `htmx.navigate_to(url, *args, **kwargs)`: This is used to navigate to some url. It is a wrapper of `Client.get` that will retrieve the page and parse the HTML into `htmx.dom: lxml.html.HtmlElement` and create a component repository in `htmx.repo`.
956
+
957
+ #### Look-ups
958
+
959
+ `htmx.select(css_selector: str) -> list[lxml.html.HtmlElement]`: Pass some CSS selector here to retrieve nodes from the DOM, so you can modify them or perform assertions over them.
960
+
961
+ `htmx.find_by_text(text: str) -> lxml.html.HtmlElement`: Returns the first element that contains certain text.
962
+
963
+ `htmx.get_component_by_type(component_type: type[THtmxComponent]) -> THtmxComponent`: Retrieves the only instance rendered of that component type in the current page. If there is more than one instance this fails.
964
+
965
+ `htmx.get_components_by_type(component_type: type[THtmxComponent]) -> list[THtmxComponent]`: Retrieves all instances of this component type in the current page.
966
+
967
+ `htmx.get_component_by_id(component_id: str) -> THtmxComponent`: Retrieves a component by its id from the current page.
968
+
969
+ #### Interactions
970
+
971
+ `htmx.type(selector: str | html.HtmlElement, text: str, clear=False)`: This simulates typing in an input or text area. If `clear=True` it clears it replaces the current text in it.
972
+
973
+ `htmx.trigger(selector: str | html.HtmlElement)`: This triggers whatever event is bound in the selected element and returns after all side effects had been processed.
974
+
975
+ ```python
976
+ self.htmx.type("input.new-todo", "3rd task")
977
+ self.htmx.trigger("input.new-todo")
978
+ ```
979
+
980
+ `htmx.send(method: Callable[P, Any], *args: P.args, **kwargs: P.kwargs)`: This sends the runtime to execute that a bound method of a HtmxComponent and returns after all side effects had been processed. Use as in:
981
+
982
+ ```python
983
+ todo_list = htmx.get_component_by_type(TodoList)
984
+ htmx.send(todo_list.new_item, text="New todo item")
985
+ ```
986
+
987
+ `htmx.dispatch_event(self, component_id: str, event_handler: str, kwargs: dict[str, Any])`: Similar to `htmx.send`, but you don't need the instance, you just need to know its id.
988
+
989
+ ```python
990
+ htmx.dispatch_event("#todo-list", "new_item", {"text": "New todo item"})
991
+ ```