Agregar dinámicamente un formulario a un conjunto de formularios Django con Ajax


Quiero agregar automáticamente nuevos formularios a un conjunto de formularios de Django usando Ajax, de modo que cuando el usuario haga clic en un botón "agregar" ejecute JavaScript que agregue un nuevo formulario (que es parte del conjunto de formularios) a la página.

 232
Author: Dmitriy, 2009-02-02

15 answers

Así es como lo hago, usando jQuery :

Mi plantilla:

<h3>My Services</h3>
{{ serviceFormset.management_form }}
{% for form in serviceFormset.forms %}
    <div class='table'>
    <table class='no_error'>
        {{ form.as_table }}
    </table>
    </div>
{% endfor %}
<input type="button" value="Add More" id="add_more">
<script>
    $('#add_more').click(function() {
        cloneMore('div.table:last', 'service');
    });
</script>

En un archivo javascript:

function cloneMore(selector, type) {
    var newElement = $(selector).clone(true);
    var total = $('#id_' + type + '-TOTAL_FORMS').val();
    newElement.find(':input').each(function() {
        var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
        var id = 'id_' + name;
        $(this).attr({'name': name, 'id': id}).val('').removeAttr('checked');
    });
    newElement.find('label').each(function() {
        var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
        $(this).attr('for', newFor);
    });
    total++;
    $('#id_' + type + '-TOTAL_FORMS').val(total);
    $(selector).after(newElement);
}

Lo que hace:

cloneMore acepta selector como primer argumento, y el type de formset como la 2. Lo que el selector debe hacer es pasarle lo que debe duplicar. En este caso, lo paso div.table:last para que jQuery busque la última tabla con una clase de table. La parte :last de la misma es importante porque la selector también se utiliza para determinar lo que el el nuevo formulario se insertará después. Lo más probable es que lo quieras al final del resto de los formularios. El argumento type es para que podamos actualizar el campo management_form, notablemente TOTAL_FORMS, así como los campos de formulario reales. Si tiene un conjunto de formularios lleno de, por ejemplo, Client modelos, los campos de administración tendrán IDs de id_clients-TOTAL_FORMS y id_clients-INITIAL_FORMS, mientras que los campos de formulario estarán en un formato de id_clients-N-fieldname con N siendo el número de formulario, comenzando por 0. Así que con el argumento type la función cloneMore mira cuántos los formularios que hay actualmente, y pasa por cada entrada y etiqueta dentro del nuevo formulario reemplazando todos los nombres de campo / ids de algo como id_clients-(N)-name a id_clients-(N+1)-name y así sucesivamente. Una vez terminado, actualiza el campo TOTAL_FORMS para reflejar el nuevo formulario y lo agrega al final del conjunto.

Esta función es particularmente útil para mí porque la forma en que está configurada me permite usarla en toda la aplicación cuando quiero proporcionar más formularios en un conjunto de formularios, y no me hace necesario tener un formulario "plantilla" para duplicar siempre y cuando le pase el nombre del conjunto de formularios y el formato en el que se presentan los formularios. Espero que ayude.

 203
Author: Paolo Bergantino,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-09-16 06:42:13

Versión simplificada de la respuesta de Paolo usando empty_form como plantilla.

<h3>My Services</h3>
{{ serviceFormset.management_form }}
<div id="form_set">
    {% for form in serviceFormset.forms %}
        <table class='no_error'>
            {{ form.as_table }}
        </table>
    {% endfor %}
</div>
<input type="button" value="Add More" id="add_more">
<div id="empty_form" style="display:none">
    <table class='no_error'>
        {{ serviceFormset.empty_form.as_table }}
    </table>
</div>
<script>
    $('#add_more').click(function() {
        var form_idx = $('#id_form-TOTAL_FORMS').val();
        $('#form_set').append($('#empty_form').html().replace(/__prefix__/g, form_idx));
        $('#id_form-TOTAL_FORMS').val(parseInt(form_idx) + 1);
    });
</script>
 87
Author: Dave,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-03-28 09:42:33

He publicado un fragmento de una aplicación en la que trabajé hace un tiempo. Similar a Paolo, pero también le permite eliminar formularios.

 24
Author: elo80ka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-03-22 12:17:17

La sugerencia de Paolo funciona muy bien con una advertencia: los botones atrás/adelante del navegador.

Los elementos dinámicos creados con el script de Paolo no se renderizarán si el usuario regresa al conjunto de formularios usando el botón atrás/adelante. Un problema que puede ser un factor decisivo para algunos.

Ejemplo:

1) El usuario agrega dos nuevos formularios al conjunto de formularios usando el botón" add-more "

2) El usuario rellena los formularios y envía el conjunto de formularios

3) El usuario hace clic en el botón atrás en el navegador

4) Formset ahora se reduce a la forma original, todos los formularios añadidos dinámicamente no están allí

Esto no es un defecto del script de Paolo en absoluto; sino un hecho de la vida con la manipulación del dom y la caché del navegador.

Supongo que uno podría almacenar los valores del formulario en la sesión y tener algo de magia ajax cuando se cargue el formset para crear los elementos de nuevo y recargar los valores de la sesión; pero dependiendo de lo anal que quieras ser sobre el mismo usuario y múltiples instancias de la forma esto puede llegar a ser muy complicado.

Alguien tiene una buena sugerencia para lidiar con esto?

Gracias!

 18
Author: cethegeek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-04-21 12:52:17

Echa un vistazo a las siguientes soluciones para los formularios dinámicos de django:

Http://code.google.com/p/django-dynamic-formset /

Https://github.com/javisantana/django-dinamyc-form/tree/master/frm

Ambos hacen uso de jQuery y son específicos de django. El primero parece un poco más pulido y ofrece una descarga que viene con demos que son excelentes.

 13
Author: Kreychek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-26 20:41:55

Simular e imitar:

  • Cree un conjunto de formularios que corresponda a la situación antes de haciendo clic en el botón "agregar".
  • Cargue la página, vea la fuente y tome nota de todos los campos <input>.
  • Modifique el conjunto de formularios para que corresponda a la situación después de hacer clic en el botón "agregar" (cambie el número de campos adicionales).
  • Cargue la página, vea la fuente y tome nota de cómo cambiaron los campos <input>.
  • Crear un JavaScript que modifique el DOM de una manera adecuada para moverlo del estado antes de al estado después de.
  • Adjunte ese JavaScript al botón "agregar".

Aunque sé que los conjuntos de formularios usan campos ocultos especiales <input> y sé aproximadamente lo que debe hacer el script, no recuerdo los detalles de la parte superior de mi cabeza. Lo que he descrito anteriormente es lo que haría en su situación.

 11
Author: akaihola,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-02-09 20:49:11

Hay un plugin jquery para este, lo utilicé con inline_form set en Django 1.3, y funciona perfectamente, incluyendo la prepoblación, la adición de formularios del lado del cliente, la eliminación y múltiples inline_formsets.

 6
Author: e-satis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-01-04 19:57:20

Una opción sería crear un conjunto de formularios con todos los formularios posibles, pero inicialmente establecer los formularios no solicitados a hidden - ie, display: none;. Cuando sea necesario mostrar un formulario, establezca su visualización css en block o lo que sea apropiado.

Sin saber más detalles de lo que su "Ajax" está haciendo, es difícil dar una respuesta más detallada.

 4
Author: Daniel Naab,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-02-02 00:49:35

Otra versión de cloneMore, que permite la desinfección selectiva de campos. Úselo cuando necesite evitar que se borren varios campos.

$('table tr.add-row a').click(function() {
    toSanitize = new Array('id', 'product', 'price', 'type', 'valid_from', 'valid_until');
    cloneMore('div.formtable table tr.form-row:last', 'form', toSanitize);
});

function cloneMore(selector, type, sanitize) {
    var newElement = $(selector).clone(true);
    var total = $('#id_' + type + '-TOTAL_FORMS').val();
    newElement.find(':input').each(function() {
        var namePure = $(this).attr('name').replace(type + '-' + (total-1) + '-', '');
        var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
        var id = 'id_' + name;
        $(this).attr({'name': name, 'id': id}).removeAttr('checked');

        if ($.inArray(namePure, sanitize) != -1) {
            $(this).val('');
        }

    });
    newElement.find('label').each(function() {
        var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
        $(this).attr('for', newFor);
    });
    total++;
    $('#id_' + type + '-TOTAL_FORMS').val(total);
    $(selector).after(newElement);
}
 4
Author: xaralis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-11-04 14:00:25

Hay un pequeño problema con la función cloneMore. Dado que también está limpiando el valor de los campos ocultos generados automáticamente por django, hace que django se queje si intenta guardar un conjunto de formularios con más de un formulario vacío.

Aquí hay una solución:

function cloneMore(selector, type) {
    var newElement = $(selector).clone(true);
    var total = $('#id_' + type + '-TOTAL_FORMS').val();
    newElement.find(':input').each(function() {
        var name = $(this).attr('name').replace('-' + (total-1) + '-','-' + total + '-');
        var id = 'id_' + name;

        if ($(this).attr('type') != 'hidden') {
            $(this).val('');
        }
        $(this).attr({'name': name, 'id': id}).removeAttr('checked');
    });
    newElement.find('label').each(function() {
        var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
        $(this).attr('for', newFor);
    });
    total++;
    $('#id_' + type + '-TOTAL_FORMS').val(total);
    $(selector).after(newElement);
}
 2
Author: Cesar Canassa,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-07 21:22:30

Creo que esta es una solución mucho mejor.

¿Cómo crearías un formset dinámico en Django?

Hace cosas que el clon no hace:

  • Agregar formulario cuando no exista un formulario inicial
  • Maneja javascript en la forma mejor, por ejemplo django-ckeditor
  • Conservar los datos iniciales
 2
Author: Bufke,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 11:47:28

@Paolo Bergantino

Para clonar todos los controladores adjuntos, simplemente modifique la línea

var newElement = $(selector).clone();

Para

var newElement = $(selector).clone(true);

Para prevenir este problema.

 1
Author: panchicore,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:10:44

Sí, también recomendaría simplemente renderizarlos en el html si tiene un número finito de entradas. (Si no lo haces tendrás que usar otro método).

Puedes esconderlos así:

{% for form in spokenLanguageFormset %}
    <fieldset class="languages-{{forloop.counter0 }} {% if spokenLanguageFormset.initial_forms|length < forloop.counter and forloop.counter != 1 %}hidden-form{% endif %}">

Entonces la js es realmente simple:

addItem: function(e){
    e.preventDefault();
    var maxForms = parseInt($(this).closest("fieldset").find("[name*='MAX_NUM_FORMS']").val(), 10);
    var initialForms = parseInt($(this).closest("fieldset").find("[name*='INITIAL_FORMS']").val(), 10);
    // check if we can add
    if (initialForms < maxForms) {
        $(this).closest("fieldset").find("fieldset:hidden").first().show();
        if ($(this).closest("fieldset").find("fieldset:visible").length == maxForms ){
            // here I'm just hiding my 'add' link
            $(this).closest(".control-group").hide();
        };
    };
}
 1
Author: Bob Spryn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-08-09 01:05:17

Debido a que todas las respuestas anteriores usan jQuery y hacen que algunas cosas sean un poco complejas, escribí el siguiente script:

function $(selector, element) {
    if (!element) {
        element = document
    }
    return element.querySelector(selector)
}

function $$(selector, element) {
    if (!element) {
        element = document
    }
    return element.querySelectorAll(selector)
}

function hasReachedMaxNum(type, form) {
    var total = parseInt(form.elements[type + "-TOTAL_FORMS"].value);
    var max = parseInt(form.elements[type + "-MAX_NUM_FORMS"].value);
    return total >= max
}

function cloneMore(element, type, form) {
    var totalElement = form.elements[type + "-TOTAL_FORMS"];
    total = parseInt(totalElement.value);
    newElement = element.cloneNode(true);
    for (var input of $$("input", newElement)) {
        input.name = input.name.replace("-" + (total - 1) + "-", "-" + total + "-");
        input.value = null
    }
    total++;
    element.parentNode.insertBefore(newElement, element.nextSibling);
    totalElement.value = total;
    return newElement
}
var addChoiceButton = $("#add-choice");
addChoiceButton.onclick = function() {
    var choices = $("#choices");
    var createForm = $("#create");
    cloneMore(choices.lastElementChild, "choice_set", createForm);
    if (hasReachedMaxNum("choice_set", createForm)) {
        this.disabled = true
    }
};

Primero debe establecer auto_id a false y así deshabilitar la duplicación de id y nombre. Debido a que los nombres de entrada tienen que ser únicos en su forma, toda identificación se realiza con ellos y no con id. También tienes que reemplazar el form, type y el contenedor del conjunto de formularios. (En el ejemplo anterior choices)

 1
Author: R3turnz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-04-03 16:40:57

Para que los programadores que están cazando recursos entiendan las soluciones anteriores un poco mejor:

Django Dynamic Formsets

Después de leer el enlace anterior, la documentación de Django y las soluciones anteriores deberían tener mucho más sentido.

Documentación de Django Formset

Como un resumen rápido de lo que me estaba confundiendo: El Formulario de Administración contiene una visión general de los formularios dentro. Debe mantener esa información precisa en orden para que Django esté al tanto de los formularios que agrega. (Comunidad, por favor dame sugerencias si algunas de mis palabras están fuera de aquí. Soy nuevo en Django.)

 0
Author: Ryan Buchmeier,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-03 14:53:19