{"id":18814279,"url":"https://github.com/forgepackages/forge-htmx","last_synced_at":"2025-08-20T22:31:59.881Z","repository":{"id":98899170,"uuid":"538772715","full_name":"forgepackages/forge-htmx","owner":"forgepackages","description":"An HTMX integration for Django with template fragments and view actions","archived":false,"fork":false,"pushed_at":"2024-11-01T00:27:24.000Z","size":191,"stargazers_count":26,"open_issues_count":5,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2024-11-01T01:21:10.974Z","etag":null,"topics":["django","forgepackages","htmx","python"],"latest_commit_sha":null,"homepage":"https://www.forgepackages.com/docs/forge-htms","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/forgepackages.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-09-20T02:09:14.000Z","updated_at":"2024-09-30T17:38:37.000Z","dependencies_parsed_at":null,"dependency_job_id":"f77bc970-81d1-43c3-aa07-7fc6082e8e48","html_url":"https://github.com/forgepackages/forge-htmx","commit_stats":{"total_commits":25,"total_committers":3,"mean_commits":8.333333333333334,"dds":0.48,"last_synced_commit":"99abc2c9d975efab797d8b4fa90487b48b32d8e0"},"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forgepackages%2Fforge-htmx","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forgepackages%2Fforge-htmx/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forgepackages%2Fforge-htmx/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/forgepackages%2Fforge-htmx/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/forgepackages","download_url":"https://codeload.github.com/forgepackages/forge-htmx/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224623337,"owners_count":17342555,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["django","forgepackages","htmx","python"],"created_at":"2024-11-07T23:39:59.785Z","updated_at":"2024-11-15T09:05:41.314Z","avatar_url":"https://github.com/forgepackages.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# forge-htmx\n\nThe `forge-htmx` Django package adds a couple of unique features for working with HTMX.\nOne is [template fragments](#template-fragments) and the other is [view actions](#view-actions).\n\nThe 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.\n\nThe `HTMXViewMixin` is the starting point for the server-side HTMX behavior.\nTo use these feaures on a view,\nsimply inherit from the class (yes, this is designed to work with class-based views).\n\n```python\nfrom django.views.generic import TemplateView\n\nfrom forgehtmx.views import HTMXViewMixin\n\n\nclass HomeView(HTMXViewMixin, TemplateView):\n    template_name = \"home.html\"\n```\n\nIn your `base.html` template (or wherever need the HTMX scripts),\nyou can use the `{% htmx_js %}` template tag:\n\n```html\n\u003c!-- base.template.html --\u003e\n{% load htmx %}\n\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n    \u003cmeta charset=\"UTF-8\"\u003e\n    \u003ctitle\u003eMy Site\u003c/title\u003e\n    {% htmx_js %}\n\u003c/head\u003e\n\u003cbody\u003e\n    {% block content %}{% endblock %}\n\u003c/body\u003e\n```\n\n## Installation\n\nYou can install `forge-htmx` with any Django project:\n\n```sh\npip install forge-htmx\n```\n\nThen add `forgehtmx` to `settings.py`:\n\n```python\nINSTALLED_APPS = [\n    # ...\n    \"forgehtmx\",\n]\n```\n\n## Template Fragments\n\nAn `{% htmxfragment %}` can be used to render a specific part of your template in HTMX responses.\nWhen you use a fragment, all `hx-get`, `hx-post`, etc. elements inside that fragment will automatically send a request to the current URL,\nrender *only* the updated content for the fragment,\nand swap out the fragment.\n\nHere's an example:\n\n```html\n\u003c!-- home.html --\u003e\n{% extends \"base.html\" %}\n\n{% load htmx %}\n\n{% block content %}\n\u003cheader\u003e\n  \u003ch1\u003ePage title\u003c/h1\u003e\n\u003c/header\u003e\n\n\u003cmain\u003e\n  {% htmxfragment main %}\n  \u003cp\u003eThe time is {% now \"jS F Y H:i\" %}\u003c/p\u003e\n\n  \u003cbutton hx-get\u003eRefresh\u003c/button\u003e\n  {% endhtmxfragment %}\n\u003c/main\u003e\n{% endblock %}\n```\n\nEverything inside `{% htmxfragment %}` will automatically update when \"Refresh\" is clicked.\n\n### Lazy template fragments\n\nIf you want to render a fragment lazily,\nyou can add the `lazy` attribute to the `{% htmxfragment %}` tag.\n\n```html\n{% htmxfragment main lazy %}\n\u003c!-- This content will be fetched with hx-get --\u003e\n{% endhtmxfragment %}\n```\n\nThis pairs nicely with passing a callable function or method as a context variable,\nwhich will only get invoked when the fragment actually gets rendered on the lazy load.\n\n```python\ndef fetch_items():\n    import time\n    time.sleep(2)\n    return [\"foo\", \"bar\", \"baz\"]\n\n\nclass HomeView(HTMXViewMixin, TemplateView):\n    def get_context_data(self, **kwargs):\n        context = super().get_context_data(**kwargs)\n        context[\"items\"] = fetch_items  # Missing () are on purpose!\n        return context\n```\n\n```html\n{% htmxfragment main lazy %}\n\u003cul\u003e\n  {% for item in items %}\n    \u003cli\u003e{{ item }}\u003c/li\u003e\n  {% endfor %}\n\u003c/ul\u003e\n{% endhtmxfragment %}\n```\n\n### How does it work?\n\nWhen you use the `{% htmxfragment %}` tag,\na standard `div` is output that looks like this:\n\n```html\n\u003cdiv fhx-fragment=\"main\" hx-swap=\"outerHTML\" hx-target=\"this\" hx-indicator=\"this\"\u003e\n  {{ fragment_content }}\n\u003c/div\u003e\n```\n\nThe `fhx-fragment` is a custom attribute that we've added (\"F\" is for \"Forge\"),\nbut the rest are standard HTMX attributes.\n\nWhen Django renders the response to an HTMX request,\nit will get the `FHX-Fragment` header,\nfind the fragment with that name in the template,\nand render that for the response.\n\nThen the response content is automatically swapped in to replace the content of your `{% htmxfragment %}` tag.\n\nNote that there is no URL specified on the `hx-get` attribute.\nBy default, HTMX will send the request to the current URL for the page.\nWhen you're working with fragments, this is typically the behavior you want!\n(You're on a page and want to selectively re-render a part of that page.)\n\nThe `{% htmxfragment %}` tag is somewhat similar to a `{% block %}` tag --\nthe fragments on a page should be named and unique,\nand you can't use it inside of loops.\nFor fragment-like behavior inside of a for-loop,\nyou'll most likely want to set up a dedicated URL that can handle a single instance of the looped items,\nand maybe leverage [dedicated templates](#dedicated-templates).\n\n## View Actions\n\nView actions let you define multiple \"actions\" on a class-based view.\nThis is an alternative to defining specific API endpoints or form views to handle basic button interactions.\n\nWith view actions you can design a single view that renders a single template,\nand associate buttons in that template with class methods in the view.\n\nAs 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.\n\nIn our template, we would use the `fhx-action` attribute to name the action:\n\n```html\n{% extends \"base.html\" %}\n\n{% load htmx %}\n\n{% block content %}\n\u003cheader\u003e\n  \u003ch1\u003e{{ pullrequest }}\u003c/h1\u003e\n\u003c/header\u003e\n\n\u003cmain\u003e\n  {% htmxfragment pullrequest %}\n  \u003cp\u003eState: {{ pullrequest.state }}\u003c/p\u003e\n\n  {% if pullrequest.state == \"open\" %}\n    \u003c!-- If it's open, they can close or merge it --\u003e\n    \u003cbutton hx-post fhx-action=\"close\"\u003eClose\u003c/button\u003e\n    \u003cbutton hx-post fhx-action=\"merge\"\u003eMerge\u003c/button\u003e\n  {% else if pullrequest.state == \"closed\" %}\n    \u003c!-- If it's closed, it can be re-opened --\u003e\n    \u003cbutton hx-post fhx-action=\"open\"\u003eOpen\u003c/button\u003e\n  {% endif %}\n\n  {% endhtmxfragment %}\n\u003c/main\u003e\n{% endblock %}\n```\n\nThen in the view class, we can define methods for each HTTP method + `fhx-action`:\n\n```python\nclass PullRequestDetailView(HTMXViewMixin, DetailView):\n    def get_queryset(self):\n        # The queryset will apply to all actions on the view, so \"permission\" logic can be shared\n        return super().get_queryset().filter(users=self.request.user)\n\n    # Action handling methods follow this format:\n    # htmx_{method}_{action}\n    def htmx_post_open(self, request, *args, **kwargs):\n        self.object = self.get_object()\n\n        if self.object.state != \"closed\":\n            raise ValueError(\"Only a closed pull request can be opened\")\n\n        self.object.state = \"closed\"\n        self.object.save()\n\n        # Render the updated content the standard calls\n        # (which will selectively render our fragment if applicable)\n        context = self.get_context_data(object=self.object)\n        return self.render_to_response(context)\n\n    def htmx_post_close(self, request, *args, **kwargs):\n        self.object = self.get_object()\n\n        if self.object.state != \"open\":\n            raise ValueError(\"Only a open pull request can be closed\")\n\n        self.object.state = \"open\"\n        self.object.save()\n\n        context = self.get_context_data(object=self.object)\n        return self.render_to_response(context)\n\n    def htmx_post_merge(self, request, *args, **kwargs):\n        self.object = self.get_object()\n\n        if self.object.state != \"open\":\n            raise ValueError(\"Only a open pull request can be merged\")\n\n        self.object.state = \"merged\"\n        self.object.save()\n\n        context = self.get_context_data(object=self.object)\n        return self.render_to_response(context)\n```\n\nThis can be a matter of preference,\nbut typically you may end up building out an entire form, API, or set of URLs to handle these behaviors.\nIf you application is only going to handle these actions via HTMX,\nthen a single View may be a simpler way to do it.\n\nNote that currently we don't have many helper-functions for parsing or returning HTMX responses --\nthis can basically all be done through standard request and response headers:\n\n```python\nclass PullRequestDetailView(HTMXViewMixin, DetailView):\n    def get_queryset(self):\n        # The queryset will apply to all actions on the view, so \"permission\" logic can be shared\n        return super().get_queryset().filter(users=self.request.user)\n\n    # You can also leave off the \"fhx-action\" attribute and just handle the HTTP method\n    def htmx_delete(self, request, *args, **kwargs):\n        self.object = self.get_object()\n\n        self.object.delete()\n\n        # Tell HTMX to do a client-side redirect when it receives the response\n        response = HttpResponse(status=204)\n        response[\"HX-Redirect\"] = \"/\"\n        return response\n```\n\n## Dedicated Templates\n\nA small additional features of `forge-htmx` is that it will automatically find templates named `{template_name}_htmx.html` for HTMX requests.\nMore than anything, this is just a nice way to formalize a naming scheme for template \"partials\" dedicated to HTMX.\n\nBecause template fragments don't work inside of loops,\nfor example,\nyou'll often need to define dedicated URLs to handle the HTMX behaviors for individual items in a loop.\nYou can sometimes think of these as \"pages within a page\".\n\nSo if you have a template that renders a collection of items,\nyou can do the initial render using a Django `{% include %}`:\n\n```html\n\u003c!-- pullrequests/pullrequest_list.html --\u003e\n{% extends \"base.html\" %}\n\n{% block content %}\n\n{% for pullrequest in pullrequests %}\n\u003cdiv\u003e\n  {% include \"pullrequests/pullrequest_detail_htmx.html\" %}\n\u003c/div\u003e\n{% endfor %}\n\n{% endblock %}\n```\n\nAnd then subsequent HTMX requests/actions on individual items can be handled by a separate URL/View:\n\n```html\n\u003c!-- pullrequests/pullrequest_detail_htmx.html --\u003e\n\u003cdiv hx-url=\"{% url 'pullrequests:detail' pullrequest.uuid %}\" hx-swap=\"outerHTML\" hx-target=\"this\"\u003e\n  \u003c!-- Send all HTMX requests to a URL for single pull requests (works inside of a loop, or on a single detail page) --\u003e\n  \u003ch2\u003e{{ pullrequest.title }}\u003c/h2\u003e\n  \u003cbutton hx-get\u003eRefresh\u003c/button\u003e\n  \u003cbutton hx-post fhx-action=\"update\"\u003eUpdate\u003c/button\u003e\n\u003c/div\u003e\n```\n\n*If* you need a URL to render an individual item, you can simply include the same template fragment in most cases:\n\n```html\n\u003c!-- pullrequests/pullrequest_detail.html --\u003e\n{% extends \"base.html\" %}\n\n{% block content %}\n\n{% include \"pullrequests/pullrequest_detail_htmx.html\" %}\n\n{% endblock %}\n```\n\n```python\n# urls.py and views.py\n# urls.py\napp_name = \"pullrequests\"\n\nurlpatterns = [\n  path(\"\u003cuuid:uuid\u003e/\", views.PullRequestDetailView.as_view(), name=\"detail\"),\n]\n\n# views.py\nclass PullRequestDetailView(HTMXViewMixin, DetailView):\n  def htmx_post_update(self, request, *args, **kwargs):\n      self.object = self.get_object()\n\n      self.object.update()\n\n      context = self.get_context_data(object=self.object)\n      return self.render_to_response(context)\n```\n\n## Tailwind CSS variant\n\nThe standard behavior for `{% htmxfragment %}` is to set `hx-indicator=\"this\"` on the rendered element.\nThis tells HTMX to add the `htmx-request` class to the fragment element when it is loading.\n\nSince Forge emphasizes using Tailwind CSS,\nhere's a simple variant you can add to your `tailwind.config.js` to easily style the loading state:\n\n```js\nconst plugin = require('tailwindcss/plugin')\n\nmodule.exports = {\n  plugins: [\n    // Add variants for htmx-request class for loading states\n    plugin(({addVariant}) =\u003e addVariant('htmx-request', ['\u0026.htmx-request', '.htmx-request \u0026']))\n  ],\n}\n```\n\nYou can then prefix any class with `htmx-request:` to decide what it looks like while HTMX requests are being sent:\n\n```html\n\u003c!-- The \"htmx-request\" class will be added to the \u003cform\u003e by default --\u003e\n\u003cform hx-post=\"{{ url }}\"\u003e\n    \u003c!-- Showing an element --\u003e\n    \u003cdiv class=\"hidden htmx-request:block\"\u003e\n        Loading\n    \u003c/div\u003e\n\n    \u003c!-- Changing a button's class --\u003e\n    \u003cbutton class=\"text-white bg-black htmx-request:opacity-50 htmx-request:cursor-wait\" type=\"submit\"\u003eSubmit\u003c/button\u003e\n\u003c/form\u003e\n```\n\n## CSRF tokens\n\nWe configure CSRF tokens for you with the HTMX JS API.\nYou don't have to put `hx-headers` on the `\u003cbody\u003e` tag, for example.\n\n## Error classes\n\nThis app also includes an HTMX extension for adding error classes for failed requests.\n\n- `htmx-error-response` for `htmx:responseError`\n- `htmx-error-response-{{ status_code }}` for `htmx:responseError`\n- `htmx-error-send` for `htmx:sendError`\n\nTo enable them, use `hx-ext=\"error-classes\"`.\n\nYou can add the ones you want as Tailwind variants and use them to show error messages.\n\n```js\nconst plugin = require('tailwindcss/plugin')\n\nmodule.exports = {\n  plugins: [\n    // Add variants for htmx-request class for loading states\n    plugin(({addVariant}) =\u003e addVariant('htmx-error-response-429', ['\u0026.htmx-error-response-429', '.htmx-error-response-429 \u0026']))\n  ],\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforgepackages%2Fforge-htmx","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fforgepackages%2Fforge-htmx","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fforgepackages%2Fforge-htmx/lists"}