ajax django

Agregar dinĂ¡micamente un formulario a un formset Django con Ajax



django formset js (15)

@Paolo Bergantino

Para clonar todos los manejadores adjuntos solo modifica la línea.

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

para

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

para evitar este problema

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


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:

cloneMore acepta el selector como el primer argumento y el type de formset como el segundo. Lo que debe hacer el selector es pasarlo a 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á el nuevo formulario después. Lo más probable es que lo desee al final del resto de los formularios. El argumento de type es para que podamos actualizar el campo management_form , en particular TOTAL_FORMS , así como los campos de formulario reales. Si tiene un formato completo de, digamos, modelos de Client , los campos de administració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 de type la función cloneMore mira cuántas formas hay actualmente, y pasa por cada entrada y etiqueta dentro de la nueva forma, 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 conjunto de formularios, y no me obliga a tener un formulario "plantilla" oculto para duplicar siempre que le pase el nombre de formset y el formato en el que se presentan los formularios. Espero eso ayude.


Creo que esta es una solución mucho mejor.

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

¿Las cosas clone no?

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

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

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 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 debe reemplazar el form , el type y el contenedor del formset. (En el ejemplo de las choices anteriores)



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


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


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


La sugerencia de Paolo funciona a la perfección con una advertencia: los botones de avance / avance del navegador.

Los elementos dinámicos creados con el script de Paolo no se procesarán si el usuario regresa al formset usando el botón de avance / retroceso. Un problema que puede ser un factor decisivo para algunos.

Ejemplo:

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

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

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 de 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 formset se cargue para crear los elementos nuevamente y volver a cargar los valores de la sesión; pero dependiendo de qué tan análogo quiera ser sobre el mismo usuario y varias instancias del formulario, esto puede ser muy complicado.

¿Alguien tiene una buena sugerencia para lidiar con esto?

¡Gracias!


Otra versión cloneMore, que permite el saneamiento selectivo 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); }


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

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 Django Formset

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


Sí, también recomendaría simplemente mostrarlos 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 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(); }; }; }


Simular e imitar:

  • 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 <input> campos <input> .
  • Modifique el formset para que se corresponda con 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> .
  • Cree algún JavaScript que modifique el DOM de una manera adecuada para moverlo del estado anterior al estado posterior .
  • Adjunte ese JavaScript al botón "agregar".

Si bien sé que los conjuntos de formularios usan campos <input> ocultos especiales y saben 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 tu situación.


Una opción sería crear un conjunto de formularios con cada forma posible, pero inicialmente establecer las formas no requeridas en ocultas, es decir, display: none; . Cuando sea necesario mostrar un formulario, configúrelo como 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.


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>