Agregando dinámicamente un formulario a un formset de Django con Ajax

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

Así es como lo hago, usando jQuery :

Mi plantilla:

My Services

{{ serviceFormset.management_form }} {% for form in serviceFormset.forms %}
{{ form.as_table }}
{% endfor %}

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:

cloneMore acepta selector como primer argumento, y el type de formset como segundo. Lo que el selector debería hacer es pasarle lo que debería duplicar. En este caso, lo paso div.table:last para que jQuery busque la última tabla con una clase de table . La :last parte es importante porque el selector también se usa para determinar en qué se insertará la nueva forma. Lo más probable es que lo desee al final del rest de los formularios. El argumento type es para que podamos actualizar el campo management_form , especialmente TOTAL_FORMS , así como los campos de formulario reales. Si tiene un formset lleno de, por ejemplo, modelos de Client , los campos de gestión tendrán ID de id_clients-TOTAL_FORMS e id_clients-INITIAL_FORMS , mientras que los campos de formulario estarán en un formato de id_clients-N-fieldname con N como el número de formulario, comenzando con 0 . Entonces, con el argumento type la función cloneMore analiza cuántos formularios hay actualmente, y pasa por cada entrada y etiqueta dentro del nuevo formulario reemplazando todos los nombres de campo / identificadores de algo como id_clients-(N)-name a id_clients-(N+1)-name y así sucesivamente. Una vez finalizado, 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 formset, y no me hace necesario tener una forma oculta de “plantilla” para duplicar siempre que lo pase el nombre del formulario y el formato en el que se presentan los formularios. Espero eso ayude.

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

 

My Services

{{ serviceFormset.management_form }}
{% for form in serviceFormset.forms %} {{ form.as_table }}
{% endfor %}

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.

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

Los elementos dynamics creados con el script de Paolo no se representarán si el usuario regresa al formset usando el botón Atrás / Adelante. Un problema que puede ser un factor decisivo para algunos.

Ejemplo:

1) El usuario agrega dos formularios nuevos al formset usando el botón “agregar-más”

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

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

4) Formset ahora se reduce a la forma original, todas las formas agregadas dinámicamente no están allí

Esto no es un defecto con el guión de Paolo; pero un hecho de la vida con la manipulación del dom y el 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 formset se cargue para crear los elementos nuevamente y volver a cargar los valores de la sesión; pero dependiendo de qué tan anal quieras ser sobre el mismo usuario y múltiples instancias del formulario, esto puede ser muy complicado.

¿Alguien tiene una buena sugerencia para lidiar con esto?

¡Gracias!

Vea las siguientes soluciones para los formularios django dynamics:

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

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

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

Simular e imitar:

  • Cree un formset 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 campos .
  • Modifique el formset para que se corresponda con la situación luego 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 cambiaron los campos .
  • Cree un JavaScript que modifique el DOM de forma adecuada para moverlo del estado anterior al estado posterior .
  • Adjunte ese JavaScript al botón “agregar”.

Si bien sé que los conjuntos de formularios utilizan campos ocultos especiales y sé aproximadamente qué debe hacer el script, no recuerdo los detalles de la parte superior de mi cabeza. Lo que describí arriba es lo que haría en tu situación.

Hay un plugin de jquery para esto , lo usé con inline_form establecido en Django 1.3, y funciona perfectamente, incluida la prepoblación, adición, eliminación y múltiples inline_formsets del formulario del lado del cliente.

Una opción sería crear un formset con todas las formas posibles, pero inicialmente establece los formularios no requeridos como ocultos, es decir, display: none; . Cuando es necesario mostrar un formulario, configure su visualización css para block o 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.

Otra versión de cloneMore, que permite la desinfección selectiva de los 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); } 

Hay un pequeño problema con la función cloneMore. Como también está limpiando el valor de los campos ocultos autogenerados de django, hace que django se queje si intentas guardar un formset 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); } 

Creo que esta es una solución mucho mejor.

¿Cómo crearías un formset dynamic en Django?

¿Las cosas clonan no?

  • Agregar formulario cuando no existen formularios iniciales
  • Maneja javascript en la forma mejor, por ejemplo django-ckeditor
  • Mantener datos iniciales

@Paolo Bergantino

para clonar todos los controladores adjuntos solo modifica la línea

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

para

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

para prevenir este problema

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

Puedes ocultarlos así:

 {% for form in spokenLanguageFormset %} 

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

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

 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í desactivar la duplicación de identificación y nombre. Debido a que los nombres de entrada deben ser únicos en su formulario, toda identificación se hace con ellos y no con los identificadores. También debe reemplazar el form , el type y el contenedor del formset. (En el ejemplo de arriba)

Para los codificadores por ahí que están buscando recursos para entender 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 confundía: el Formulario de gestión contiene una descripción general de los formularios. Debe mantener esa información precisa para que Django conozca los formularios que agrega. (Comunidad, por favor dame sugerencias si parte de mi redacción no está aquí. Soy nuevo en Django).