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

260

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

Chip Tol
fuente
Solo estoy adivinando su caso de uso aquí, ¿es algo así como la función "Adjuntar otro archivo" en gmail, donde se le presenta al usuario un campo de carga de archivos y se agregan nuevos campos al DOM sobre la marcha cuando el usuario hace clic al botón "Adjuntar otro archivo" más?
prairiedogg 02 de
Esto es algo en lo que iba a trabajar pronto, así que también me interesarán las respuestas.
Van Gale
2
Esta pregunta es un poco confusa, menciona "Ajax" en el título, descripción y etiquetas. Sin embargo, ninguna de las respuestas hace uso de Ajax, todavía requiere que se envíe el formulario.
Antoine Pinsard

Respuestas:

219

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);
}

Que hace:

cloneMoreacepta selectorcomo primer argumento, y typeof formset como el segundo. Lo que selectordebería hacer es pasarle lo que debería duplicar. En este caso, lo paso div.table:lastpara que jQuery busque la última tabla con una clase de table. La :lastparte de esto es importante porque selectortambién se usa para determinar después de qué se insertará el nuevo formulario. Es más que probable que lo desee al final del resto de los formularios. El typeargumento es para que podamos actualizar el management_formcampo, en particular TOTAL_FORMS, así como los campos de formulario reales. Si tiene un conjunto de formularios lleno de, por ejemplo, Clientmodelos, los campos de administración tendrán ID id_clients-TOTAL_FORMSy id_clients-INITIAL_FORMS, mientras que los campos de formulario estarán en un formato de id_clients-N-fieldnameconNsiendo el número de formulario, comenzando con 0. Así, con el typeargumento de las cloneMoremiradas de la función de cuántas formas hay actualmente, y pasa a través de cada entrada y etiqueta dentro de la nueva forma de la sustitución de todos los nombres de campo / nº ID de algo así como id_clients-(N)-nameque id_clients-(N+1)-namey así sucesivamente. Una vez finalizado, actualiza el TOTAL_FORMScampo 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 hace que necesite tener un formulario oculto de "plantilla" para duplicar siempre que lo pase, el nombre del formulario y el formato en que se presentan los formularios. Espero eso ayude.

Paolo Bergantino
fuente
En IE, un clon de un elemento clonado se representa como <indefinido> al seleccionar en JS, ¿por qué?
panchicore
Descubrí que en Django 1.1 necesitará asignar un valor al prefixmiembro del objeto Formset. Este debería ser el mismo valor que el typeargumento para la cloneMorefunción.
Derek Reynolds
3
Modifiqué esto para tomar el selector sin: last y utilicé var total = $ (selector) .length; para obtener mi total porque una actualización de la página eliminaría mis conjuntos de formularios pero dejaría el aumento TOTAL que lleva a que se guarde el número incorrecto. Luego agregué: último al selector según sea necesario. Gracias por esto
Greg
2
He encontrado que esto usa $ (this) .attr ({'nombre': nombre, 'id': id}). Val (''). RemoveAttr ('marcado'); Para borrar la entrada, se desordenarán las casillas de verificación. Establecer val ('') le da a las casillas de verificación un atributo de valor vacío. Y dado que las casillas de verificación no usan el atributo de valor, esto nunca se actualizará, sin importar cuántas veces haga clic en él. Pero parece que el valor tiene mayor prioridad que el atributo "marcado" de las casillas de verificación. Lo que significa que siempre publicará casillas de verificación no marcadas.
niklasdstrom
por favor, paolo, ¿puedes consultar mi problema stackoverflow.com/questions/62252867/…
art_cs
109

Versión simplificada de la respuesta de Paolo usando empty_formcomo 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>
Dave
fuente
¿Cómo puedo tratar esto en la vista? cuando uso CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)solo obtengo un formulario, en método limpio. ¿Puedes explicar cómo manejar esto en las vistas?
AJ
Brillante, gracias. Hace un excelente uso de los ayudantes de Django disponibles (como empty_form), lo cual agradezco.
BigglesZX
@BigglesZX: he adaptado la solución y se están generando las nuevas filas de formularios vacíos. Sin embargo, los cuadros de selección generan una lista de opciones FK (disponibles), en lugar de menús desplegables que de otro modo se generarían para el conjunto original de formularios. ¿Se ha informado algún problema de esta naturaleza?
user12379095
@Dave, ¿podría actualizar la respuesta para versiones posteriores, es decir, 3.x? es simple y claro pero no funciona para mí
Poula Adel
1
@PoulaAdel ¿Qué no funciona? Acabo de probar esto en Django 3.0.5 y todavía funciona para mí. Sorprendente después de 8 años, pero supongo que Django y jQuery tienen una buena compatibilidad con el código anterior.
Dave
18

La sugerencia de Paolo funciona de maravilla con una advertencia: los botones de retroceso / avance del navegador.

Los elementos dinámicos creados con la secuencia de comandos de Paolo no se representarán si el usuario regresa al conjunto de formularios con 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 con el botón "Agregar más"

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

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

4) El conjunto de formularios ahora se reduce al formulario original, todos los formularios agregados dinámicamente no están allí

Esto no es un defecto con el guión de Paolo; pero un hecho de la vida con manipulación dom y caché del navegador.

Supongo que uno podría almacenar los valores del formulario en la sesión y tener algo de magia ajax cuando el conjunto de formularios se carga para crear los elementos nuevamente y volver a cargar los valores de la sesión; pero dependiendo de qué tan anal quiera ser sobre el mismo usuario y las múltiples instancias del formulario, esto puede ser muy complicado.

¿Alguien tiene una buena sugerencia para lidiar con esto?

¡Gracias!

cethegeek
fuente
2
Si redirige después de un envío exitoso, el botón de retroceso no es un problema. Si completa los formularios del DB en la próxima visita, todos los formularios aparecerán inicialmente. Si falla los formularios debido a una entrada no válida, todos ellos deberían estar allí en la pantalla con errores. A menos que no entienda sus declaraciones ... Esa redirección posterior al envío es realmente importante en una buena aplicación que funcione, una que muchos codificadores simplemente no obtienen en función de la cantidad de aplicaciones que se comportan mal en la web.
Boatcoder
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs
11

Simula e imita:

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

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

akaihola
fuente
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho stackoverflow.com/questions/62285767/… pero no obtuve una respuesta! te aprecio mucho
art_cs
6

Hay un complemento jquery para esto , lo utilicé con el conjunto inline_form en Django 1.3, y funciona perfectamente, incluida la prepoblación, la adición, eliminación y múltiples conjuntos de formularios del lado del cliente.

e-satis
fuente
Si bien la publicación de blog vinculada todavía existe, los enlaces de descarga están rotos. Aparentemente, el complemento fue creado por @ elo80ka, cuya respuesta apunta a una versión (¿preliminar?) Del script.
lfurini
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs
4

Una opción sería la creación de un juego de formularios con todas las formas posibles, pero en un principio establecer las formas que no se necesiten en Oculto - es decir, display: none;. Cuando sea necesario mostrar un formulario, configure su pantalla css en blocko lo que sea apropiado.

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

Daniel Naab
fuente
4

Otra versión 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);
}
xaralis
fuente
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs
2

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);
}
Cesar Canassa
fuente
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs
2

Para los codificadores que están buscando recursos para comprender un poco mejor las soluciones anteriores:

Conjuntos de formularios dinámicos de Django

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 del formulario de Django

Como resumen rápido de lo que me estaba confundiendo: El formulario de administración contiene una descripción general de los formularios que contiene. Debe mantener esa información precisa para que Django conozca los formularios que agrega. (Comunidad, por favor dame sugerencias si alguna de mis palabras está fuera de aquí. Soy nuevo en Django).

Ryan Buchmeier
fuente
1

@Paolo Bergantino

para clonar todos los controladores adjuntos simplemente modifique la línea

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

para

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

para evitar este problema

panchicore
fuente
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs
1

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

Puedes ocultarlos 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 el 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();
        };
    };
}
Bob Spryn
fuente
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs
1

Como 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 en falso y así deshabilitar la duplicación de id y nombre. Debido a que los nombres de entrada deben ser únicos en su forma, toda la identificación se realiza con ellos y no con los ID. También tiene que reemplazar el form, typey el contenedor del juego de formularios. (En el ejemplo anterior choices)

R3turnz
fuente
¿me pueden ayudar stackoverflow.com/questions/62285767/… , he intentado mucho pero no obtuve una respuesta! te aprecio mucho
art_cs