Динамическое добавление форм в Django с помощью наборов форм и JavaScript

Этот учебник демонстрирует, как несколько копий формы могут быть динамически добавлены на страницу и обработаны с помощью наборов форм Django и JavaScript.

В веб-приложении пользователю может потребоваться отправить одну и ту же форму несколько раз подряд, если он вводит данные для добавления объектов в базу данных. Вместо того, чтобы отправлять одну и ту же форму снова и снова, Django позволяет нам добавить несколько копий одной и той же формы на веб-страницу с помощью наборов форм.

Мы продемонстрируем это на примере приложения для наблюдения за птицами, позволяющего отслеживать птиц, которых видел пользователь. Оно будет включать всего две страницы: страницу со списком птиц и страницу с формой для добавления птиц в список. Вы можете посмотреть готовый исходный код этого примера на GitHub.

Для этого учебника полезно иметь базовое представление о:

  • Django формы
  • Django представления на основе классов
  • JavaScript манипуляция DOM

Настройка

Модель

В приложении будет только одна модель, Bird, которая будет хранить информацию о каждой птице, которую мы видели.

# models.py
from django.db import models

class Bird(models.Model):
    common_name = models.CharField(max_length=250)
    scientific_name = models.CharField(max_length=250)
    
    def __str__(self):
      return self.common_name

Модель состоит из двух полей CharFields для общего названия и научного названия птицы

URLs

Мы будем управлять URL-адресами с помощью файла urls.py уровня проекта, настроенного на включение файла urls.py уровня приложения.

# project level urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('birds.urls')),
]

На уровне приложения urls.py будет содержать пути к двум страницам приложения.

# app level urls.py
from django.urls import path
from .views import BirdAddView, BirdListView

urlpatterns = [
    path('add', BirdAddView.as_view(), name="add_bird"),
    path('', BirdListView.as_view(), name="bird_list")
]

Виды

Изначально мы настроим представление только для страницы со списком птиц. Это представление будет использовать общий ListView, чтобы получить все экземпляры птиц и сделать их доступными для отображения в указанном шаблоне.

# views.py
from django.views.generic import ListView
from .models import Bird

class BirdListView(ListView):
    model = Bird
    template_name = "bird_list.html"

Позже мы добавим представление для страницы с формой.

Formsets

На данном этапе мы могли бы создать форму и представление, которое предоставило бы форму шаблону, чтобы пользователь мог добавить новую птицу в список. Но пользователь сможет добавить только одну птицу за один раз. Чтобы добавить несколько птиц за один раз, нам нужно использовать набор форм вместо обычной формы.

Наборы форм позволяют нам иметь несколько копий одной и той же формы на одной странице. Это полезно, если пользователь должен иметь возможность отправить несколько форм одновременно для создания нескольких экземпляров модели. В нашем примере это позволит пользователю добавить несколько птиц в свой список за один раз без необходимости отправлять форму для каждой отдельной птицы.

Модельные наборы форм

Наборы форм могут быть созданы для различных типов форм, включая формы модели. Поскольку мы хотим создавать новые экземпляры модели Bird, набор форм модели является естественным вариантом.

Если бы мы просто хотели добавить одну форму модели на нашу страницу, мы бы создали ее следующим образом:

# forms.py
from django.forms import ModelForm
from .models import Bird

# A regular form, not a formset
class BirdForm(ModelForm):
    class Meta:
      model = Bird
      fields = [common_name, scientific_name]

Форма модели требует, чтобы мы указали модель, с которой мы хотим связать форму, и поля, которые мы хотим включить в форму.

Для создания набора форм модели нам не нужно определять форму модели вообще. Вместо этого мы используем функцию Django's modelformset_factory(), которая возвращает класс форм для данной модели.

# forms.py
from django.forms import modelformset_factory
from .models import Bird

BirdFormSet = modelformset_factory(
    Bird, fields=("common_name", "scientific_name"), extra=1
)

modelformset_factory() требует, чтобы первым аргументом была модель, для которой создается набор форм. После указания модели можно указать различные опциональные аргументы. Мы будем использовать два необязательных аргумента - fields и extra.

  • fields - задает поля, которые будут отображаться в форме
  • extra - количество форм для первоначального отображения на странице

Установив extra в 1, мы будем изначально передавать шаблону только одну форму. Один - это значение по умолчанию, но мы явно укажем его для ясности. Если установить extra в любое другое число, шаблон получит это количество форм.

Extra позволяет нам отображать пользователю несколько копий формы, но мы можем не знать, сколько птиц пользователь хочет добавить за один раз. Если мы покажем слишком мало форм, то пользователю все равно придется отправлять форму несколько раз. Если мы покажем слишком много форм, пользователю, возможно, придется прокрутить страницу далеко вниз, чтобы найти кнопку отправки. К счастью, задавая дополнительные параметры, мы не ограничиваем себя только этим количеством копий формы. Мы можем добавить на страницу столько копий формы, сколько нам реально необходимо (до 1000 копий), даже если значение параметра extra равно 1. Мы можем добавить другие копии формы на страницу динамически с помощью JavaScript. Мы сделаем это, когда создадим наш шаблон для отображения формы. Перед этим нам нужно создать представление.

View

Нам нужно представление, которое может обрабатывать GET-запрос для первоначального отображения формы и POST-запрос, когда форма отправлена. Мы будем использовать класс TemplateView для базовой функциональности представления и добавим методы для GET и POST запросов.

Запрос GET требует, чтобы мы создали экземпляр набора форм и добавили его в контекст.

# views.py
from django.views.generic import ListView, TemplateView # Import TemplateView
from .models import Bird
from .forms import BirdFormSet # Import the formset

class BirdListView(ListView):
    model = Bird
    template_name = "bird_list.html"

# View for adding birds
class BirdAddView(TemplateView):
    template_name = "add_bird.html"

    # Define method to handle GET request
    def get(self, *args, **kwargs):
        # Create an instance of the formset
        formset = BirdFormSet(queryset=Bird.objects.none())
        return self.render_to_response({'bird_formset': formset})

Чтобы создать экземпляр набора форм, мы сначала импортируем набор форм, а затем вызываем его в методе get. Если мы просто вызовем набор форм без аргументов, мы получим набор форм, который содержит форму для всех экземпляров птиц в базе данных. Поскольку мы хотим, чтобы это представление добавляло только новых птиц, нам нужно предотвратить предварительное заполнение отображаемых форм экземплярами птиц. Для этого мы используем специальный пользовательский набор queryset. В качестве аргумента queryset мы зададим значение Bird.objects.none(), что создаст пустой queryset. Таким образом, никакие птицы не будут предварительно заполнены в формах.

После создания экземпляра набора форм мы вызываем render_to_response с аргументом в виде словаря, в котором набор форм назначен ключу bird_formset.

Для запроса POST нам нужно определить метод post, который обрабатывает форму, когда она отправлена.

# views.py
from django.views.generic import ListView, TemplateView
from .models import Bird
from .forms import BirdFormSet
from django.urls import reverse_lazy
from django.shortcuts import redirect

class BirdListView(ListView):
    model = Bird
    template_name = "bird_list.html"

class BirdAddView(TemplateView):
    template_name = "add_bird.html"

    def get(self, *args, **kwargs):
        formset = BirdFormSet(queryset=Bird.objects.none())
        return self.render_to_response({'bird_formset': formset})

    # Define method to handle POST request
    def post(self, *args, **kwargs):

        formset = BirdFormSet(data=self.request.POST)

        # Check if submitted forms are valid
        if formset.is_valid():
            formset.save()
            return redirect(reverse_lazy("bird_list"))

        return self.render_to_response({'bird_formset': formset})

Сначала мы создаем экземпляр BirdFormSet. На этот раз мы включаем отправленные данные из запроса через self.request.POST. Как только набор форм создан, мы должны проверить его. Подобно обычной форме, можно вызвать функцию is_valid() с набором форм для проверки отправленных форм и полей формы. Если набор форм валиден, то мы вызываем save() для набора форм, который создает новые объекты Bird и добавляет их в базу данных. После этого мы перенаправляем пользователя на страницу со списком птиц. Если набор форм недействителен, набор форм возвращается пользователю с соответствующими сообщениями об ошибках.

Шаблон

Теперь, когда мы настроили представления, мы можем создать шаблоны, которые будут отображаться пользователю.

Чтобы отобразить список птиц, мы пройдем по объектному_списку, который предоставляется ListView и включает все экземпляры птиц в базе данных.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bird List</title>
</head>
<body>
    <h1>Bird List</h1>
    <a href='{% url "add_bird" %}'>Add bird</a>
    {% for bird in object_list %}
        <p>{{bird.common_name}}: {{bird.scientific_name}}</p>
    {% endfor %}
</body>
</html>

Мы отобразим общее название и научное название каждой птицы. и включим ссылку на страницу с формой для добавления птицы в список.

Шаблон для нашей страницы добавления птицы первоначально будет показывать только одну форму, которую мы указали с помощью extra.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add bird</title>
</head>
<body>
    <h1>Add a new bird</h1>
    <form id="form-container" method="POST">
        {% csrf_token %}
        {{bird_formset.management_form}}
        {% for form in bird_formset %}
        <div class="bird-form">
        {{form.as_p}}
        </div>
        {% endfor %}
        <button type="submit">Create Birds</button>
    </form>
    </body>
</html>

Создаем HTML <форму> с id form-container и методом POST. Как и в обычной форме, мы включаем csrf_token. В отличие от обычной формы, мы должны включить {{bird_formset.management_form}}. Это вставляет форму управления на страницу, которая включает скрытые входы, содержащие информацию о количестве отображаемых форм, и используется Django, когда форма отправлена.

<input type="hidden" name="form-TOTAL_FORMS" value="1" id="id_form-TOTAL_FORMS">
<input type="hidden" name="form-INITIAL_FORMS" value="0" id="id_form-INITIAL_FORMS">
<input type="hidden" name="form-MIN_NUM_FORMS" value="0" id="id_form-MIN_NUM_FORMS">
<input type="hidden" name="form-MAX_NUM_FORMS" value="1000" id="id_form-MAX_NUM_FORMS">

Вход form-TOTAL_FORMS содержит значение общего количества отправляемых форм. Если оно не совпадает с фактическим количеством форм, которые Django получает при отправке форм, будет выдана ошибка.

Формы в наборе форм добавляются на страницу с помощью цикла for для прохождения каждой формы в наборе форм и ее отображения. Мы выбрали отображение формы в тегах <p>, используя {{form.as_p}}. Формы отображаются в HTML следующим образом:

<p>
<label for="id_form-0-common_name">Common name:</label>
<input type="text" name="form-0-common_name" maxlength="250" id="id_form-0-common_name">
</p>
<p>
<label for="id_form-0-scientific_name">Scientific name:</label>
<input type="text" name="form-0-scientific_name" maxlength="250" id="id_form-0-scientific_name">
<input type="hidden" name="form-0-id" id="id_form-0-id">
</p>

Каждое поле в форме получает атрибут name и id, который содержит число и название поля. Поле для общего имени имеет атрибут name form-0-common_name и атрибут id id_form-0-common_name. Это важно, потому что номер в обоих атрибутах используется для идентификации формы, частью которой является поле. Для нашей единственной формы каждое поле является частью формы 0, поскольку нумерация форм начинается с 0.

Хотя этот шаблон будет работать и позволит пользователю отправить одну птицу за раз, он все же не позволит нам добавить больше форм, если пользователь захочет добавить более одной птицы. Мы могли бы добавить больше форм, установив extra на другое число. Вместо этого мы будем использовать JavaScript, чтобы позволить пользователю выбрать, сколько форм он хочет отправить.

Создание динамических наборов форм

Поскольку мы используем наборы форм и настроили наши представления для работы с набором форм вместо обычной формы, пользователи могут отправлять столько форм, сколько захотят за один раз. Нам просто нужно сделать эти формы доступными для них на странице. Это можно сделать, используя манипуляции с DOM с помощью JavaScript.

Добавление дополнительных форм требует использования JavaScript для:

  • Получите существующую форму со страницы
  • Создайте копию формы
  • Увеличить номер формы
  • Вставьте новую форму на страницу
  • Обновите количество общих форм в форме управления

Прежде чем перейти к JavaScript, добавим к HTML-форме еще одну кнопку, которая будет использоваться для добавления дополнительных форм на страницу.

<button id="add-form" type="button">Add Another Bird</button>

Эта кнопка будет добавлена непосредственно перед кнопкой "Создать птиц".

Для получения существующей формы на странице мы используем document.querySelectorAll('.bird-form'), где bird-form - это класс, присвоенный div, который содержит поля каждой формы. Хотя document.querySelectorAll('.bird-form') вернет список всех элементов на странице, имеющих класс bird-form, на данный момент у нас только одна форма на странице, поэтому он вернет список, включающий только одну форму. Мы будем использовать document.querySelector() для получения других элементов на странице, необходимых для выполнения всех шагов, а именно: всей HTML-формы, кнопки, которую пользователь может нажать, чтобы добавить новую форму на страницу, и общего ввода формы управления.

let birdForm = document.querySelectorAll(".bird-form")
let container = document.querySelector("#form-container")
let addButton = document.querySelector("#add-form")
let totalForms = document.querySelector("#id_form-TOTAL_FORMS")

Нам также нужно получить номер последней формы на странице. Опять же, поскольку мы знаем, что изначально на странице будет отображаться только одна форма, мы можем просто сказать, что последняя форма - это форма 0. Но в случае, если мы хотим, чтобы на странице изначально было более одной формы, мы можем найти ее, взяв количество форм, хранящихся в birdForm, и вычтя единицу, чтобы учесть нумерацию форм, начинающуюся с 0.

let formNum = birdForm.length-1 // Get the number of the last form on the page with zero-based indexing

Мы хотим, чтобы новая форма добавлялась на страницу только тогда, когда пользователь нажимает на кнопку "Добавить еще одну птицу". Это означает, что остальные шаги должны выполняться только при нажатии на кнопку. Нам нужно создать функцию, выполняющую остальные шаги, и прикрепить ее к нажатию кнопки с помощью слушателя событий. Мы назовем нашу функцию addForm, чтобы ее можно было связать с нажатием кнопки по

addButton.addEventListener('click', addForm)

Функция addForm будет выглядеть следующим образом

function addForm(e) {
        e.preventDefault()

        let newForm = birdForm[0].cloneNode(true) //Clone the bird 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
        container.insertBefore(newForm, addButton) //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
}

Сначала мы предотвращаем действие по умолчанию при нажатии на кнопку, чтобы выполнялась только наша функция addForm. Затем мы создаем новую форму, клонируя birdForm с помощью .cloneNode(). Мы передаем true в качестве аргумента, чтобы все дочерние узлы birdForm также были скопированы в newForm.

Поскольку мы создали копию, newForm включает точно такие же значения атрибутов, как и форма, уже находящаяся на странице. Это означает, что номера форм одинаковы. Поскольку мы не можем отправить две формы с одинаковым номером, нам нужно увеличить номер формы. Мы сделаем это, увеличив formNum, а затем используя регулярное выражение, чтобы найти и заменить все экземпляры номера формы в HTML newForm.

Просматривая HTML формы, мы видим, что все атрибуты, включающие номер формы, содержат общий шаблон form-0-. Мы создадим регулярное выражение для соответствия этому шаблону и сохраним его в formRegex.

Далее мы определим все экземпляры, соответствующие formRegex в HTML newForm, и заменим их новой строкой. Мы хотим, чтобы заменяющая строка была такой же, как и совпавший шаблон, а число теперь будет значением недавно увеличенного formNum. Получив доступ к HTML newForm с помощью .innerHTML, а затем используя .replace(), мы сможем подобрать регулярное выражение и выполнить замену.

Теперь, когда у нас есть правильный номер формы в newForm, мы можем добавить ее на страницу. Используем .insertBefore() и добавим новую форму перед addButton.

Теперь, когда форма размещена на странице, нам нужно убедиться, что форма управления правильно обновлена с правильным количеством форм, которые будут отправлены. Нам нужно увеличить значение скрытого поля form-TOTAL_FORMS. Мы уже сохранили это поле в totalForms. Чтобы обновить его, мы используем .setAttribute(), чтобы установить атрибут value на единицу больше, чем текущий номер формы. Мы делаем на единицу больше, потому что form-TOTAL_FORMS включает количество форм на странице, начиная с 1, а нумерация отдельных форм начинается с 0.

Теперь мы можем добавить весь JavaScript в тег <script> перед закрывающим тегом <body>, чтобы получить наш окончательный шаблон.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add bird</title>
</head>
<body>
    <h1>Add a new bird</h1>
    <form id="form-container" method="POST">
        {% csrf_token %}
        {{bird_formset.management_form}}
        {% for form in bird_formset %}
        <div class="bird-form">
        {{form.as_p}}
        </div>
        {% endfor %}
        <button id="add-form" type="button">Add Another Bird</button>
        <button type="submit">Create Birds</button>
    </form>
    

    <script>
        let birdForm = document.querySelectorAll(".bird-form")
        let container = document.querySelector("#form-container")
        let addButton = document.querySelector("#add-form")
        let totalForms = document.querySelector("#id_form-TOTAL_FORMS")

        let formNum = birdForm.length-1
        addButton.addEventListener('click', addForm)

        function addForm(e){
            e.preventDefault()

            let newForm = birdForm[0].cloneNode(true)
            let formRegex = RegExp(`form-(\\d){1}-`,'g')

            formNum++
            newForm.innerHTML = newForm.innerHTML.replace(formRegex, `form-${formNum}-`)
            container.insertBefore(newForm, addButton)
            
            totalForms.setAttribute('value', `${formNum+1}`)
        }
    </script>
    </body>
</html>

Вот и все! Теперь пользователь может добавить на страницу столько форм, сколько захочет, нажав кнопку "Добавить другую птицу", и когда форма будет отправлена, она будет сохранена в базе данных, пользователь будет перенаправлен обратно к списку птиц, и все недавно отправленные птицы будут отображены.

Итоги

Django formsets позволяет нам включать несколько копий одной и той же формы на одной странице и корректно обрабатывать их при отправке. Кроме того, мы можем позволить пользователю выбрать, сколько форм он хочет отправить, используя JavaScript для добавления дополнительных форм на страницу. Хотя в этом руководстве использовались наборы форм modelformsets, можно использовать и другие типы форм.

Полезные ресурсы

https://www.brennantymrak.com/articles/django-dynamic-formsets-javascript

Вернуться на верх