A Django plugin for creating AJAX driven forms in Bootstrap modal.
Install
django-bootstrap-modal-forms
:$ pip install django-bootstrap-modal-forms
Add
bootstrap_modal_forms
to your INSTALLED_APPS in settings.py:INSTALLED_APPS = [ ... 'bootstrap_modal_forms', ... ]
- Include Bootstrap, jQuery and
jquery.bootstrap.modal.forms.js
on every page where you would like to set up the AJAX driven Django forms in Bootstrap modal.
IMPORTANT: Adjust Bootstrap and jQuery file paths to match yours, but include jquery.bootstrap.modal.forms.js
exactly as in code bellow.
<head>
<link rel="stylesheet" href="{% static 'assets/css/bootstrap.css' %}">
</head>
<body>
<script src="{% static 'assets/js/bootstrap.js' %}"></script>
<script src="{% static 'assets/js/jquery.js' %}"></script>
<script src="{% static 'js/jquery.bootstrap.modal.forms.js' %}"></script>
<!-- You can alternatively load the minified version -->
<script src="{% static 'js/jquery.bootstrap.modal.forms.min.js' %}"></script>
</body>
- Click event on html element instantiated with
modalForm
opens modal - Form at
formURL
is appended to the modal - On submit the form is POSTed via AJAX request to
formURL
- Unsuccessful POST request returns errors, which are shown in modal
- Successful POST request submits the form and redirects to
success_url
and showssuccess_message
, which are both defined in related Django view
Define BookModelForm and inherit built-in form BSModalModelForm
.
Define form's html and save it as Django template.
- Bootstrap 4 modal elements are used in this example.
- Button triggering the submission should have type attribute set to
"button"
and not"submit"
. - Add
class="submit-btn"
or customsubmitBtn
class (see paragraph Options) to this button. - Form will POST to
formURL
defined in #6. - Add
class="invalid"
or customerrorClass
(see paragraph Options) to the elements that wrap the fields. class="invalid"
acts as a flag for the fields having errors after the form has been POSTed.
book/create_book.html
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title">Create new Book</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
{% for field in form %}
<div class="form-group{% if field.errors %} invalid{% endif %}">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% for error in field.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="submit-btn btn btn-primary">Create</button>
</div>
</form>
Define a class-based view BookCreateView and inherit from built-in generic view BSModalCreateView
. BookCreateView processes the form defined in #1, uses the template defined in #2 and redirects to success_url
showing success_message
.
views.py
from django.urls import reverse_lazy
from .forms import BookModelForm
from .models import Book
from bootstrap_modal_forms.generic import BSModalCreateView
class BookCreateView(BSModalCreateView):
template_name = 'examples/create_book.html'
form_class = BookModelForm
success_message = 'Success: Book was created.'
success_url = reverse_lazy('index')
Define URL for the view in #3.
Define the Bootstrap modal window and html element triggering modal opening.
- Single modal can be used for multiple
modalForms
in single template (see #6). - When using multiple modals on the same page each modal should have unique
id
and the same value should also be set asmodalID
option when instantiatingmodalForm
on trigger element. - Trigger element (in this example button with
id="create-book"
) is used for instantiation ofmodalForm
in #6. - Any element can be trigger element as long as
modalForm
is bound to it. - Click event on trigger element loads form's html from #2 within
<div class="modal-content"></div>
and sets action attribute of the form toformURL
set in #6.
index.html
<div class="modal fade" tabindex="-1" role="dialog" id="modal">
<div class="modal-dialog" role="document">
<div class="modal-content"></div>
</div>
</div>
<!-- Create book button -->
<button id="create-book" class="btn btn-primary" type="button" name="button">Create book</button>
Add script to the template from #5 and bind the modalForm
to the trigger element. Set BookCreateView URL defined in #4 as formURL
property of modalForm
.
- If you want to create more modalForms in single template using the single modal window from #5, repeat steps #1 to #4, create new trigger element as in #5 and bind the new
modalForm
with unique URL to it. - Default values for
modalID
,modalContent
,modalForm
anderrorClass
are used in this example, whileformURL
is customized. If you customize any other option adjust the code of the above examples accordingly.
Set asyncUpdate
and asyncSettings
settings to create or update objects without page redirection to successUrl
and define whether a modal should close or stay opened after form submission. See comments in example below and paragraph modalForm options for explanation of asyncSettings
.
index.html
<!-- asyncSettings.dataElementId -->
<table id="books-table" class="table">
<thead>
...
</thead>
<tbody>
{% for book in books %}
<tr>
...
<!-- Update book buttons -->
<button type="button" class="update-book btn btn-sm btn-primary" data-form-url="{% url 'update_book' book.pk %}">
<span class="fa fa-pencil"></span>
</button>
...
</td>
</tr>
{% endfor %}
</tbody>
</table>
<script type="text/javascript">
$(function () {
...
# asyncSettings.successMessage
var asyncSuccessMessage = [
"<div ",
"style='position:fixed;top:0;z-index:10000;width:100%;border-radius:0;' ",
"class='alert alert-icon alert-success alert-dismissible fade show mb-0' role='alert'>",
"Success: Book was updated.",
"<button type='button' class='close' data-dismiss='alert' aria-label='Close'>",
"<span aria-hidden='true'>×</span>",
"</button>",
"</div>",
"<script>",
"$('.alert').fadeTo(2000, 500).slideUp(500, function () {$('.alert').slideUp(500).remove();});",
"<\/script>"
].join();
# asyncSettings.addModalFormFunction
function updateBookModalForm() {
$(".update-book").each(function () {
$(this).modalForm({
formURL: $(this).data("form-url"),
asyncUpdate: true,
asyncSettings: {
closeOnSubmit: false,
successMessage: asyncSuccessMessage
dataUrl: "books/",
dataElementId: "#books-table",
dataKey: "table",
addModalFormFunction: updateBookModalForm
}
});
});
}
updateBookModalForm();
...
});
</script>
views.py
from django.http import JsonResponse
from django.template.loader import render_to_string
from .models import Book
def books(request):
data = dict()
if request.method == 'GET':
books = Book.objects.all()
# asyncSettings.dataKey = 'table'
data['table'] = render_to_string(
'_books_table.html',
{'books': books},
request=request
)
return JsonResponse(data)
- modalID
Sets the custom id of the modal.
Default: "#modal"
- modalContent
Sets the custom class of the element to which the form's html is appended. If you change
modalContent
to the custom class, you should also changemodalForm
accordingly. To keep Bootstrap's modal style you should than copy Bootstrap's style formodal-content
and set it to your new modalContent class.Default: ".modal-content"
- modalForm
Sets the custom form selector.
Default: ".modal-content form"
- formURL
Sets the url of the form's view and html.
Default: null
- errorClass
Sets the custom class for the form fields having errors.
Default: ".invalid"
- submitBtn
Sets the custom class for the button triggering form submission in modal.
Default: ".submit-btn"
- asyncUpdate
Sets asynchronous content update after form submission.
Default: false
- asyncSettings.closeOnSubmit
Sets whether modal closes or not after form submission.
Default: false
- asyncSettings.successMessage
Sets successMessage shown after succesful for submission. Should be set to string defining message element. See
asyncSuccessMessage
example above.Default: null
- asyncSettings.dataUrl
Sets url of the view returning new queryset = all of the objects plus newly created or updated one after asynchronous update.
Default: null
- asyncSettings.dataElementId
Sets the
id
of the element which renders asynchronously updated queryset.Default: null
- asyncSettings.dataKey
Sets the key containing asynchronously updated queryset in the data dictionary returned from the view providing updated queryset.
Default: null
- asyncSettings.addModalFormFunction
Sets the method needed for reinstantiation of event listeners on button after asynchronous update.
Default: null
triggerElement.modalForm({
modalID: "#modal",
modalContent: ".modal-content",
modalForm: ".modal-content form",
formURL: null,
errorClass: ".invalid",
submitBtn: ".submit-btn",
asyncUpdate: false,
asyncSettings: {
closeOnSubmit: false,
successMessage: null,
dataUrl: null,
dataElementId: null,
dataKey: null,
addModalFormFunction: null
}
});
Import forms with from bootstrap_modal_forms.forms import BSModalForm
.
- BSModalForm
Inherits PopRequestMixin and Django's forms.Form.
- BSModalModelForm
Inherits PopRequestMixin, CreateUpdateAjaxMixin and Django's forms.ModelForm.
Import mixins with from bootstrap_modal_forms.mixins import PassRequestMixin
.
- PassRequestMixin
Puts the request into the form's kwargs.
- PopRequestMixin
Pops request out of the kwargs and attaches it to the form's instance.
- CreateUpdateAjaxMixin
Saves or doesn't save the object based on the request type.
- DeleteMessageMixin
Deletes object if request is not ajax request.
- LoginAjaxMixin
Authenticates user if request is not ajax request.
Import generic views with from bootstrap_modal_forms.generic import BSModalFormView
.
- BSModalFormView
Inherits PassRequestMixin and Django's generic.FormView.
- BSModalCreateView
Inherits PassRequestMixin and Django's SuccessMessageMixin and generic.CreateView.
- BSModalUpdateView
Inherits PassRequestMixin and Django's SuccessMessageMixin and generic.UpdateView.
- BSModalReadView
Inherits Django's generic.DetailView.
- BSModalDeleteView
Inherits DeleteMessageMixin and Django's generic.DeleteView.
To see django-bootstrap-modal-forms
in action clone the repository and run the examples locally:
$ git clone https://github.com/trco/django-bootstrap-modal-forms.git
$ cd django-bootstrap-modal-forms
$ pip install -r requirements.txt
$ python manage.py migrate
$ python manage.py runserver
Run unit and functional tests inside of project folder:
$ python manage.py test
For explanation how all the parts of the code work together see paragraph Usage. To test the working solution presented here clone and run Examples.
forms.py
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from bootstrap_modal_forms.mixins import PopRequestMixin, CreateUpdateAjaxMixin
class CustomUserCreationForm(PopRequestMixin, CreateUpdateAjaxMixin,
UserCreationForm):
class Meta:
model = User
fields = ['username', 'password1', 'password2']
signup.html
{% load widget_tweaks %}
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h3 class="modal-title">Sign up</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="{% if form.non_field_errors %}invalid{% endif %} mb-2">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class="form-control" placeholder=field.label %}
<div class="{% if field.errors %} invalid{% endif %}">
{% for error in field.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="submit-btn btn btn-primary">Sign up</button>
</div>
</form>
views.py
from django.urls import reverse_lazy
from bootstrap_modal_forms.generic import BSModalCreateView
from .forms import CustomUserCreationForm
class SignUpView(BSModalCreateView):
form_class = CustomUserCreationForm
template_name = 'examples/signup.html'
success_message = 'Success: Sign up succeeded. You can now Log in.'
success_url = reverse_lazy('index')
.html file containing modal, trigger element and script instantiating modalForm
<div class="modal fade" tabindex="-1" role="dialog" id="modal">
<div class="modal-dialog" role="document">
<div class="modal-content"></div>
</div>
</div>
<button id="signup-btn" class="btn btn-primary" type="button" name="button">Sign up</button>
<script type="text/javascript">
$(function () {
// Sign up button
$("#signup-btn").modalForm({
formURL: "{% url 'signup' %}"
});
});
</script>
For explanation how all the parts of the code work together see paragraph Usage. To test the working solution presented here clone and run Examples.
You can set the login redirection by setting the LOGIN_REDIRECT_URL
in settings.py
.
You can also set the custom login redirection by:
- Adding
success_url
to theextra_context
ofCustomLoginView
- Setting this
success_url
variable as a value of thehidden input field
withname="next"
within the Login form html
login.html
{% load widget_tweaks %}
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h3 class="modal-title">Log in</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="{% if form.non_field_errors %}invalid{% endif %} mb-2">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class="form-control" placeholder=field.label %}
<div class="{% if field.errors %} invalid{% endif %}">
{% for error in field.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
</div>
{% endfor %}
<!-- Hidden input field for custom redirection after successful login -->
<input type="hidden" name="next" value="{{ success_url }}">
</div>
<div class="modal-footer">
<button type="button" class="submit-btn btn btn-primary">Log in</button>
</div>
</form>
views.py
from django.urls import reverse_lazy
from bootstrap_modal_forms.generic import BSModalLoginView
from .forms import CustomAuthenticationForm
class CustomLoginView(BSModalLoginView):
authentication_form = CustomAuthenticationForm
template_name = 'examples/login.html'
success_message = 'Success: You were successfully logged in.'
extra_context = dict(success_url=reverse_lazy('index'))
.html file containing modal, trigger element and script instantiating modalForm
<div class="modal fade" tabindex="-1" role="dialog" id="modal">
<div class="modal-dialog" role="document">
<div class="modal-content"></div>
</div>
</div>
<button id="login-btn" class="btn btn-primary" type="button" name="button">Sign up</button>
<script type="text/javascript">
$(function () {
// Log in button
$("#login-btn").modalForm({
formURL: "{% url 'login' %}"
});
});
</script>
For explanation how all the parts of the code work together see paragraph Usage. To test the working solution presented here clone and run Examples.
create_book.html
{% load widget_tweaks %}
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h3 class="modal-title">Create Book</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="{% if form.non_field_errors %}invalid{% endif %} mb-2">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class="form-control" placeholder=field.label %}
<div class="{% if field.errors %} invalid{% endif %}">
{% for error in field.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="submit-btn btn btn-primary">Create</button>
</div>
</form>
update_book.html
{% load widget_tweaks %}
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h3 class="modal-title">Update Book</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="{% if form.non_field_errors %}invalid{% endif %} mb-2">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class="form-control" placeholder=field.label %}
<div class="{% if field.errors %} invalid{% endif %}">
{% for error in field.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="submit-btn btn btn-primary">Update</button>
</div>
</form>
read_book.html
{% load widget_tweaks %}
<div class="modal-header">
<h3 class="modal-title">Book details</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="">
Title: {{ book.title }}
</div>
<div class="">
Author: {{ book.author }}
</div>
<div class="">
Price: {{ book.price }} €
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
{% load widget_tweaks %}
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h3 class="modal-title">Delete Book</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<p>Are you sure you want to delete book with title
<strong>{{ book.title }}</strong>?</p>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-danger">Delete</button>
</div>
</form>
views.py
from django.urls import reverse_lazy
from django.views import generic
from .forms import BookModelForm
from .models import Book
from bootstrap_modal_forms.generic import (
BSModalCreateView,
BSModalUpdateView,
BSModalReadView,
BSModalDeleteView
)
class Index(generic.ListView):
model = Book
context_object_name = 'books'
template_name = 'index.html'
# Create
class BookCreateView(BSModalCreateView):
template_name = 'examples/create_book.html'
form_class = BookModelForm
success_message = 'Success: Book was created.'
success_url = reverse_lazy('index')
# Update
class BookUpdateView(BSModalUpdateView):
model = Book
template_name = 'examples/update_book.html'
form_class = BookModelForm
success_message = 'Success: Book was updated.'
success_url = reverse_lazy('index')
# Read
class BookReadView(BSModalReadView):
model = Book
template_name = 'examples/read_book.html'
# Delete
class BookDeleteView(BSModalDeleteView):
model = Book
template_name = 'examples/delete_book.html'
success_message = 'Success: Book was deleted.'
success_url = reverse_lazy('index')
urls.py
from django.urls import path
from books import views
urlpatterns = [
path('', views.Index.as_view(), name='index'),
path('create/', views.BookCreateView.as_view(), name='create_book'),
path('update/<int:pk>', views.BookUpdateView.as_view(), name='update_book'),
path('read/<int:pk>', views.BookReadView.as_view(), name='read_book'),
path('delete/<int:pk>', views.BookDeleteView.as_view(), name='delete_book')
]
.html file containing modal, trigger elements and script instantiating modalForms
<!-- Modal 1 with id="create-book"-->
<div class="modal fade" id="create-modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
</div>
</div>
</div>
<!-- Modal 2 with id="modal" -->
<div class="modal fade" tabindex="-1" role="dialog" id="modal">
<div class="modal-dialog" role="document">
<div class="modal-content"></div>
</div>
</div>
<!-- Create book button -->
<button id="create-book" class="btn btn-primary" type="button" name="button">Create book</button>
{% for book in books %}
<div class="text-center">
<!-- Read book buttons -->
<button type="button" id="read-book" class="bs-modal btn btn-sm btn-primary" data-form-url="{% url 'read_book' book.pk %}">
<span class="fa fa-eye"></span>
</button>
<!-- Update book buttons -->
<button type="button" id="update-book" class="bs-modal btn btn-sm btn-primary" data-form-url="{% url 'update_book' book.pk %}">
<span class="fa fa-pencil"></span>
</button>
<!-- Delete book buttons -->
<button type="button" id="delete-book" class="bs-modal btn btn-sm btn-danger" data-form-url="{% url 'delete_book' book.pk %}">
<span class="fa fa-trash"></span>
</button>
</div>
{% endfor %}
<script type="text/javascript">
$(function () {
// Update, Read and Delete book buttons open modal with id="modal" (default)
// The formURL is retrieved from the data of the element
$(".bs-modal").each(function () {
$(this).modalForm({
formURL: $(this).data('form-url')
});
});
// Create book button opens form in modal with id="create-modal"
$("#create-book").modalForm({
formURL: "{% url 'create_book' %}",
modalID: "#create-modal"
});
});
</script>
- See the difference between button triggering Create action and buttons triggering Read, Update and Delete actions.
- Within the for loop in .html file the
data-form-url
attribute of each Update, Read and Delete button should be set to relevant URL with pk argument of the object to be updated, read or deleted. - These
data-form-url
URLs should than be set asformURLs
formodalForms
bound to the buttons.
For explanation how all the parts of the code work together see paragraph Usage. To test the working solution presented here clone and run Examples.
filter_book.html
{% load widget_tweaks %}
<form method="post" action="">
{% csrf_token %}
<div class="modal-header">
<h3 class="modal-title">Filter Books</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="{% if form.non_field_errors %}invalid{% endif %} mb-2">
{% for error in form.non_field_errors %}
{{ error }}
{% endfor %}
</div>
{% for field in form %}
<div class="form-group">
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
{% render_field field class="form-control" placeholder=field.label %}
<div class="{% if field.errors %} invalid{% endif %}">
{% for error in field.errors %}
<p class="help-block">{{ error }}</p>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<div class="modal-footer">
<input type="submit" class="btn btn-primary" name="clear" value="Clear"/>
<button type="button" class="submit-btn btn btn-primary">Filter</button>
</div>
</form>
views.py
class BookFilterView(BSModalFormView):
template_name = 'examples/filter_book.html'
form_class = BookFilterForm
def form_valid(self, form):
if 'clear' in self.request.POST:
self.filter = ''
else:
self.filter = '?type=' + form.cleaned_data['type']
response = super().form_valid(form)
return response
def get_success_url(self):
return reverse_lazy('index') + self.filter
index.html
...
<button id="filter-book" class="bs-modal btn btn-primary" type="button" name="button" data-form-url="{% url 'filter_book' %}">
<span class="fa fa-filter mr-2"></span>Filter books
</button>
...
<script type="text/javascript">
$(function () {
...
$(".bs-modal").each(function () {
$(this).modalForm({formURL: $(this).data('form-url')});
});
...
});
</script>
This is an Open Source project and any contribution is appreciated.
This project is licensed under the MIT License.