plain.htmx 0.0.0__tar.gz

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.
Files changed (47) hide show
  1. plain_htmx-0.0.0/LICENSE +28 -0
  2. plain_htmx-0.0.0/PKG-INFO +430 -0
  3. plain_htmx-0.0.0/README.md +410 -0
  4. plain_htmx-0.0.0/plain/htmx/README.md +408 -0
  5. plain_htmx-0.0.0/plain/htmx/__init__.py +0 -0
  6. plain_htmx-0.0.0/plain/htmx/assets/htmx/htmx.js +3905 -0
  7. plain_htmx-0.0.0/plain/htmx/assets/htmx/htmx.min.js +1 -0
  8. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph-ext.js +872 -0
  9. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph-ext.min.js +1 -0
  10. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph-htmx.js +24 -0
  11. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph.amd.js +852 -0
  12. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph.cjs.js +850 -0
  13. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph.esm.js +850 -0
  14. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph.js +848 -0
  15. plain_htmx-0.0.0/plain/htmx/assets/htmx/idiomorph/idiomorph.min.js +1 -0
  16. plain_htmx-0.0.0/plain/htmx/assets/htmx/plainhtmx.js +56 -0
  17. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/ajax-header.js +7 -0
  18. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/alpine-morph.js +16 -0
  19. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/class-tools.js +92 -0
  20. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/client-side-templates.js +96 -0
  21. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/debug.js +11 -0
  22. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/disable-element.js +18 -0
  23. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/event-header.js +37 -0
  24. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/head-support.js +141 -0
  25. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/include-vals.js +24 -0
  26. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/json-enc.js +12 -0
  27. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/loading-states.js +183 -0
  28. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/method-override.js +11 -0
  29. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/morphdom-swap.js +17 -0
  30. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/multi-swap.js +45 -0
  31. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/path-deps.js +60 -0
  32. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/path-params.js +11 -0
  33. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/preload.js +147 -0
  34. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/rails-method.js +10 -0
  35. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/remove-me.js +27 -0
  36. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/response-targets.js +130 -0
  37. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/restored.js +15 -0
  38. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/sse.js +355 -0
  39. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/ext/ws.js +477 -0
  40. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/htmx.d.ts +450 -0
  41. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/htmx.js +3905 -0
  42. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/htmx.min.js +1 -0
  43. plain_htmx-0.0.0/plain/htmx/assets/htmx/src/htmx.min.js.gz +0 -0
  44. plain_htmx-0.0.0/plain/htmx/jinja.py +157 -0
  45. plain_htmx-0.0.0/plain/htmx/templates/htmx/js.html +18 -0
  46. plain_htmx-0.0.0/plain/htmx/views.py +74 -0
  47. plain_htmx-0.0.0/pyproject.toml +27 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2023, Dropseed, LLC
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,430 @@
1
+ Metadata-Version: 2.1
2
+ Name: plain.htmx
3
+ Version: 0.0.0
4
+ Summary: HTMX integration for Plain
5
+ Home-page: https://github.com/dropseed/plain
6
+ License: MIT
7
+ Author: Dave Gaeddert
8
+ Author-email: dave.gaeddert@dropseed.dev
9
+ Requires-Python: >=3.8,<4.0
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Project-URL: Repository, https://github.com/dropseed/plain
18
+ Description-Content-Type: text/markdown
19
+
20
+ <!-- This file is compiled from plain-htmx/plain/htmx/README.md. Do not edit this file directly. -->
21
+
22
+ # HTMX
23
+
24
+ Integrate HTMX with templates and views.
25
+
26
+ The `plain-htmx` package adds a couple of unique features for working with HTMX.
27
+ One is [template fragments](#template-fragments) and the other is [view actions](#view-actions).
28
+
29
+ The combination of these features lets you build HTMX-powered views that focus on server-side rendering and avoid overly complicated URL structures or REST APIs that you may not otherwise need.
30
+
31
+ The `HTMXViewMixin` is the starting point for the server-side HTMX behavior.
32
+ To use these feaures on a view,
33
+ simply inherit from the class (yes, this is designed to work with class-based views).
34
+
35
+ ```python
36
+ from plain.views import TemplateView
37
+
38
+ from plain.htmx.views import HTMXViewMixin
39
+
40
+
41
+ class HomeView(HTMXViewMixin, TemplateView):
42
+ template_name = "home.html"
43
+ ```
44
+
45
+ In your `base.html` template (or wherever need the HTMX scripts),
46
+ you can use the `{% htmx_js %}` template tag:
47
+
48
+ ```html
49
+ <!-- base.template.html -->
50
+ {% load htmx %}
51
+ <!DOCTYPE html>
52
+ <html lang="en">
53
+ <head>
54
+ <meta charset="UTF-8">
55
+ <title>My Site</title>
56
+ {% htmx_js %}
57
+ </head>
58
+ <body>
59
+ {% block content %}{% endblock %}
60
+ </body>
61
+ ```
62
+
63
+ ## Installation
64
+
65
+ ```python
66
+ INSTALLED_PACKAGES = [
67
+ # ...
68
+ "plain.htmx",
69
+ ]
70
+ ```
71
+
72
+ ## Template Fragments
73
+
74
+ An `{% htmxfragment %}` can be used to render a specific part of your template in HTMX responses.
75
+ When you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL,
76
+ render *only* the updated content for the fragment,
77
+ and swap out the fragment.
78
+
79
+ Here's an example:
80
+
81
+ ```html
82
+ <!-- home.html -->
83
+ {% extends "base.html" %}
84
+
85
+ {% load htmx %}
86
+
87
+ {% block content %}
88
+ <header>
89
+ <h1>Page title</h1>
90
+ </header>
91
+
92
+ <main>
93
+ {% htmxfragment main %}
94
+ <p>The time is {% now "jS F Y H:i" %}</p>
95
+
96
+ <button hx-get>Refresh</button>
97
+ {% endhtmxfragment %}
98
+ </main>
99
+ {% endblock %}
100
+ ```
101
+
102
+ Everything inside `{% htmxfragment %}` will automatically update when "Refresh" is clicked.
103
+
104
+ ### Lazy template fragments
105
+
106
+ If you want to render a fragment lazily,
107
+ you can add the `lazy` attribute to the `{% htmxfragment %}` tag.
108
+
109
+ ```html
110
+ {% htmxfragment main lazy=True %}
111
+ <!-- This content will be fetched with hx-get -->
112
+ {% endhtmxfragment %}
113
+ ```
114
+
115
+ This pairs nicely with passing a callable function or method as a context variable,
116
+ which will only get invoked when the fragment actually gets rendered on the lazy load.
117
+
118
+ ```python
119
+ def fetch_items():
120
+ import time
121
+ time.sleep(2)
122
+ return ["foo", "bar", "baz"]
123
+
124
+
125
+ class HomeView(HTMXViewMixin, TemplateView):
126
+ def get_context(self, **kwargs):
127
+ context = super().get_context(**kwargs)
128
+ context["items"] = fetch_items # Missing () are on purpose!
129
+ return context
130
+ ```
131
+
132
+ ```html
133
+ {% htmxfragment main lazy=True %}
134
+ <ul>
135
+ {% for item in items %}
136
+ <li>{{ item }}</li>
137
+ {% endfor %}
138
+ </ul>
139
+ {% endhtmxfragment %}
140
+ ```
141
+
142
+ ### How does it work?
143
+
144
+ When you use the `{% htmxfragment %}` tag,
145
+ a standard `div` is output that looks like this:
146
+
147
+ ```html
148
+ <div plain-hx-fragment="main" hx-swap="outerHTML" hx-target="this" hx-indicator="this">
149
+ {{ fragment_content }}
150
+ </div>
151
+ ```
152
+
153
+ The `plain-hx-fragment` is a custom attribute that we've added ("F" is for "Forge"),
154
+ but the rest are standard HTMX attributes.
155
+
156
+ When Plain renders the response to an HTMX request,
157
+ it will get the `Plain-HX-Fragment` header,
158
+ find the fragment with that name in the template,
159
+ and render that for the response.
160
+
161
+ Then the response content is automatically swapped in to replace the content of your `{% htmxfragment %}` tag.
162
+
163
+ Note that there is no URL specified on the `hx-get` attribute.
164
+ By default, HTMX will send the request to the current URL for the page.
165
+ When you're working with fragments, this is typically the behavior you want!
166
+ (You're on a page and want to selectively re-render a part of that page.)
167
+
168
+ The `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag --
169
+ the fragments on a page should be named and unique,
170
+ and you can't use it inside of loops.
171
+ For fragment-like behavior inside of a for-loop,
172
+ you'll most likely want to set up a dedicated URL that can handle a single instance of the looped items,
173
+ and maybe leverage [dedicated templates](#dedicated-templates).
174
+
175
+ ## View Actions
176
+
177
+ View actions let you define multiple "actions" on a class-based view.
178
+ This is an alternative to defining specific API endpoints or form views to handle basic button interactions.
179
+
180
+ With view actions you can design a single view that renders a single template,
181
+ and associate buttons in that template with class methods in the view.
182
+
183
+ As an example, let's say we have a `PullRequest` model and we want users to be able to open, close, or merge it with a button.
184
+
185
+ In our template, we would use the `plain-hx-action` attribute to name the action:
186
+
187
+ ```html
188
+ {% extends "base.html" %}
189
+
190
+ {% load htmx %}
191
+
192
+ {% block content %}
193
+ <header>
194
+ <h1>{{ pullrequest }}</h1>
195
+ </header>
196
+
197
+ <main>
198
+ {% htmxfragment pullrequest %}
199
+ <p>State: {{ pullrequest.state }}</p>
200
+
201
+ {% if pullrequest.state == "open" %}
202
+ <!-- If it's open, they can close or merge it -->
203
+ <button hx-post plain-hx-action="close">Close</button>
204
+ <button hx-post plain-hx-action="merge">Merge</button>
205
+ {% else if pullrequest.state == "closed" %}
206
+ <!-- If it's closed, it can be re-opened -->
207
+ <button hx-post plain-hx-action="open">Open</button>
208
+ {% endif %}
209
+
210
+ {% endhtmxfragment %}
211
+ </main>
212
+ {% endblock %}
213
+ ```
214
+
215
+ Then in the view class, we can define methods for each HTTP method + `plain-hx-action`:
216
+
217
+ ```python
218
+ class PullRequestDetailView(HTMXViewMixin, DetailView):
219
+ def get_queryset(self):
220
+ # The queryset will apply to all actions on the view, so "permission" logic can be shared
221
+ return super().get_queryset().filter(users=self.request.user)
222
+
223
+ # Action handling methods follow this format:
224
+ # htmx_{method}_{action}
225
+ def htmx_post_open(self):
226
+ self.object = self.get_object()
227
+
228
+ if self.object.state != "closed":
229
+ raise ValueError("Only a closed pull request can be opened")
230
+
231
+ self.object.state = "closed"
232
+ self.object.save()
233
+
234
+ # Render the updated content the standard calls
235
+ # (which will selectively render our fragment if applicable)
236
+ context = self.get_context(object=self.object)
237
+ return self.render_to_response(context)
238
+
239
+ def htmx_post_close(self):
240
+ self.object = self.get_object()
241
+
242
+ if self.object.state != "open":
243
+ raise ValueError("Only a open pull request can be closed")
244
+
245
+ self.object.state = "open"
246
+ self.object.save()
247
+
248
+ context = self.get_context(object=self.object)
249
+ return self.render_to_response(context)
250
+
251
+ def htmx_post_merge(self):
252
+ self.object = self.get_object()
253
+
254
+ if self.object.state != "open":
255
+ raise ValueError("Only a open pull request can be merged")
256
+
257
+ self.object.state = "merged"
258
+ self.object.save()
259
+
260
+ context = self.get_context(object=self.object)
261
+ return self.render_to_response(context)
262
+ ```
263
+
264
+ This can be a matter of preference,
265
+ but typically you may end up building out an entire form, API, or set of URLs to handle these behaviors.
266
+ If you application is only going to handle these actions via HTMX,
267
+ then a single View may be a simpler way to do it.
268
+
269
+ Note that currently we don't have many helper-functions for parsing or returning HTMX responses --
270
+ this can basically all be done through standard request and response headers:
271
+
272
+ ```python
273
+ class PullRequestDetailView(HTMXViewMixin, DetailView):
274
+ def get_queryset(self):
275
+ # The queryset will apply to all actions on the view, so "permission" logic can be shared
276
+ return super().get_queryset().filter(users=self.request.user)
277
+
278
+ # You can also leave off the "plain-hx-action" attribute and just handle the HTTP method
279
+ def htmx_delete(self):
280
+ self.object = self.get_object()
281
+
282
+ self.object.delete()
283
+
284
+ # Tell HTMX to do a client-side redirect when it receives the response
285
+ response = HttpResponse(status=204)
286
+ response["HX-Redirect"] = "/"
287
+ return response
288
+ ```
289
+
290
+ ## Dedicated Templates
291
+
292
+ A small additional features of `plain-htmx` is that it will automatically find templates named `{template_name}_htmx.html` for HTMX requests.
293
+ More than anything, this is just a nice way to formalize a naming scheme for template "partials" dedicated to HTMX.
294
+
295
+ Because template fragments don't work inside of loops,
296
+ for example,
297
+ you'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop.
298
+ You can sometimes think of these as "pages within a page".
299
+
300
+ So if you have a template that renders a collection of items,
301
+ you can do the initial render using a Django `{% include %}`:
302
+
303
+ ```html
304
+ <!-- pullrequests/pullrequest_list.html -->
305
+ {% extends "base.html" %}
306
+
307
+ {% block content %}
308
+
309
+ {% for pullrequest in pullrequests %}
310
+ <div>
311
+ {% include "pullrequests/pullrequest_detail_htmx.html" %}
312
+ </div>
313
+ {% endfor %}
314
+
315
+ {% endblock %}
316
+ ```
317
+
318
+ And then subsequent HTMX requests/actions on individual items can be handled by a separate URL/View:
319
+
320
+ ```html
321
+ <!-- pullrequests/pullrequest_detail_htmx.html -->
322
+ <div hx-url="{% url 'pullrequests:detail' pullrequest.uuid %}" hx-swap="outerHTML" hx-target="this">
323
+ <!-- Send all HTMX requests to a URL for single pull requests (works inside of a loop, or on a single detail page) -->
324
+ <h2>{{ pullrequest.title }}</h2>
325
+ <button hx-get>Refresh</button>
326
+ <button hx-post plain-hx-action="update">Update</button>
327
+ </div>
328
+ ```
329
+
330
+ *If* you need a URL to render an individual item, you can simply include the same template fragment in most cases:
331
+
332
+ ```html
333
+ <!-- pullrequests/pullrequest_detail.html -->
334
+ {% extends "base.html" %}
335
+
336
+ {% block content %}
337
+
338
+ {% include "pullrequests/pullrequest_detail_htmx.html" %}
339
+
340
+ {% endblock %}
341
+ ```
342
+
343
+ ```python
344
+ # urls.py and views.py
345
+ # urls.py
346
+ default_namespace = "pullrequests"
347
+
348
+ urlpatterns = [
349
+ path("<uuid:uuid>/", views.PullRequestDetailView, name="detail"),
350
+ ]
351
+
352
+ # views.py
353
+ class PullRequestDetailView(HTMXViewMixin, DetailView):
354
+ def htmx_post_update(self):
355
+ self.object = self.get_object()
356
+
357
+ self.object.update()
358
+
359
+ context = self.get_context(object=self.object)
360
+ return self.render_to_response(context)
361
+ ```
362
+
363
+ ## Tailwind CSS variant
364
+
365
+ The standard behavior for `{% htmxfragment %}` is to set `hx-indicator="this"` on the rendered element.
366
+ This tells HTMX to add the `htmx-request` class to the fragment element when it is loading.
367
+
368
+ Since Plain emphasizes using Tailwind CSS,
369
+ here's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:
370
+
371
+ ```js
372
+ const plugin = require('tailwindcss/plugin')
373
+
374
+ module.exports = {
375
+ plugins: [
376
+ // Add variants for htmx-request class for loading states
377
+ plugin(({addVariant}) => addVariant('htmx-request', ['&.htmx-request', '.htmx-request &']))
378
+ ],
379
+ }
380
+ ```
381
+
382
+ You can then prefix any class with `htmx-request:` to decide what it looks like while HTMX requests are being sent:
383
+
384
+ ```html
385
+ <!-- The "htmx-request" class will be added to the <form> by default -->
386
+ <form hx-post="{{ url }}">
387
+ <!-- Showing an element -->
388
+ <div class="hidden htmx-request:block">
389
+ Loading
390
+ </div>
391
+
392
+ <!-- Changing a button's class -->
393
+ <button class="text-white bg-black htmx-request:opacity-50 htmx-request:cursor-wait" type="submit">Submit</button>
394
+ </form>
395
+ ```
396
+
397
+ ## CSRF tokens
398
+
399
+ We configure CSRF tokens for you with the HTMX JS API.
400
+ You don't have to put `hx-headers` on the `<body>` tag, for example.
401
+
402
+ ## Error classes
403
+
404
+ This app also includes an HTMX extension for adding error classes for failed requests.
405
+
406
+ - `htmx-error-response` for `htmx:responseError`
407
+ - `htmx-error-response-{{ status_code }}` for `htmx:responseError`
408
+ - `htmx-error-send` for `htmx:sendError`
409
+
410
+ To enable them, use `hx-ext="error-classes"`.
411
+
412
+ You can add the ones you want as Tailwind variants and use them to show error messages.
413
+
414
+ ```js
415
+ const plugin = require('tailwindcss/plugin')
416
+
417
+ module.exports = {
418
+ plugins: [
419
+ // Add variants for htmx-request class for loading states
420
+ plugin(({addVariant}) => addVariant('htmx-error-response-429', ['&.htmx-error-response-429', '.htmx-error-response-429 &']))
421
+ ],
422
+ }
423
+ ```
424
+
425
+ ## CSP
426
+
427
+ ```
428
+ <meta name="htmx-config" content='{"includeIndicatorStyles":false}'>
429
+ ```
430
+