Adicionando dinamicamente um formulário a um formset do Django com o Ajax

260

Quero adicionar automaticamente novos formulários a um conjunto de formulários do Django usando o Ajax, para que, quando o usuário clicar em um botão "adicionar", execute JavaScript que adicione um novo formulário (que faz parte do conjunto de formulários) à página.

Chip Tol
fonte
Estou apenas adivinhando o seu caso de uso aqui, é algo como o recurso "Anexar outro arquivo" no gmail, em que o usuário recebe um campo de upload de arquivo e novos campos são adicionados ao DOM em tempo real quando o usuário clica para "Anexar outro arquivo" botão mais?
Prairiedogg 02/02/09
Isso é algo em que eu iria trabalhar em breve, então também vou me interessar por respostas.
Van Gale
2
Esta pergunta é um pouco confusa, menciona "Ajax" no título, descrição e tags. No entanto, nenhuma das respostas faz uso do Ajax, mas ainda exige que o formulário seja enviado.
Antoine Pinsard

Respostas:

219

É assim que eu faço, usando o jQuery :

Meu modelo:

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

Em um arquivo 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);
}

O que faz:

cloneMoreaceita selectorcomo o primeiro argumento e o typeformset como o 2º. O que o que selectordeve fazer é passar o que deve duplicar. Nesse caso, eu o passo div.table:lastpara que o jQuery procure a última tabela com uma classe de table. A :lastparte disso é importante porque selectortambém é usada para determinar o que o novo formulário será inserido depois. É provável que você queira isso no final dos demais formulários. O typeargumento é para que possamos atualizar o management_formcampo, principalmente TOTAL_FORMS, bem como os campos reais do formulário. Se você tem um formset cheio de, digamos, Clientmodelos, os campos de gestão terá IDs de id_clients-TOTAL_FORMSe id_clients-INITIAL_FORMS, enquanto os campos do formulário será em um formato de id_clients-N-fieldnamecomNsendo o número do formulário, começando com 0. Assim, com o typeargumento cloneMorede função analisa a forma como muitas formas atualmente existem, e passa por cada entrada e etiqueta dentro da nova forma de substituir todos os nomes de campo / ids de algo como id_clients-(N)-namea id_clients-(N+1)-namee assim por diante. Depois de concluído, ele atualiza o TOTAL_FORMScampo para refletir o novo formulário e o adiciona ao final do conjunto.

Essa função é particularmente útil para mim porque, da maneira como está configurada, permite que eu a use em todo o aplicativo quando quero fornecer mais formulários em um conjunto de formulários e não me faz precisar ter um "modelo" oculto para duplicar contanto que eu o transmita, o nome do conjunto de formulários e o formato em que os formulários são dispostos. Espero que ajude.

Paolo Bergantino
fonte
No IE, um clone de um elemento clonado é representado como <não definido> ao selecionar em JS, por quê?
panchicore
Eu descobri que no Django 1.1 você precisará atribuir um valor ao prefixmembro do Formset Object. Este deve ter o mesmo valor que o typeargumento para a cloneMorefunção.
Derek Reynolds
3
Eu modifiquei isso para pegar o seletor sem: last e usei var total = $ (seletor) .length; para obter meu total porque uma atualização da página removeria meus conjuntos de formulários, mas deixaria o aumento TOTAL levando ao número errado sendo salvo. Em seguida, adicionei: last ao seletor, conforme necessário. Obrigado por isso.
Greg
2
Eu descobri que isso usando $ (this) .attr ({'nome': nome, 'id': id}). Val (''). RemoveAttr ('marcado'); Para limpar a entrada, você estragará as caixas de seleção. A configuração de val ('') fornece às caixas de seleção um atributo de valor vazio. E como as caixas de seleção não usam o atributo value, isso nunca será atualizado - não importa quantas vezes você clicar nele. Mas parece que o valor tem prioridade mais alta do que o "marcado" atribuído às caixas de seleção. O que significa que você sempre postará caixas de seleção não marcadas.
Niklasdstrom
paolo, você pode verificar meu problema stackoverflow.com/questions/62252867/…
art_cs
109

Versão simplificada da resposta de Paolo usando empty_formcomo modelo.

<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
fonte
como posso lidar com isso na exibição? Quando eu uso, CompetitorFormSet = modelformset_factory(ProjectCompetitor, formset=CompetitorFormSets) ctx['competitor_form_set'] = CompetitorFormSet(request.POST)recebo apenas um formulário, no método clean. você pode explicar como lidar com isso em visualizações?
AJ
Brilhante - obrigado. Faz excelente uso dos ajudantes disponíveis do Django (como empty_form), o que eu aprecio.
BigglesZX
@BigglesZX - adaptei a solução e as novas linhas de formulários vazios estão sendo geradas. No entanto, as caixas de seleção estão gerando uma lista de opções FK (disponíveis), em vez de listas suspensas que, de outra forma, são geradas para o conjunto original de formulários. Algum problema dessa natureza foi relatado?
user12379095 17/02
@ Dave você pode atualizar a resposta para versões posteriores, ou seja, 3.x? é simples e claro, mas não está funcionando para mim
Poula Adel 09/04
1
@PoulaAdel O que não está funcionando? Eu apenas tentei isso no Django 3.0.5 e ainda funciona para mim. Surpreendente após 8 anos, mas acho que o Django e o jQuery têm boa compatibilidade com versões anteriores com códigos mais antigos.
Dave
18

A sugestão de Paolo funciona lindamente com uma ressalva - os botões voltar / avançar do navegador.

Os elementos dinâmicos criados com o script de Paolo não serão renderizados se o usuário retornar ao conjunto de formulários usando o botão voltar / avançar. Um problema que pode ser um rompimento de negócios para alguns.

Exemplo:

1) O usuário adiciona dois novos formulários ao formset usando o botão "add-more"

2) O usuário preenche os formulários e envia o conjunto de formulários

3) O usuário clica no botão Voltar no navegador

4) O Formset agora é reduzido ao formulário original, todos os formulários adicionados dinamicamente não estão lá

Este não é um defeito no script de Paolo; mas um fato da vida com manipulação dom e cache do navegador.

Suponho que alguém possa armazenar os valores do formulário na sessão e ter alguma mágica do ajax quando o formset for carregado para criar os elementos novamente e recarregar os valores da sessão; mas, dependendo de quão anal você deseja ser sobre o mesmo usuário e várias instâncias do formulário, isso pode se tornar muito complicado.

Alguém tem uma boa sugestão para lidar com isso?

Obrigado!

cethegeek
fonte
2
Se você redirecionar após o envio bem-sucedido, o botão Voltar não será um problema. Se você preencher os formulários do banco de dados na próxima visita, todos os formulários aparecerão inicialmente. Se você falhar nos formulários devido a entrada inválida, todos eles deverão estar no visor novamente com erros. A menos que eu não esteja entendendo suas declarações ... Esse redirecionamento de envio de post é realmente importante em um bom aplicativo funcional, que muitos codificadores simplesmente não obtêm com base no número de aplicativos com comportamento ruim que eu encontro na Web.
boatcoder
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs
11

Simule e imite:

  • Crie um conjunto de formulários que corresponda à situação antes de clicar no botão "adicionar".
  • Carregue a página, visualize a fonte e anote todos os <input>campos.
  • Modifique o conjunto de formulários para corresponder à situação depois de clicar no botão "adicionar" (altere o número de campos extras).
  • Carregue a página, visualize a fonte e tome nota de como o <input> campos foram alterados.
  • Crie algum JavaScript que modifique o DOM de maneira adequada para movê-lo do estado anterior para o estado posterior .
  • Anexe esse JavaScript ao botão "adicionar".

Embora eu saiba que os conjuntos de formulários usam <input>campos ocultos especiais e saiba aproximadamente o que o script deve fazer, não me lembro dos detalhes. O que eu descrevi acima é o que eu faria na sua situação.

akaihola
fonte
você pode me ajudar stackoverflow.com/questions/62285767/… , eu tentei muito stackoverflow.com/questions/62285767/… mas não obtive uma resposta! eu aprecio muito você
art_cs
6

Existe um plugin jquery para isso , usei-o com o inline_form definido no Django 1.3, e ele funciona perfeitamente, incluindo pré-população, adição de formulários do lado do cliente, remoção e vários inline_formsets.

e-satis
fonte
Enquanto a postagem do blog ainda existir, os links de download estão quebrados. Aparentemente, o plugin foi criado por @ elo80ka, cuja resposta aponta para uma versão (preliminar?) Do script.
Lfurini 28/01/19
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs
4

Uma opção seria criar um conjunto de formulários com todos os formulários possíveis, mas inicialmente defina os formulários não necessários como ocultos - ou seja display: none;,. Quando for necessário exibir um formulário, defina sua exibição em css comoblock ou o que for apropriado.

Sem saber mais detalhes do que seu "Ajax" está fazendo, é difícil dar uma resposta mais detalhada.

Daniel Naab
fonte
4

Outra versão cloneMore, que permite a higienização seletiva de campos. Use-o quando precisar impedir que vários campos sejam apagados.

$('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
fonte
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs
2

Há um pequeno problema com a função cloneMore. Como também está limpando o valor dos campos ocultos gerados automaticamente pelo django, faz com que o django se queixe se você tentar salvar um conjunto de formulários com mais de um formulário vazio.

Aqui está uma correção:

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
fonte
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs
2

Para os codificadores que estão procurando recursos para entender um pouco melhor as soluções acima:

Django Dynamic Formsets

Depois de ler o link acima, a documentação do Django e as soluções anteriores devem fazer muito mais sentido.

Documentação do Django Formset

Como um resumo rápido do que eu estava ficando confuso: O Formulário de Gerenciamento contém uma visão geral dos formulários contidos nele. Você deve manter essas informações precisas para que o Django esteja ciente dos formulários adicionados. (Comunidade, por favor, me dê sugestões se algumas das minhas palavras estiverem desativadas aqui. Sou novo no Django.)

Ryan Buchmeier
fonte
1

@Paolo Bergantino

clonar todos os manipuladores anexados apenas modifique a linha

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

para

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

para evitar esse problema.

panchicore
fonte
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs
1

Sim, eu também recomendo apenas renderizá-los no html se você tiver um número finito de entradas. (Caso contrário, você precisará usar outro método).

Você pode ocultá-los assim:

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

Então o js é realmente simples:

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
fonte
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs
1

Como todas as respostas acima usam jQuery e tornam algumas coisas um pouco complexas, escrevi o seguinte 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
    }
};

Primeiro, você deve definir auto_id como false e, assim, desativar a duplicação de id e nome. Como os nomes de entrada precisam ser únicos, toda a identificação é feita com eles e não com os IDs. Você também tem que substituir o form, typee o recipiente do formset. (No exemplo acima choices)

R3turnz
fonte
você pode me ajudar a stackoverflow.com/questions/62285767/… , tentei bastante, mas não obtive resposta! eu aprecio muito você
art_cs