28

I have a block of form elements which I would like to clone and increment their ID's using jQuery clone method. I have tried a number of examples but a lot of them only clone a single field.

My block is structured as such:

<div id="clonedInput1" class="clonedInput">
  <div>
    <div>
      <label for="txtCategory" class="">Learning category <span class="requiredField">*</span></label>
      <select class="" name="txtCategory[]" id="category1">
        <option value="">Please select</option>
      </select>
    </div>
   <div>
     <label for="txtSubCategory" class="">Sub-category <span class="requiredField">*</span></label>
     <select class="" name="txtSubCategory[]" id="subcategory1">
       <option value="">Please select category</option>
     </select>
   </div>
   <div>
     <label for="txtSubSubCategory">Sub-sub-category <span class="requiredField">*</span></label>
     <select name="txtSubSubCategory[]" id="subsubcategory1">
       <option value="">Please select sub-category</option>
     </select>
   </div>
</div>

Obviously elements are lined up a lot better but you get the idea.

I would like to keep the id structure i.e. category1, subcategory1 etc as I use these to dynamically display select options based on the parent selection so if its possible to have each cloned block like category1/category2/category3 etc that would be great.

Milan Jaric
  • 5,556
  • 2
  • 26
  • 34
puks1978
  • 3,667
  • 11
  • 44
  • 103

4 Answers4

24

HTML

<div id="clonedInput1" class="clonedInput">
    <div>
        <label for="txtCategory" class="">Learning category <span class="requiredField">*</span></label>
        <select class="" name="txtCategory[]" id="category1">
            <option value="">Please select</option>
        </select>
    </div>
    <div>
        <label for="txtSubCategory" class="">Sub-category <span class="requiredField">*</span></label>
        <select class="" name="txtSubCategory[]" id="subcategory1">
            <option value="">Please select category</option>
        </select>
    </div>
    <div>
        <label for="txtSubSubCategory">Sub-sub-category <span class="requiredField">*</span></label>
        <select name="txtSubSubCategory[]" id="subsubcategory1">
            <option value="">Please select sub-category</option>
        </select>
    </div>
    <div class="actions">
        <button class="clone">Clone</button> 
        <button class="remove">Remove</button>
    </div>
</div>

JavaScript - Jquery v1.7 and earlier

var regex = /^(.+?)(\d+)$/i;
var cloneIndex = $(".clonedInput").length;

$("button.clone").live("click", function(){
    $(this).parents(".clonedInput").clone()
        .appendTo("body")
        .attr("id", "clonedInput" +  cloneIndex)
        .find("*").each(function() {
            var id = this.id || "";
            var match = id.match(regex) || [];
            if (match.length == 3) {
                this.id = match[1] + (cloneIndex);
            }
    });
    cloneIndex++;
});

There is only one silly part :) .attr("id", "clonedInput" + $(".clonedInput").length) but it works ;)

JAvascript - JQuery recent (supporting .on())

var regex = /^(.+?)(\d+)$/i;
var cloneIndex = $(".clonedInput").length;

function clone(){
    $(this).parents(".clonedInput").clone()
        .appendTo("body")
        .attr("id", "clonedInput" +  cloneIndex)
        .find("*")
        .each(function() {
            var id = this.id || "";
            var match = id.match(regex) || [];
            if (match.length == 3) {
                this.id = match[1] + (cloneIndex);
            }
        })
        .on('click', 'button.clone', clone)
        .on('click', 'button.remove', remove);
    cloneIndex++;
}
function remove(){
    $(this).parents(".clonedInput").remove();
}
$("button.clone").on("click", clone);

$("button.remove").on("click", remove);

working example here

Milan Jaric
  • 5,556
  • 2
  • 26
  • 34
  • sorry to be a real pain in the butt. Would it be possible to add a remove button/link for the cloned elements. 1 for each so say if I created 10 clones, I could remove the fifth? – puks1978 Nov 05 '11 at 05:11
  • Thanks @Shomz I'll add your code to complete example aboe (just to be unobtrusive ) – Milan Jaric Nov 05 '11 at 18:11
  • Hi guys, thanks again for the awesome code. It works 99.9%. There is one silly bit (I think the bit you are thinking of Milan Jaric) where if I clone 3 times, and remove the middle (number 2) I have element1 and element3. Then when I go to clone again, I have a second element3 as the .length thinks there is only 3. I guess I could work out a way to cycle through is available element and assign the next available number but if you have any suggestions that would be great too. – puks1978 Nov 06 '11 at 10:11
  • There is one quick way, move to outer scope at page load var cloneIndex = $('.cloneInput').length; and each time you clone html increment it. I'm updating answer – Milan Jaric Nov 06 '11 at 11:15
  • knowing this is a really old solution but feel it should be mentioned jQuery `live` method has been deprecated for awhile now http://api.jquery.com/live/ – zadubz Jan 11 '15 at 00:53
  • I added solution with .on statement and updated fiddle – Milan Jaric Jan 12 '15 at 07:17
  • I don't understand why match.length return 0 for "not found" and return 3 for "found", can someone help me understand about this – Terry Lin Jun 12 '15 at 12:00
  • Because result contains ["clonedInputNN","clonedInput","NN"], I used () brackets in regular expression to capture values. so it is added in match result. If nothing is found result will be empty array. BTW looks like there is issue in regular expression if clone exceed more than 9 fields. So instead it should be `var regex = /^(.*)(\d+)$/i;` – Milan Jaric Jun 12 '15 at 13:24
  • 2
    Hi, nice work! I adapt your script to hidden button delete when Have one option. See http://jsfiddle.net/effectlabweb/8p1e6ug5/ – Rodrigo Porcionato Aug 24 '15 at 21:41
  • 1
    The regexs above will not work with more than 9 elements, you need to use non-greedy matching: `/^(.+?)(\d+)$/i;` (@MilanJaric, your regex won't work either, because it singles out *only the last integer*) – Antoine Lizée Jan 10 '16 at 23:58
  • @EffectlabCriaçãodeSistemas Desculpa, but your amendment causes problems as removing rows then re-adding causes the IDs to keep incrementally increasing. – BSUK Dec 07 '19 at 00:46
  • if this is use with a form, it will cause the form to send the data – Francesco Taioli Aug 05 '20 at 13:14
  • something like `` should do a trick, or you can add `event.preventDefault()` in `clone` and `remove` handlers – Milan Jaric Aug 05 '20 at 13:30
  • Hello Milan, thanks for this code, it works perfectly! How could I modify it to clone table rows by incrementing the id's of the input, select, textarea, etc. – tonydeleon Nov 03 '21 at 13:25
6

Another option would be to use a recursive function:

// Accepts an element and a function
function childRecursive(element, func){
    // Applies that function to the given element.
    func(element);
    var children = element.children();
    if (children.length > 0) {
        children.each(function (){
            // Applies that function to all children recursively
            childRecursive($(this), func);
        });
    }
}

Then you can make a function or three for setting the attributes and values of your yet-to-be-cloned form fields:

// Expects format to be xxx-#[-xxxx] (e.g. item-1 or item-1-name)
function getNewAttr(str, newNum){
    // Split on -
    var arr = str.split('-');
    // Change the 1 to wherever the incremented value is in your id
    arr[1] = newNum;
    // Smash it back together and return
    return arr.join('-');
}

// Written with Twitter Bootstrap form field structure in mind
// Checks for id, name, and for attributes.
function setCloneAttr(element, value){
    // Check to see if the element has an id attribute
    if (element.attr('id') !== undefined){
        // If so, increment it
        element.attr('id', getNewAttr(element.attr('id'),value));
    } else { /*If for some reason you want to handle an else, here you go*/ }
    // Do the same with name...
    if(element.attr('name') !== undefined){
        element.attr('name', getNewAttr(element.attr('name'),value));
    } else {}
    // And don't forget to show some love to your labels.
    if (element.attr('for') !== undefined){
        element.attr('for', getNewAttr(element.attr('for'),value));
    } else {}
}

// Sets an element's value to ''
function clearCloneValues(element){
    if (element.attr('value') !== undefined){
        element.val('');
    }
}

Then add some markup:

<div id="items">
    <input type="hidden" id="itemCounter" name="itemCounter" value="0">
    <div class="item">
        <div class="control-group">
            <label class="control-label" for="item-0-name">Item Name</label>
            <div class="controls">
                <input type="text" name="item-0-name" id="item-0-name" class="input-large">
            </div>
        </div><!-- .control-group-->
        <div class="control-group">
            <label for="item-0-description" class="control-label">Item Description</label>
            <div class="controls">
                <input type="text" name="item-0-description" id="item-0-description" class="input-large">
            </div>
        </div><!-- .control-group-->
    </div><!-- .item -->
</div><!-- #items -->

<input type="button" value="Add Item" id="addItem">

And then all you need is some jQuery goodness to pull it all together:

$(document).ready(function(){
    $('#addItem').click(function(){
        //increment the value of our counter
        $('#itemCounter').val(Number($('#allergyCounter').val()) + 1);
        //clone the first .item element
        var newItem = $('div.item').first().clone();
        //recursively set our id, name, and for attributes properly
        childRecursive(newItem, 
            // Remember, the recursive function expects to be able to pass in
            // one parameter, the element.
            function(e){
                setCloneAttr(e, $('#itemCounter').val());
        });
        // Clear the values recursively
        childRecursive(newItem, 
            function(e){
                clearCloneValues(e);
            }
        );
        // Finally, add the new div.item to the end
        newItem.appendTo($('#items'));
    });
});

Obviously, you don't necessarily need to use recursion to get everything if you know going in exactly what things you need to clone and change. However, these functions allow you to reuse them for any size of nested structure with as many fields as you want so long as they're all named with the right pattern.

There's a working jsFiddle here.

Craig Burton
  • 188
  • 1
  • 9
4

Clone the main element, strip the id number from it. In the new element replace every instance of that id number in every element id you want incremented with the new id number.

Ok, here's a quicky code here.

Basically, this part is the most important:

(parseInt(/test(\d+)/.exec($(this).attr('id'))[1], 10)+1

It parses the current id (using RegEx to strip the number from the string) and increases it by 1. In your case instead of 'test', you should put 'clonedInput' and also not only increase the value of the main element id, but the three from the inside as well (category, subcategory and subsubcategory). This should be easy once you have the new id.

Hope this helps. :)

Shomz
  • 37,421
  • 4
  • 57
  • 85
  • Who could pass up the offer for code ;) As I mentioned, I have tried a number of ways with no luck so yes please...that would be super. – puks1978 Nov 05 '11 at 04:32
  • There you go, hope I made it clearer. You could also implement a check if the new id number you get is already used (for example, if you clone element #1, then #2, and then #1 or #2 again - you'll get the new id = 3, and the #3 already exists since we cloned the #2, so we'd need the next increment), etc... Let me know if you need more help here. :) – Shomz Nov 05 '11 at 04:45
0

Add data attribute to the input to get the field name, increment the value with variable.

html :

<td>
   <input type="text" data-origin="field" name="field" id="field" required="" >
  <div role="button" onclick='InsertFormRow($(this).closest("tr"),"tableID","formID");' id="addrow"> + </div>

</td>

and put this javascript function

var rowNum = 1;

var InsertFormRow = function(row, ptable, form)
{
    nextrow = $(row).clone(true).insertAfter(row).prev('#' + ptable + ' tbody>tr:last');
    nextrow.attr("id", rowNum);
    nextrow.find("input").each(function() {
        this.name =  $(this).data("origin") + "_" + rowNum;
        this.id =  $(this).data("origin") + "_" + rowNum;
    });
    rowNum++;     
}