Skip to main content

Dynamically adding a form to a Django formset with Ajax


I'd like to be able to automatically add new forms to a Django formset with an ajax function. I.e., the user clicks an "add" button and some javascript will add a new form (which is part of the formset) to the page.



Thanks for the help in advance. :-)


Source: Tips4allCCNA FINAL EXAM

Comments

  1. This is how I do it, using jQuery:

    My template:

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


    In a javascript file:

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


    What it does:

    cloneMore accepts selector as the first argument, and the type of formset as the 2nd one. What the selector should do is pass it what it should duplicate. In this case, I pass it div.table:last so that jQuery looks for the last table with a class of table. The :last part of it is important because the selector is also used to determine what the new form will be inserted after. More than likely you'd want it at the end of the rest of the forms. The type argument is so that we can update the management_form field, notably TOTAL_FORMS, as well as the actual form fields. If you have a formset full of, say, Client models, the management fields will have IDs of id_clients-TOTAL_FORMS and id_clients-INITIAL_FORMS, while the form fields will be in a format of id_clients-N-fieldname with N being the form number, starting with 0. So with the type argument the cloneMore function looks at how many forms there currently are, and goes through every input and label inside the new form replacing all the field names/ids from something like id_clients-(N)-name to id_clients-(N+1)-name and so on. After it is finished, it updates the TOTAL_FORMS field to reflect the new form and adds it to the end of the set.

    This function is particularly helpful to me because the way it is setup it allows me to use it throughout the app when I want to provide more forms in a formset, and doesn't make me need to have a hidden "template" form to duplicate as long as I pass it the formset name and the format in which the forms are laid out. Hope it helps.

    ReplyDelete
  2. Paolo's suggestion works beautifully with one caveat - the browser's back/forward buttons.

    The dynamic elements created with Paolo's script will not be rendered if the user returns to the formset using the back/forward button. An issue that may be a deal breaker for some.

    Example:

    1) User adds two new forms to the formset using the "add-more" button

    2) User populates the forms and submits the formset

    3) User clicks the back button in the browser

    4) Formset is now reduced to the original form, all dynamically added forms are not there

    This is not a defect with Paolo's script at all; but a fact of life with dom manipulation and browser's cache.

    I suppose one could store the values of the form in the session and have some ajax magic when the formset loads to create the elements again and reload the values from the session; but depending on how anal you want to be about the same user and multiple instances of the form this may become very complicated.

    Anyone has a good suggestion for dealing with this?

    Thanks!

    ReplyDelete
  3. I've posted a snippet from an app I worked on a while back. Similar to Paolo's, but also allows you delete forms.

    ReplyDelete
  4. Simulate and imitate:


    Create a formset which corresponds to the situation before clicking the "add" button.
    Load the page, view the source and take a note of all <input> fields.
    Modify the formset to correspond to the situation after clicking the "add" button (change the number of extra fields).
    Load the page, view the source and take a note of how the <input> fields changed.
    Create some JavaScript which modifies the DOM in a suitable way to move it from the before state to the after state.
    Attach that JavaScript to the "add" button.


    While I do know formsets use special hidden <input> fields and know approximately what the script must do, I don't recall the details off the top of my head. What I described above is what I would do in your situation.

    ReplyDelete
  5. One option would be to create a formset with every possible form, but initially set the unrequired forms to hidden - ie, display: none;. When it's necessary to display a form, set it's css display to block or whatever is appropriate.

    Without know more details of what your "Ajax" is doing, it's hard to give a more detailed response.

    ReplyDelete
  6. Check out the following solutions to dynamic django forms:

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

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

    They both make use of jQuery and are django-specific. The first seems a bit more polished and offers a download that comes w/demos which are excellent.

    ReplyDelete
  7. There is a jquery plugin for this, I used it with inline_form set in Django 1.3, and it works perfectly, including prepopulation, client side form adding, removing, and multiple inline_formsets.

    ReplyDelete
  8. put http://dpaste.com/hold/17376/ to django/contrib/admin/media/js/admin/AddInline.js

    download jquery (jquery.com, version 1.3.6) and put it to django/contrib/admin/media/js/jquery.js

    and then just add

    class Media:

    js = ('/media/js/jquery.js', '/media/js/admin/AddInline.js', )


    to your ModelAdmin definition for your model.

    All inlines in your form will get magic 'Add another' link at the bottom.

    ReplyDelete
  9. There is a small issue with the cloneMore function. Since it's also cleaning the value of the django auto-generated hidden fields, it causes django to complain if you try to save a formset with more than one empty form.

    Here is a fix:

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

    ReplyDelete
  10. Another cloneMore version, which allows for selective sanitization of fields. Use it when you need to prevent several fields from being erased.

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

    ReplyDelete
  11. I think this is a much better solution.

    How would you make a dynamic formset in Django?

    Does things clone doesn't:


    Add form when no initial forms exists
    Handles javascript in the form better, for example django-ckeditor
    Keep initial data

    ReplyDelete
  12. Simplified version of Paolo's answer using "empty_form" as a template.

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

    ReplyDelete
  13. @Paolo Bergantino

    to clone all the handlers attached just modify the line

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


    for

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


    to prevent this problem.

    ReplyDelete

Post a Comment

Popular posts from this blog

[韓日関係] 首相含む大幅な内閣改造の可能性…早ければ来月10日ごろ=韓国

div not scrolling properly with slimScroll plugin

I am using the slimScroll plugin for jQuery by Piotr Rochala Which is a great plugin for nice scrollbars on most browsers but I am stuck because I am using it for a chat box and whenever the user appends new text to the boxit does scroll using the .scrollTop() method however the plugin's scrollbar doesnt scroll with it and when the user wants to look though the chat history it will start scrolling from near the top. I have made a quick demo of my situation http://jsfiddle.net/DY9CT/2/ Does anyone know how to solve this problem?

Why does this javascript based printing cause Safari to refresh the page?

The page I am working on has a javascript function executed to print parts of the page. For some reason, printing in Safari, causes the window to somehow update. I say somehow, because it does not really refresh as in reload the page, but rather it starts the "rendering" of the page from start, i.e. scroll to top, flash animations start from 0, and so forth. The effect is reproduced by this fiddle: http://jsfiddle.net/fYmnB/ Clicking the print button and finishing or cancelling a print in Safari causes the screen to "go white" for a sec, which in my real website manifests itself as something "like" a reload. While running print button with, let's say, Firefox, just opens and closes the print dialogue without affecting the fiddle page in any way. Is there something with my way of calling the browsers print method that causes this, or how can it be explained - and preferably, avoided? P.S.: On my real site the same occurs with Chrome. In the ex