Правильный способ динамического добавления экземпляров Django formset и POST с помощью HTMX?
Я делаю форму с вложенным динамическим набором форм, используя htmx я (хочу избежать использования JS, но если нет выбора.... ), чтобы инстанцировать больше полей набора форм, чтобы сделать динамическую вложенную форму, однако когда я POST, только данные из 1 экземпляра Chlid formset
(последний) размещаются, остальная часть формы размещается правильно и Child model
получает связь с Parent model
Я прочитал документацию django о том, как POST экземпляры formset и попытался применить это в своем коде, также я понял, как POST одновременно Parent
и Child
. Для наборов форм я делаю htmx get запрос hx-get к частичному шаблону, который содержит дочерний набор форм, и это работает отлично, единственная проблема в том, что это всегда возвращает form-0
набор форм на сторону клиента, поэтому для POST данные повторяются x
раз для каждого поля и принимаются только данные, размещенные в последнем экземпляре, Однако я попытался изменить значение extra=int
в моем наборе форм, чтобы получить больше форм, это дало ожидаемый результат, один Child instance на форму в extra=int
, так что моя проблема в htmx и в том, как я вызываю new Child formset
экземпляры.
вот мой код. (я планирую вложить больше дочерних наборов форм внутри этой формы, поэтому для удобства я называю ее sformset)
****views.py****
def createPlan(request):#Requst for the Parent form
form = PlanForm(request.POST or None)
sformset = StructureFormset(request.POST or None) #Nesting the Child formset
context = {
'form':form,
'sformset':sformset,
}
if request.method == 'POST':
print(request.POST)
if form.is_valid() and sformset.is_valid():
plan = form.save(commit=False)
print(plan)
plan.save()
sform = sformset.save(commit=False)
for structure in sform:
structure.plan = plan
structure.save()
return render(request, 'app/plan_forms.html', context)
def addStructure(request):
sformset = StructureFormset(queryset=Structure.objects.none())#add a empty formset instance
context = {"sformset":sformset}
return render(request, 'app/formsets/structure_form.html', context)
****forms.py****
StructureFormset = modelformset_factory(Structure,
fields = (
'material_type',
'weight',
'thickness',
'provider'
))
****relevant part for plan_forms.html template****
<form method="POST">
{% csrf_token %}
<div class="col-12 px-2">
<div class="row px-3 py-1">
<div class="col-3 px-1">{{ form.format }}</div>
<div class="col-3 px-1">{{ form.pc }}</div>
<div class="col-3 px-1">{{ form.revission }}</div>
<div class="col-3 px-1">{{ form.rev_date }}</div>
</div>
<div class="row px-3 py-1">
<div class="col-3 px-1">{{ form.client }}</div>
<div class="col-3 px-1">{{ form.product }}</div>
<div class="col-3 px-1">{{ form.gp_code }}</div>
<div class="col-3 px-1">{{ form.code }}</div>
</div>
</div>
<div>
<table>
<tbody style="user-select: none;" id="structureforms" hx-sync="closest form:queue">
<!--Structure formset goes here-->
</tbody>
<tfoot>
<a href="" hx-get="{% url 'structure-form' %}" hx-swap="beforeend" hx-target="#structureforms">
Add structure <!--Button to call structure formset-->
</a>
</tfoot>
</table>
</div>
<div class="col-12 px-2">
<div class="row px-4 py-1">{{ form.observation }}</div>
<div class="row px-4 py-1">{{ form.continuation }}</div>
<div class="row px-4 py-1">{{ form.dispatch_conditions }}</div>
<div class="row px-3 py-1">
<div class="col-6 px-1">{{ form.elaborator }}</div>
<div class="col-6 px-1">{{ form.reviewer }}</div>
</div>
</div>
<button type="submit">Submit</button>
</form>
****formsets/structure_form.html****
<tr>
<td class="col-12 px-1">
{{ sformset }}
</td>
</tr>
**** relevant urls.py****
urlpatterns = [
path('create_plan/', views.createPlan, name='create_plan'),
path('htmx/structure-form/', views.addStructure, name='structure-form')]
Кроме того, форма, которую я построил в admin.py, используя fields и inlines, является именно тем, что я хочу получить в качестве исходного продукта (за исключением количества исходных наборов форм и стилей)
Кратко о проблеме: В настоящее время ваш код успешно добавляет новый набор форм, но каждый новый набор форм поставляется с атрибутом name
form-0-title
(то же самое для id
и других атрибутов). Кроме того, после добавления нового набора форм с помощью hx-get
скрытые поля, первоначально созданные ManagementForm
, больше не будут отражать количество наборов форм на странице.
Что нужно
После добавления нового набора форм на сайт, вот что, по моему мнению, должно произойти, чтобы Django смог обработать отправку формы.
Обновите атрибут
value
в элементе input с помощьюid="id_form-TOTAL_FORMS"
, чтобы число соответствовало фактическому количеству наборов форм на странице после того, какhx-get
введет новый набор форм.Обновить
name
иid
нового набора форм изform-0-title
, чтобы использовать любое число, отражающее текущее общее количество наборов форм.Обновите атрибуты меток
for
таким же образом.
Вы можете сделать это с помощью Javascript на стороне клиента. В качестве альтернативы, вы можете сделать практически то же самое с помощью Django на стороне сервера, и тогда htmx может быть единственным javascript, необходимым для всего остального. Для этого я использовал empty_form
для создания html-содержимого набора форм, которое можно изменять по мере необходимости. Эта работа показана в помощнике build_new_formset()
, ниже.
Пример
Вот что у меня работает:
forms.py
from django import forms
from django.forms import formset_factory
class BookForm(forms.Form):
title = forms.CharField()
author = forms.CharField()
BookFormSet = formset_factory(BookForm)
views.py
from django.utils.safestring import mark_safe
from app2.forms import BookFormSet
def formset_view(request):
template = 'formset.html'
if request.POST:
formset = BookFormSet(request.POST)
if formset.is_valid():
print(f">>>> form is valid. Request.post is {request.POST}")
return HttpResponseRedirect(reverse('app2:formset_view'))
else:
formset = BookFormSet()
return render(request, template, {'formset': formset})
def add_formset(request, current_total_formsets):
new_formset = build_new_formset(BookFormSet(), current_total_formsets)
context = {
'new_formset': new_formset,
'new_total_formsets': current_total_formsets + 1,
}
return render(request, 'formset_partial.html', context)
# Helper to build the needed formset
def build_new_formset(formset, new_total_formsets):
html = ""
for form in formset.empty_form:
html += form.label_tag().replace('__prefix__', str(new_total_formsets))
html += str(form).replace('__prefix__', str(new_total_formsets))
return mark_safe(html)
Примечание к помощнику build_new_formset()
: formset.empty_form
будет опускать номера индексов, которые должны идти в атрибутах id
, name
и label
, и вместо этого будет использовать "__prefix__"
. Вы хотите заменить эту часть "__prefix__"
на соответствующее число. Например, если это второй набор форм на странице, его id
должен быть id_form-1-title
(заменен с id_form-__prefix__-title
).
formset.html
<form action="{% url 'app2:formset_view' %}" method="post">
{% csrf_token %}
{{ formset.management_form }}
{% for form in formset %}
<p>{{ form }}</p>
{% endfor %}
<button type="button"
hx-trigger="click"
hx-get="{% url 'app2:add_formset' formset.total_form_count %}"
hx-swap="outerHTML">
Add formset
</button>
<input type="submit" value="Submit">
</form>
formset_partial.html
<input hx-swap-oob="true"
type="hidden"
name="form-TOTAL_FORMS"
value="{{ new_total_formsets }}"
id="id_form-TOTAL_FORMS">
<p>{{ new_formset }}</p>
<button type="button"
hx-trigger="click"
hx-get="{% url 'app2:add_formset' new_total_formsets %}"
hx-swap="outerHTML">
Add formset
</button>
Примечание по поводу скрытого input
: С каждым новым добавленным набором форм, value
элемента input
, который имеет id="id_form-TOTAL_FORMS"
, больше не будет отражать фактическое количество наборов форм на странице. Вы можете отправить новый скрытый input
с вашим набором форм и включить в него hx-swap-oob="true"
. Htmx затем заменит старый на новый
Ссылка на документы: https://docs.djangoproject.com/en/4.1/topics/forms/formsets/
Мой подход к работе с динамическими наборами форм с помощью могущественного HTMX
:
Вид, который отображает empty_form
экземпляра набора форм (т.е. formset.empty_form
). Это вернет форму с __prefix__
в той части, которая обычно содержит номер формы (т. е. вместо будет form-0-field_name
form-__prefix__-field_name
). Хитрость заключается в том, чтобы просто заменить __prefix__
на соответствующий номер формы.
@В ответе Мэтта это достигается путем итерации по форме и выполнения соответствующей замены. Хотя это работает хорошо, я думаю, что еще более простой способ - просто изменить префикс набора форм после его инстанцирования. Таким образом, вы не будете использовать вспомогательную функцию build_new_formset
.
На стороне клиента:
Рендеринг sformset.management_form
в plan_forms.html
. И включите значение form-TOTAL_FORMS
[скрытого поля ввода] набора форм в GET-запрос HTMX с помощью атрибута hx-vals
(пожалуйста, обратите внимание, что я использую селектор jquery, это просто для удобства. Ванильный JS тоже должен работать).
plan_forms.html
<form method="POST">
{% csrf_token %}
{{sformset.management_form}}
...
<tbody style="user-select: none;" id="structureforms" hx-sync="closest form:queue">
<!--Structure formset goes here-->
</tbody>
<tfoot>
<button hx-get="{% url 'structure-form' %}"
hx-swap="beforeend"
hx-target="#structureforms"
hx-get="{% url 'structure-form' %}"
hx-vals='js:{totalForms: $("#id_form-TOTAL_FORMS").val()}' \\ New>
Add structure <!--Button to call structure formset-->
</button>
</tfoot>
...
</form>
После запроса htmx необходимо обновить общее количество форм в наборе форм (увеличивается при каждом добавлении набора форм). Если вы хотите строго использовать HTMX, вы можете использовать атрибут hx-on
, который позволяет слушать события и запускать inline JS. Однако я нахожу hyperscript
более удобным для этого атрибут , поскольку он является командой increment
.
plan_forms.html
<form method="POST">
...
<tbody style="user-select: none;"
id="structureforms"
hx-sync="closest form:queue"
_="on htmx:afterSettle increment #id_form-TOTAL_FORMS's value">
<!--Structure formset goes here-->
</tbody>
...
</form>
Затем вы можете обработать запрос в своем представлении: views.py
...
def createPlan(request): #Requst for the Parent form
form = PlanForm(request.POST or None)
sformset = StructureFormset(request.POST or None) #Nesting the Child formset
...
def addStructure(request):
sformset = StructureFormset(queryset=Structure.objects.none()).empty_form # Creates an empty formset
form_number = int(request.GET.get("totalForms")) - 1 # forms are numbered from 0, so they will always be 1 less than formset's total forms.
sformset.prefix = sformset.prefix.replace("__prefix__", str(form_number)) # override the formset's prefix, replacing the __prefix__
context = {"sformset":sformset}
return render(request, 'app/formsets/structure_form.html', context)
Я думаю, что это практически все.
Notes
Вместо hx-vals
можно использовать hx-include
...
hx-include='[name="form-TOTAL_FORMS"]'
...
А в представлении addStructure
вам придется получить значение с помощью request.GET.get("form0TOTAL_FORMS")