I'm loading a form using django in a modal (using htmx). I'm using a little vanilla javascript to load formsets dynamically as seen in this tutorial Dynamically Add Forms in Django with Formsets and JavaScript. Everything is working fine when you first load the modal but return an error when you load the modal a second time. After form saving I forced a complete reload of the page (not ideal as I want to refresh only a partial) but the problem is there when you load the modal. From what I understand (very little about JS) the problem is you cannot declare a variable more than 1 time using let (you can do let let ingredientForm = ... and only ingredientForm the second time) but obviously is not possible in my case as the script is loaded every time I fire the modal. Any solution?
views.py
@login_required
def recipe_create(request):
user = request.user
template = "recipes/partials/_recipe_create.html"
if request.method == "POST":
form = RecipeForm(request.POST)
formset = IngredientFormSet(request.POST)
if form.is_valid() and formset.is_valid():
recipe = form.save(commit=False)
recipe.creator = user
recipe.save()
for form in formset:
if form.cleaned_data:
name = form.cleaned_data["name"]
ingredient, created = Ingredient.objects.get_or_create(name=name)
RecipeIngredient.objects.get_or_create(
ingredient=ingredient,
recipe=recipe,
)
return HttpResponse(status=204, headers={"HX-Redirect": reverse_lazy("recipes:recipe_list")})
form = RecipeForm(request.POST)
context = {"recipe_form": form, "ingredient_formset": formset}
return render(request, template, context)
template.html
{% extends 'base.html' %}
{% load extra_filters thumbnail %}
{% block page_title %}Ricette{% endblock page_title %}
{% block content %}
<div id="modal" class="modal fade show">
<div id="dialog" class="modal-dialog modal-xl modal-fullscreen-md-down" hx-target="this">
</div>
</div>
<div id="main" class="container my-5 px-3 px-sm-0">
<div class="d-md-flex align-items-center text-center justify-content-between">
<div class="d-md-flex align-items-center">
<h1 class="me-4">Ricette</h1>
<button class="btn btn-outline-secondary btn-sm d-none d-md-inline {% if user.is_anonymous %}disabled{% endif %}"
hx-get="{% url 'recipes:recipe_create' %}"
hx-target="#dialog"
hx-trigger="click">
Aggiungi Ricetta
</button>
</div>
<form class="d-md-flex d-none" role="search">
<input name="recipe-search" class="form-control me-2" placeholder="Cerca Ricetta" aria-label="Search"
hx-post="{% url 'recipes:recipe_search' %}" hx-trigger="keyup changed delay:500ms" hx-target="#recipe-list" hx-swap="outerHTML">
</form>
</div>
{% block recipe_create %}
<div id="recipe-create" class="mb-4">
<a href="#" class="btn btn-outline-secondary btn-sm d-block d-md-none {% if user.is_anonymous %}disabled{% endif %}"
hx-get="{% url 'recipes:recipe_create' %}"
hx-target="#dialog"
hx-trigger="click">Aggiungi Ricetta</a>
<form class="d-flex d-md-none mt-3" role="search">
<input name="recipe-search" class="form-control me-2" placeholder="Cerca Ricetta" aria-label="Search"
hx-post="{% url 'recipes:recipe_search' %}" hx-trigger="keyup changed delay:500ms" hx-target="#recipe-list" hx-swap="outerHTML">
</form>
</div>
{% endblock recipe_create %}
<hr>
{% block recipes_list %}
{% if page_obj %}
<section id="recipe-list" hx-trigger="recipeSaved from:body"
hx-get="{% url 'recipes:recipe_list' %}">
<!-- RECIPES LIST -->
<table class="table">
<thead>
<tr>
<th scope="col">Ricetta</th>
<th scope="col">Ingredienti</th>
<th scope="col">Categoria</th>
<th scope="col">Tag</th>
</tr>
</thead>
<tbody>
{% for recipe in page_obj %}
<tr>
<td class="align-middle">
<a href="{% url 'recipes:recipe_detail' recipe.id recipe.slug %}">{{ recipe.title|title}}</a>
</td>
<td class="align-middle">{% include 'includes/ingredient_list_comma.html' %}</td>
<td>
<img src="{{ recipe.category.image|thumbnail_url:'thumbnail' }}" alt="{{ recipe.category.name }}" class="tag-icon">
<a href="#">{{ recipe.category.name }}</a>
</td>
<td class="align-middle">
{% for tag in recipe.tags.all %}
<img src="{{ tag.icon.url }}" alt="{{ tag.label }}" class="tag-icon">
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- END RECIPES LIST -->
<!-- PAGINATION -->
<nav aria-label="Recipes navigation" class="d-flex justify-content-md-end justify-content-center my-4">
{% if page_obj.paginator.num_pages > 1 %}
<ul class="pagination pagination-sm me-5">
{% if page_obj.has_previous %}
<li class="page-item">
<a href="?page=1" aria-label="First" class="page-link">
<span aria-hidden="true">«</span>
</a>
</li>
<li class="page-item">
<a href="?page={{ page_obj.previous_page_number }}" aria-label="Previous" class="page-link">
<span aria-hidden="true">‹</span>
</a>
</li>
{% endif %}
{% for page_number in page_obj.paginator.page_range %}
{% if page_number == page_obj.number %}
<li class="page-item"><a class="page-link active" href="?page={{ page_number }}">{{ page_number }}</a></li>
{% else %}
<li class="page-item"><a class="page-link" href="?page={{ page_number }}">{{ page_number }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
<span aria-hidden="true">›</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
<span aria-hidden="true">»</span>
</a>
</li>
{% endif %}
</ul>
{% endif %}
{% if number_of_recipes >= 10 %}
<ul class="pagination pagination-sm">
<li class="page-item">
<a href="?items=5" hx-boost="true" hx-target="#recipe-list" class="page-link {% if selected_number_of_items == '5' %}active{% endif %}">5</a>
</li>
<li class="page-item">
<a href="?items=10" hx-boost="true" hx-target="#recipe-list" class="page-link {% if selected_number_of_items == '10' %}active{% endif %}">10</a>
</li>
<li class="page-item">
<a href="?items=25" hx-boost="true" hx-target="#recipe-list" class="page-link {% if selected_number_of_items == '25' %}active{% endif %}">25</a>
</li>
</ul>
{% endif %}
</nav>
<!-- END PAGINATION -->
</section>
{% else %}
<p class="text-center fst-italic my-5">Nessuna ricetta presente del database</p>
{% endif %}
{% endblock recipes_list %}
</div>
{% endblock content %}
partial_template.html
{% load crispy_forms_tags %}
<div id="dialog" class="modal-dialog modal-xl modal-fullscreen-md-down" hx-target="this">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">Crea la tua ricetta</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="form-container" hx-post="{% url 'recipes:recipe_create' %}" hx-target="this">
{% crispy recipe_form %}
<hr class="text-black-50">
<div class="d-flex align-items-baseline mb-3 ">
<h5 class="flex-grow-1">Ingredienti</h5>
<button id="add-form" type="button" class="btn btn-sm btn-success me-2">
<i class="bi bi-plus-circle-fill me-2"></i>Ingrediente
</button>
<button id="delete-form" type="button" class="btn btn-sm btn-danger">
<i class="bi bi-dash-circle-fill me-2"></i>Ingrediente
</button>
</div>
<div id="ingredient-form-list">
{{ ingredient_formset.management_form }}
{% for formset in ingredient_formset %}
<div class="ingredient-form">
{% crispy formset %}
</div>
{% endfor %}
</div>
<div class="text-end">
<button class="btn btn-primary" type="submit">Salva</button>
<button class="btn btn-danger" data-bs-dismiss="modal">Cancella</button>
</div>
</form>
</div>
</div>
</div>
<script>
let ingredientForm = document.querySelectorAll(".ingredient-form")
let ingredientFormList = document.querySelector('#ingredient-form-list')
let container = document.querySelector("#form-container")
let addButton = document.querySelector("#add-form")
let deleteButton = document.querySelector("#delete-form")
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")
let formNum = ingredientForm.length-1 // Get the number of the last form on the page with zero-based indexing
checkDisableState(deleteButton) //Disable deleteButton when only initial formset is present
addButton.addEventListener('click', addForm)
function addForm(e) {
e.preventDefault()
let newForm = ingredientForm[0].cloneNode(true) //Clone the ingredient form
let formRegex = RegExp(`form-(\\d){1}-`,'g') //Regex to find all instances of the form number
formNum++ //Increment the form number
newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`) //Update the new form to have the correct form number
ingredientFormList.append(newForm) //Insert the new form at the end of the list of forms
totalForms.setAttribute('value', `${formNum+1}`) //Increment the number of total forms in the management form
checkDisableState(deleteButton) //Disable deleteButton when only initial formset is present
}
deleteButton.addEventListener('click', deleteForm)
function deleteForm(e) {
e.preventDefault()
let lastForm = ingredientFormList.lastChild //Get the last form of the list
lastForm.remove()
formNum--
totalForms.setAttribute('value', `${formNum+1}`) //Decrement the number of total forms in the management form
checkDisableState(deleteButton) //Disable deleteButton when only initial formset is present
}
function checkDisableState(e) {
if (formNum == 0)
e.setAttribute('disabled', '')
else
e.disabled = false
}
</script>
custom.js(for populating the modal using htmx)
const modal = new bootstrap.Modal(document.getElementById("modal"))
htmx.on("htmx:afterSwap", (e) => {
// Response targeting #dialog => show the modal
if (e.detail.target.id == "dialog") {
modal.show()
}
})
htmx.on("htmx:beforeSwap", (e) => {
// Empty response targeting #dialog => hide the modal
if (e.detail.target.id == "dialog" && !e.detail.xhr.response) {
modal.hide()
e.detail.shouldSwap = false
}
})
htmx.on("hidden.bs.modal", () => {
document.getElementById("dialog").innerHTML = ""
})
error when getting the modal fired up a second time (so the dynamic form adding is not working anymore)
Uncaught SyntaxError: Identifier 'ingredientForm' has already been declared
at ot (htmx.min.js:1:19317)
at at (htmx.min.js:1:19410)
at htmx.min.js:1:8026
at htmx.min.js:1:34917
at W (htmx.min.js:1:3851)
at a (htmx.min.js:1:34893)
I want to be able to use the script every time I open the modal, not only on the first time. Is there any way to reset variables when closing the modal?