├── .gitignore ├── .python-version ├── Pipfile ├── Pipfile.lock ├── README.md ├── django-htmx-modal-form-10-fps.gif ├── django_htmx_modal_form ├── __init__.py ├── asgi.py ├── settings.py ├── urls.py └── wsgi.py ├── manage.py ├── movie_collection ├── __init__.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ └── __init__.py ├── models.py ├── static │ └── toast.js ├── templates │ ├── index.html │ ├── movie_form.html │ └── movie_list.html └── views.py ├── poetry.lock ├── pyproject.toml └── theme ├── __init__.py ├── apps.py ├── static └── js │ ├── alpine.min.js │ └── htmx.min.js └── static_src ├── .gitignore ├── package-lock.json ├── package.json ├── postcss.config.js ├── src └── styles.css └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.venv/ 2 | db.sqlite3 3 | __pycache__/ 4 | .idea/ 5 | theme/static/css/dist 6 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.13 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | django = "==3.2.*" 8 | django-widget-tweaks = "==1.4.*" 9 | 10 | [dev-packages] 11 | autopep8 = "*" 12 | 13 | [requires] 14 | python_version = "3.9" 15 | 16 | [scripts] 17 | migrate = "python manage.py migrate" 18 | server = "python manage.py runserver" 19 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "0b5186c5244f7c11e83bad2f9e4dda2ccb24c1c4f33b444319ffe3b0171e45d1" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.9" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "asgiref": { 20 | "hashes": [ 21 | "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0", 22 | "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9" 23 | ], 24 | "markers": "python_version >= '3.7'", 25 | "version": "==3.5.0" 26 | }, 27 | "django": { 28 | "hashes": [ 29 | "sha256:6d93497a0a9bf6ba0e0b1a29cccdc40efbfc76297255b1309b3a884a688ec4b6", 30 | "sha256:b896ca61edc079eb6bbaa15cf6071eb69d6aac08cce5211583cfb41515644fdf" 31 | ], 32 | "index": "pypi", 33 | "version": "==3.2.13" 34 | }, 35 | "django-widget-tweaks": { 36 | "hashes": [ 37 | "sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e", 38 | "sha256:fe6b17d5d595c63331f300917980db2afcf71f240ab9341b954aea8f45d25b9a" 39 | ], 40 | "index": "pypi", 41 | "version": "==1.4.12" 42 | }, 43 | "pytz": { 44 | "hashes": [ 45 | "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", 46 | "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" 47 | ], 48 | "version": "==2022.1" 49 | }, 50 | "sqlparse": { 51 | "hashes": [ 52 | "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", 53 | "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" 54 | ], 55 | "markers": "python_version >= '3.5'", 56 | "version": "==0.4.2" 57 | } 58 | }, 59 | "develop": { 60 | "autopep8": { 61 | "hashes": [ 62 | "sha256:44f0932855039d2c15c4510d6df665e4730f2b8582704fa48f9c55bd3e17d979", 63 | "sha256:ed77137193bbac52d029a52c59bec1b0629b5a186c495f1eb21b126ac466083f" 64 | ], 65 | "index": "pypi", 66 | "version": "==1.6.0" 67 | }, 68 | "pycodestyle": { 69 | "hashes": [ 70 | "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20", 71 | "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f" 72 | ], 73 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 74 | "version": "==2.8.0" 75 | }, 76 | "toml": { 77 | "hashes": [ 78 | "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", 79 | "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" 80 | ], 81 | "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", 82 | "version": "==0.10.2" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django+HTMX modal form 2 | 3 | This project is forked from Benoit Blanchon's demonstration of how to show a Django Form in a modal dialog box using HTMX. 4 | 5 | Here are Benoit's blog posts and youtube videos about the technique: 6 | 7 | * [Modal forms with Django+HTMX](https://blog.benoitblanchon.fr/django-htmx-modal-form/) (blog post) 8 | * [Modal forms with Django+HTMX](https://youtu.be/3dyQigrEj8A) (YouTube video) 9 | * [Toasts with Django+HTMX](https://blog.benoitblanchon.fr/django-htmx-toasts/) (blog post) 10 | * [Toasts with Django+HTMX](https://youtu.be/pAtrj8A-Kl4) (YouTube video) 11 | 12 | This fork moves to django 4, uses poetry to manage the project, and uses Tailwind CSS instead of Bootstrap. 13 | 14 | No incompatibilites were intentionally introduced versus django 3.2, so the original pipenv files should probably still install the virtual environment and run the server just fine. 15 | 16 | To set things up using `poetry` run: 17 | 18 | ``` 19 | poetry install 20 | poetry run python manage.py migrate 21 | poetry run python manage.py tailwind install 22 | poetry run python manage.py tailwind build 23 | poetry run python manage.py runserver 24 | ``` 25 | 26 | ### Credits 27 | 28 | The Tailwind CSS branch includes markup adapted from TailwindUI, which is used according to the terms of the TailwindUI license. It also includes an SVG spinner adapted from [Flowbite](https://flowbite.com/docs/components/spinner/), which is Copyright (c) Themesberg (Crafty Dwarf Inc.) and is [used under the terms of the MIT license](https://flowbite.com/docs/getting-started/license/). 29 | 30 | Both the authors of TailwindUI and the authors of Flowbite have really helped me improve my understanding of Tailwind, and I'm grateful that they publish their work under such generous terms. -------------------------------------------------------------------------------- /django-htmx-modal-form-10-fps.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffbeier/django-htmx-modal-form/42a17593eb064106ca933c852cb98e0e7c3968e9/django-htmx-modal-form-10-fps.gif -------------------------------------------------------------------------------- /django_htmx_modal_form/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffbeier/django-htmx-modal-form/42a17593eb064106ca933c852cb98e0e7c3968e9/django_htmx_modal_form/__init__.py -------------------------------------------------------------------------------- /django_htmx_modal_form/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for django_htmx_modal_form project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_htmx_modal_form.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /django_htmx_modal_form/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for django_htmx_modal_form project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.2.11. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.2/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'django-insecure-69(8-htd04&)ascyb=lh5cd%q@vfdg105y&xsi$9yif5^&d9c&' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'movie_collection', 35 | 36 | 'widget_tweaks', 37 | 38 | 'django.contrib.auth', 39 | 'django.contrib.contenttypes', 40 | 'django.contrib.sessions', 41 | 'django.contrib.messages', 42 | 'django.contrib.staticfiles', 43 | 'tailwind', 44 | 'theme', 45 | 'django_browser_reload', 46 | ] 47 | 48 | MIDDLEWARE = [ 49 | 'django.middleware.security.SecurityMiddleware', 50 | 'django.contrib.sessions.middleware.SessionMiddleware', 51 | 'django.middleware.common.CommonMiddleware', 52 | 'django.middleware.csrf.CsrfViewMiddleware', 53 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 54 | 'django.contrib.messages.middleware.MessageMiddleware', 55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 56 | 'django_browser_reload.middleware.BrowserReloadMiddleware', 57 | ] 58 | 59 | ROOT_URLCONF = 'django_htmx_modal_form.urls' 60 | 61 | TEMPLATES = [ 62 | { 63 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 64 | 'DIRS': [], 65 | 'APP_DIRS': True, 66 | 'OPTIONS': { 67 | 'context_processors': [ 68 | 'django.template.context_processors.debug', 69 | 'django.template.context_processors.request', 70 | 'django.contrib.auth.context_processors.auth', 71 | 'django.contrib.messages.context_processors.messages', 72 | ], 73 | }, 74 | }, 75 | ] 76 | 77 | WSGI_APPLICATION = 'django_htmx_modal_form.wsgi.application' 78 | 79 | 80 | # Database 81 | # https://docs.djangoproject.com/en/3.2/ref/settings/#databases 82 | 83 | DATABASES = { 84 | 'default': { 85 | 'ENGINE': 'django.db.backends.sqlite3', 86 | 'NAME': BASE_DIR / 'db.sqlite3', 87 | } 88 | } 89 | 90 | 91 | # Password validation 92 | # https://docs.djangoproject.com/en/3.2/ref/settings/#auth-password-validators 93 | 94 | AUTH_PASSWORD_VALIDATORS = [ 95 | { 96 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 97 | }, 98 | { 99 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 100 | }, 101 | { 102 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 103 | }, 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 106 | }, 107 | ] 108 | 109 | 110 | # Internationalization 111 | # https://docs.djangoproject.com/en/3.2/topics/i18n/ 112 | 113 | LANGUAGE_CODE = 'en-us' 114 | 115 | TIME_ZONE = 'UTC' 116 | 117 | USE_I18N = True 118 | 119 | USE_L10N = True 120 | 121 | USE_TZ = True 122 | 123 | 124 | # Static files (CSS, JavaScript, Images) 125 | # https://docs.djangoproject.com/en/3.2/howto/static-files/ 126 | 127 | STATIC_URL = '/static/' 128 | 129 | # Default primary key field type 130 | # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field 131 | 132 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 133 | 134 | TAILWIND_APP_NAME = 'theme' 135 | INTERNAL_IPS = [ 136 | "127.0.0.1", 137 | ] 138 | -------------------------------------------------------------------------------- /django_htmx_modal_form/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path, include 2 | from movie_collection import views 3 | 4 | 5 | urlpatterns = [ 6 | path('', views.index), 7 | path('movies', views.movie_list, name='movie_list'), 8 | path('movies/add', views.add_movie, name='add_movie'), 9 | path('movies//remove', views.remove_movie, name='remove_movie'), 10 | path('movies//edit', views.edit_movie, name='edit_movie'), 11 | path("__reload__/", include("django_browser_reload.urls")), 12 | ] 13 | -------------------------------------------------------------------------------- /django_htmx_modal_form/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for django_htmx_modal_form project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_htmx_modal_form.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_htmx_modal_form.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /movie_collection/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffbeier/django-htmx-modal-form/42a17593eb064106ca933c852cb98e0e7c3968e9/movie_collection/__init__.py -------------------------------------------------------------------------------- /movie_collection/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class MovieCollectionConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'movie_collection' 7 | -------------------------------------------------------------------------------- /movie_collection/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from .models import Movie 4 | 5 | 6 | class MovieForm(forms.ModelForm): 7 | 8 | class Meta: 9 | model = Movie 10 | fields = ['title', 'year', 'rating'] 11 | -------------------------------------------------------------------------------- /movie_collection/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.2.11 on 2022-01-20 13:32 2 | 3 | import django.core.validators 4 | from django.db import migrations, models 5 | 6 | 7 | def add_sample_movies(apps, schema_editor): 8 | Movie = apps.get_model("movie_collection", "Movie") 9 | db_alias = schema_editor.connection.alias 10 | Movie.objects.using(db_alias).bulk_create([ 11 | Movie(title="Back to the Future", year=1985, rating=5), 12 | Movie(title="Who Framed Roger Rabbit", year=1988, rating=4), 13 | Movie(title="Phantom of the Paradise", year=1974, rating=4), 14 | ]) 15 | 16 | 17 | class Migration(migrations.Migration): 18 | 19 | initial = True 20 | 21 | dependencies = [ 22 | ] 23 | 24 | operations = [ 25 | migrations.CreateModel( 26 | name='Movie', 27 | fields=[ 28 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 29 | ('title', models.CharField(max_length=40, unique=True)), 30 | ('year', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1895), django.core.validators.MaxValueValidator(2050)])), 31 | ('rating', models.PositiveSmallIntegerField(choices=[(1, '★☆☆☆☆'), (2, '★★☆☆☆'), (3, '★★★☆☆'), (4, '★★★★☆'), (5, '★★★★★')])), 32 | ], 33 | ), 34 | migrations.RunPython(add_sample_movies, migrations.RunPython.noop) 35 | ] 36 | -------------------------------------------------------------------------------- /movie_collection/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffbeier/django-htmx-modal-form/42a17593eb064106ca933c852cb98e0e7c3968e9/movie_collection/migrations/__init__.py -------------------------------------------------------------------------------- /movie_collection/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.core.validators import MaxValueValidator, MinValueValidator 3 | 4 | 5 | class Movie(models.Model): 6 | 7 | title = models.CharField(max_length=40, unique=True) 8 | 9 | year = models.PositiveSmallIntegerField( 10 | validators=[ 11 | MinValueValidator(1895), 12 | MaxValueValidator(2050), 13 | ] 14 | ) 15 | 16 | rating = models.PositiveSmallIntegerField(choices=( 17 | (1, "★☆☆☆☆"), 18 | (2, "★★☆☆☆"), 19 | (3, "★★★☆☆"), 20 | (4, "★★★★☆"), 21 | (5, "★★★★★"), 22 | )) 23 | -------------------------------------------------------------------------------- /movie_collection/static/toast.js: -------------------------------------------------------------------------------- 1 | function toastHandler() { 2 | return { 3 | toasts: [], 4 | visible: [], 5 | add(toast) { 6 | toast.id = Date.now() 7 | this.toasts.push(toast) 8 | this.fire(toast.id) 9 | }, 10 | fire(id) { 11 | this.visible.push(this.toasts.find(toast => toast.id == id)) 12 | const timeShown = 2000 * this.visible.length 13 | setTimeout(() => { 14 | this.remove(id) 15 | }, timeShown) 16 | }, 17 | remove(id) { 18 | const toast = this.visible.find(toast => toast.id == id) 19 | const index = this.visible.indexOf(toast) 20 | this.visible.splice(index, 1) 21 | }, 22 | } 23 | } 24 | 25 | ;(function () { 26 | htmx.on("showMessage", (e) => { 27 | let message_detail = {type: 'info', text: '🎬 ' + e.detail.value} 28 | dispatchEvent( 29 | new CustomEvent('notice', { 30 | detail: message_detail, 31 | bubbles: true, 32 | cancelable: true, 33 | composed: true 34 | } 35 | ) 36 | ); 37 | }) 38 | })() 39 | -------------------------------------------------------------------------------- /movie_collection/templates/index.html: -------------------------------------------------------------------------------- 1 | {% load static tailwind_tags %} 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% tailwind_css %} 9 | My Movie Collection 10 | 11 | 12 |
13 |
14 |
15 |

My Movie Collection

16 |

A sample project that shows how to support modal forms with 17 | Django+HTMX with minimal JavaScript code.

18 |
19 |
20 | 24 |
25 |
26 |
27 |
28 | 29 | 30 | 31 | 34 | 37 | 38 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 |
32 | Title 33 | Rating 39 | Edit 40 |

Retrieving Movies...

50 |
51 |
52 |
53 |

Retrieving Movie List...

54 | 55 |
56 | 58 | 60 | 62 | 63 |
64 |
65 |
66 |
67 |
68 | 69 | 81 | 82 | 83 |
88 | 109 |
110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /movie_collection/templates/movie_form.html: -------------------------------------------------------------------------------- 1 | {% load widget_tweaks %} 2 | {% with WIDGET_ERROR_CLASS='is-invalid' %} 3 | 39 | {% endwith %} 40 | -------------------------------------------------------------------------------- /movie_collection/templates/movie_list.html: -------------------------------------------------------------------------------- 1 | {% for movie in movies %} 2 | 3 | 4 | {{ movie.title }} 5 |
6 |
Year
7 |
{{ movie.year }}
8 |
9 | 10 | {{ movie.year }} 11 | {{ movie.get_rating_display }} 12 | 13 | 14 | 15 | 16 | {% endfor %} -------------------------------------------------------------------------------- /movie_collection/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.shortcuts import render 4 | from django.http import HttpResponse 5 | from django.views.decorators.http import require_POST 6 | from django.shortcuts import get_object_or_404 7 | 8 | from .models import Movie 9 | from .forms import MovieForm 10 | 11 | 12 | def index(request): 13 | return render(request, 'index.html') 14 | 15 | 16 | def movie_list(request): 17 | return render(request, 'movie_list.html', { 18 | 'movies': Movie.objects.all(), 19 | }) 20 | 21 | 22 | def add_movie(request): 23 | if request.method == "POST": 24 | form = MovieForm(request.POST) 25 | if form.is_valid(): 26 | movie = form.save() 27 | return HttpResponse( 28 | status=204, 29 | headers={ 30 | 'HX-Trigger': json.dumps({ 31 | "movieListChanged": None, 32 | "showMessage": f"{movie.title} added." 33 | }) 34 | }) 35 | else: 36 | form = MovieForm() 37 | return render(request, 'movie_form.html', { 38 | 'form': form, 39 | }) 40 | 41 | 42 | def edit_movie(request, pk): 43 | movie = get_object_or_404(Movie, pk=pk) 44 | if request.method == "POST": 45 | form = MovieForm(request.POST, instance=movie) 46 | if form.is_valid(): 47 | form.save() 48 | return HttpResponse( 49 | status=204, 50 | headers={ 51 | 'HX-Trigger': json.dumps({ 52 | "movieListChanged": None, 53 | "showMessage": f"{movie.title} updated." 54 | }) 55 | } 56 | ) 57 | else: 58 | form = MovieForm(instance=movie) 59 | return render(request, 'movie_form.html', { 60 | 'form': form, 61 | 'movie': movie, 62 | }) 63 | 64 | 65 | @ require_POST 66 | def remove_movie(request, pk): 67 | movie = get_object_or_404(Movie, pk=pk) 68 | movie.delete() 69 | return HttpResponse( 70 | status=204, 71 | headers={ 72 | 'HX-Trigger': json.dumps({ 73 | "movieListChanged": None, 74 | "showMessage": f"{movie.title} deleted." 75 | }) 76 | }) 77 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "asgiref" 3 | version = "3.5.2" 4 | description = "ASGI specs, helper code, and adapters" 5 | category = "main" 6 | optional = false 7 | python-versions = ">=3.7" 8 | 9 | [package.extras] 10 | tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] 11 | 12 | [[package]] 13 | name = "django" 14 | version = "4.0.5" 15 | description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." 16 | category = "main" 17 | optional = false 18 | python-versions = ">=3.8" 19 | 20 | [package.dependencies] 21 | asgiref = ">=3.4.1,<4" 22 | sqlparse = ">=0.2.2" 23 | tzdata = {version = "*", markers = "sys_platform == \"win32\""} 24 | 25 | [package.extras] 26 | argon2 = ["argon2-cffi (>=19.1.0)"] 27 | bcrypt = ["bcrypt"] 28 | 29 | [[package]] 30 | name = "django-browser-reload" 31 | version = "1.6.0" 32 | description = "Automatically reload your browser in development." 33 | category = "main" 34 | optional = false 35 | python-versions = ">=3.7" 36 | 37 | [package.dependencies] 38 | Django = ">=3.2" 39 | 40 | [[package]] 41 | name = "django-tailwind" 42 | version = "3.3.0" 43 | description = "Tailwind CSS Framework for Django projects" 44 | category = "main" 45 | optional = false 46 | python-versions = ">=3.8,<4.0" 47 | 48 | [package.dependencies] 49 | django = ">=2.2.28" 50 | django-browser-reload = ">=1.6.0,<2.0.0" 51 | 52 | [[package]] 53 | name = "django-widget-tweaks" 54 | version = "1.4.12" 55 | description = "Tweak the form field rendering in templates, not in python-level form definitions." 56 | category = "main" 57 | optional = false 58 | python-versions = ">=3.7" 59 | 60 | [[package]] 61 | name = "sqlparse" 62 | version = "0.4.2" 63 | description = "A non-validating SQL parser." 64 | category = "main" 65 | optional = false 66 | python-versions = ">=3.5" 67 | 68 | [[package]] 69 | name = "tzdata" 70 | version = "2022.1" 71 | description = "Provider of IANA time zone data" 72 | category = "main" 73 | optional = false 74 | python-versions = ">=2" 75 | 76 | [metadata] 77 | lock-version = "1.1" 78 | python-versions = "^3.9" 79 | content-hash = "e6bd82c6b882d85df1e1b029ff42676e32017ce25d20917fa68e44daa5a6c7f3" 80 | 81 | [metadata.files] 82 | asgiref = [ 83 | {file = "asgiref-3.5.2-py3-none-any.whl", hash = "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4"}, 84 | {file = "asgiref-3.5.2.tar.gz", hash = "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424"}, 85 | ] 86 | django = [ 87 | {file = "Django-4.0.5-py3-none-any.whl", hash = "sha256:502ae42b6ab1b612c933fb50d5ff850facf858a4c212f76946ecd8ea5b3bf2d9"}, 88 | {file = "Django-4.0.5.tar.gz", hash = "sha256:f7431a5de7277966f3785557c3928433347d998c1e6459324501378a291e5aab"}, 89 | ] 90 | django-browser-reload = [ 91 | {file = "django-browser-reload-1.6.0.tar.gz", hash = "sha256:9ca69c71796f53868bdc7421f120d147f7a64faa0d5d8c06970ba3f8061af63c"}, 92 | {file = "django_browser_reload-1.6.0-py3-none-any.whl", hash = "sha256:31b8b2d51e8faa5878f21e6b60b8f43e1123907c6e082e9e967962ba63958829"}, 93 | ] 94 | django-tailwind = [ 95 | {file = "django-tailwind-3.3.0.tar.gz", hash = "sha256:51e67e3d8a9597b8b4dbbd82f8f3336ffc8e2e20bf5ef1bfa49dc832ae2248be"}, 96 | {file = "django_tailwind-3.3.0-py3-none-any.whl", hash = "sha256:3e80b7d2187dabc082f7bb08fb983f412cce0c120080a05cde060b1cb461bb16"}, 97 | ] 98 | django-widget-tweaks = [ 99 | {file = "django-widget-tweaks-1.4.12.tar.gz", hash = "sha256:9bfc5c705684754a83cc81da328b39ad1b80f32bd0f4340e2a810cbab4b0c00e"}, 100 | {file = "django_widget_tweaks-1.4.12-py3-none-any.whl", hash = "sha256:fe6b17d5d595c63331f300917980db2afcf71f240ab9341b954aea8f45d25b9a"}, 101 | ] 102 | sqlparse = [ 103 | {file = "sqlparse-0.4.2-py3-none-any.whl", hash = "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d"}, 104 | {file = "sqlparse-0.4.2.tar.gz", hash = "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae"}, 105 | ] 106 | tzdata = [ 107 | {file = "tzdata-2022.1-py2.py3-none-any.whl", hash = "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9"}, 108 | {file = "tzdata-2022.1.tar.gz", hash = "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3"}, 109 | ] 110 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "django-htmx-modal-form" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Your Name "] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.9" 9 | Django = "^4.0.5" 10 | django-widget-tweaks = "^1.4.12" 11 | django-tailwind = "^3.3.0" 12 | 13 | [tool.poetry.dev-dependencies] 14 | 15 | [build-system] 16 | requires = ["poetry-core>=1.0.0"] 17 | build-backend = "poetry.core.masonry.api" 18 | -------------------------------------------------------------------------------- /theme/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geoffbeier/django-htmx-modal-form/42a17593eb064106ca933c852cb98e0e7c3968e9/theme/__init__.py -------------------------------------------------------------------------------- /theme/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ThemeConfig(AppConfig): 5 | name = 'theme' 6 | -------------------------------------------------------------------------------- /theme/static/js/alpine.min.js: -------------------------------------------------------------------------------- 1 | (()=>{var We=!1,Ge=!1,j=[];function Nt(e){nn(e)}function nn(e){j.includes(e)||j.push(e),on()}function he(e){let t=j.indexOf(e);t!==-1&&j.splice(t,1)}function on(){!Ge&&!We&&(We=!0,queueMicrotask(sn))}function sn(){We=!1,Ge=!0;for(let e=0;ee.effect(t,{scheduler:r=>{Je?Nt(r):r()}}),Ye=e.raw}function Ze(e){K=e}function Dt(e){let t=()=>{};return[n=>{let i=K(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),Y(i))},i},()=>{t()}]}var $t=[],Lt=[],Ft=[];function jt(e){Ft.push(e)}function _e(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Lt.push(t))}function Kt(e){$t.push(e)}function Bt(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Qe(e,t){!e._x_attributeCleanups||Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}var et=new MutationObserver(Xe),tt=!1;function rt(){et.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),tt=!0}function cn(){an(),et.disconnect(),tt=!1}var ee=[],nt=!1;function an(){ee=ee.concat(et.takeRecords()),ee.length&&!nt&&(nt=!0,queueMicrotask(()=>{ln(),nt=!1}))}function ln(){Xe(ee),ee.length=0}function m(e){if(!tt)return e();cn();let t=e();return rt(),t}var it=!1,ge=[];function zt(){it=!0}function Vt(){it=!1,Xe(ge),ge=[]}function Xe(e){if(it){ge=ge.concat(e);return}let t=[],r=[],n=new Map,i=new Map;for(let o=0;os.nodeType===1&&t.push(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.push(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{Qe(s,o)}),n.forEach((o,s)=>{$t.forEach(a=>a(s,o))});for(let o of r)if(!t.includes(o)&&(Lt.forEach(s=>s(o)),o._x_cleanups))for(;o._x_cleanups.length;)o._x_cleanups.pop()();t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.includes(o)||!o.isConnected||(delete o._x_ignoreSelf,delete o._x_ignore,Ft.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function xe(e){return P(N(e))}function C(e,t,r){return e._x_dataStack=[t,...N(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function ot(e,t){let r=e._x_dataStack[0];Object.entries(t).forEach(([n,i])=>{r[n]=i})}function N(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?N(e.host):e.parentNode?N(e.parentNode):[]}function P(e){let t=new Proxy({},{ownKeys:()=>Array.from(new Set(e.flatMap(r=>Object.keys(r)))),has:(r,n)=>e.some(i=>i.hasOwnProperty(n)),get:(r,n)=>(e.find(i=>{if(i.hasOwnProperty(n)){let o=Object.getOwnPropertyDescriptor(i,n);if(o.get&&o.get._x_alreadyBound||o.set&&o.set._x_alreadyBound)return!0;if((o.get||o.set)&&o.enumerable){let s=o.get,a=o.set,c=o;s=s&&s.bind(t),a=a&&a.bind(t),s&&(s._x_alreadyBound=!0),a&&(a._x_alreadyBound=!0),Object.defineProperty(i,n,{...c,get:s,set:a})}return!0}return!1})||{})[n],set:(r,n,i)=>{let o=e.find(s=>s.hasOwnProperty(n));return o?o[n]=i:e[e.length-1][n]=i,!0}});return t}function ye(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function be(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>un(n,i),s=>st(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function un(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function st(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),st(e[t[0]],t.slice(1),r)}}var Ht={};function x(e,t){Ht[e]=t}function te(e,t){return Object.entries(Ht).forEach(([r,n])=>{Object.defineProperty(e,`$${r}`,{get(){let[i,o]=at(t);return i={interceptor:be,...i},_e(t,o),n(t,i)},enumerable:!1})}),e}function qt(e,t,r,...n){try{return r(...n)}catch(i){J(i,e,t)}}function J(e,t,r=void 0){Object.assign(e,{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} 2 | 3 | ${r?'Expression: "'+r+`" 4 | 5 | `:""}`,t),setTimeout(()=>{throw e},0)}var ve=!0;function Ut(e){let t=ve;ve=!1,e(),ve=t}function k(e,t,r={}){let n;return _(e,t)(i=>n=i,r),n}function _(...e){return Wt(...e)}var Wt=ct;function Gt(e){Wt=e}function ct(e,t){let r={};te(r,e);let n=[r,...N(e)];if(typeof t=="function")return fn(n,t);let i=dn(n,t,e);return qt.bind(null,e,t,i)}function fn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(P([n,...e]),i);we(r,o)}}var lt={};function pn(e,t){if(lt[e])return lt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e)||/^(let|const)\s/.test(e)?`(() => { ${e} })()`:e,o=(()=>{try{return new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`)}catch(s){return J(s,t,e),Promise.resolve()}})();return lt[e]=o,o}function dn(e,t,r){let n=pn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=P([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>J(l,r,t));n.finished?(we(i,n.result,a,s,r),n.result=void 0):c.then(l=>{we(i,l,a,s,r)}).catch(l=>J(l,r,t)).finally(()=>n.result=void 0)}}}function we(e,t,r,n,i){if(ve&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>we(e,s,r,n)).catch(s=>J(s,i,t)):e(o)}else e(t)}var ut="x-";function E(e=""){return ut+e}function Yt(e){ut=e}var Jt={};function d(e,t){Jt[e]=t}function re(e,t,r){let n={};return Array.from(t).map(Zt((o,s)=>n[o]=s)).filter(Qt).map(hn(n,r)).sort(_n).map(o=>mn(e,o))}function Xt(e){return Array.from(e).map(Zt()).filter(t=>!Qt(t))}var ft=!1,ne=new Map,er=Symbol();function tr(e){ft=!0;let t=Symbol();er=t,ne.set(t,[]);let r=()=>{for(;ne.get(t).length;)ne.get(t).shift()();ne.delete(t)},n=()=>{ft=!1,r()};e(r),n()}function at(e){let t=[],r=a=>t.push(a),[n,i]=Dt(e);return t.push(i),[{Alpine:I,effect:n,cleanup:r,evaluateLater:_.bind(_,e),evaluate:k.bind(k,e)},()=>t.forEach(a=>a())]}function mn(e,t){let r=()=>{},n=Jt[t.type]||r,[i,o]=at(e);Bt(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),ft?ne.get(er).push(n):n())};return s.runCleanups=o,s}var Ee=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),Se=e=>e;function Zt(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=rr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var rr=[];function Z(e){rr.push(e)}function Qt({name:e}){return nr().test(e)}var nr=()=>new RegExp(`^${ut}([^:^.]+)\\b`);function hn(e,t){return({name:r,value:n})=>{let i=r.match(nr()),o=r.match(/:([a-zA-Z0-9\-:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var dt="DEFAULT",Ae=["ignore","ref","data","id","bind","init","for","mask","model","modelable","transition","show","if",dt,"teleport","element"];function _n(e,t){let r=Ae.indexOf(e.type)===-1?dt:e.type,n=Ae.indexOf(t.type)===-1?dt:t.type;return Ae.indexOf(r)-Ae.indexOf(n)}function B(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}var pt=[],mt=!1;function Te(e=()=>{}){return queueMicrotask(()=>{mt||setTimeout(()=>{Oe()})}),new Promise(t=>{pt.push(()=>{e(),t()})})}function Oe(){for(mt=!1;pt.length;)pt.shift()()}function ir(){mt=!0}function R(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>R(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)R(n,t,!1),n=n.nextElementSibling}function O(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}function sr(){document.body||O("Unable to initialize. Trying to load Alpine before `` is available. Did you forget to add `defer` in Alpine's `