blog.farhan.codes

Farhan's Personal and Professional Blog


Duplicate a Django modelformset_factory Form

I created a formset_factory and wanted to have a simple “click me to add another form”. This seemed like a routine task, but the solutions I found online were unnecessarily complicated or required me to install a separate Django app, which I had no intention of doing.

So I created my own…

The only pre-requirement that this needs besides standard Django is jQuery.

So here is a rough overview of how this works:

  • Create a modelformset in my views.py and send it to the template.
  • Add in a link that’s executed to trigger the new form adding.
  • Django’s formset_factory’s required management_form creates the id_form-TOTAL_FORMS hidden variable. The jQuery must update this value.
  • Have jQuery locate the current form and create a blank copy from.
  • Update the name and id parameters of the copied form using jQuery
  • Identify where to paste the new form
  • Paste it there!

Here is my views.py snippet. In my case, the model is called “Component” and the related form is called “ComponentForm”. I define them as follows:

ComponentFormSet = modelformset_factory(Component, form=ComponentForm)
componentformset = ComponentFormSet( queryset=Component.objects.none() )

The queryset must be set to Component.objects.none() for whatever reason, otherwise you will get the latest Component value. I pass the componentformset in the context to the template.

Next, the HTML must be rendered in the template as follows. Noticed that I used componentformset.0 with the .0. Why? Because componentformset, being a modelformset_factory, is a set of forms, not an individual form. We have an individual form, so I am only going to display the first one in the list. After that, the template includes a tbody called formtemplate. This will later tell our jQuery where to copy our template from. Finally, the <a> tag is necessary to tell jQuery when to add a new form. The newcomponents is where the new form will be pasted.

{{ componentformset.management_form }}
<tbody id="formtemplate">
{{ componentformset.0 }}
tbody>

<tbody id="newcomponents">
tbody>
<a href="#" id='addForm'>Add Componenta>

Next is the Javascript. (In my actual production code I put this above the HTML, but you could really put this anywhere since it’ll only trigger when the page fully renders).

<script type="text/javascript">
    $(document).ready(function() {
      var addForm = $('#addForm');
      var formNumberObject = $('#id_form-TOTAL_FORMS');

      var addForm = function(e) {
        e.preventDefault();
        newFormCount = parseInt(formNumberObject.val()) + 1;
        formNumberObject.val( newFormCount );

        var tabletemplate = $('#formtemplate tr').clone();
        var pastehere = $('#newcomponents');

        changevalues = tabletemplate.find('[id^=id_form-0]');
        changevalues.each( function(i, index) {
          currentid = $(this).attr('id').replace(/(id_form-)[0-9]+/, 'id_form-' + (newFormCount-1) );
          $(this).attr('id', currentid);

          currentname = $(this).attr('name').replace(/(form-)[0-9]+/, 'form-' + (newFormCount-1) );
          $(this).attr('name', currentname);

        });

        tabletemplate.appendTo(pastehere);
      }
      addForm.on('click', addForm);
    });
  
</script>

The JS does the following. This is in order of the logic of the program, not in order of the code above, but you should be able to piece it together.

  • Identify the ‘addForm’ link and set it to execute the function ‘addForm’ (yes, they have the same name, but you can change that)
  • Identify the id_form-TOTAL_FORMS by ID in the rendered HTML. Again, this is produced by the management_form part of our formset_factory.
  • Identify the number of forms. I suppose I could have just set this to 0 by default, but I’ll have the JS do it for me.
  • When the user clicks the ‘addForm’ link, it will execute the function ‘addForm’, which does the following:
    • Add 1 to the current newFormCount value.
    • Create a copy of the form and store it in “tabletemplate”.
    • Identify where to paste the data, identified by newcomponents.
    • Find each instance where the ID is in the RegEx pattern id_form-0
    • Iterate for each instance, and replace the number 0 with the current number. This will keep the individual component name intact, but update the count.
  • Append the newly created form to the “pastehere” tbody.