8

I am currently working with generating dynamic input field inside a form. I have complex example that uses checkboxes and select boxes. It has two type of elements: main_items and sub_items. As mentioned, I can add input fields dynamically with some jquery through a clone function that replicates a new set of input fields with unique id attributes. But I am having great difficulty with two things: First, keeping the id’s unique for each element duplicated, specifically for the select boxes. Second, I have only been able to get the first drop down menu to work for the first item but I have not figured out a way to do it for the other items. JSFIDDLE

$('#btnAdd').click(function () {
    var num = $('.clonedSection').length;
    var newNum = num + 1;

    var newSection = $('#pq_entry_' + num).clone().attr('id', 'pq_entry_' + newNum);
    newSection.find('input[type="text"]').val('');
    newSection.find('select').val('');
    newSection.find('input[type="checkbox"]').prop('checked', false);
    //hide sub item
    newSection.find('.sub-item').hide();

    //change the input element selectors to use name
    newSection.find('input[name^="first_item_"]').attr('id', 'main_item_' + newNum).attr('name', 'main_item_' + newNum);
    newSection.find('input[name^="second_item_"]').attr('id', 'second_item_' + newNum).attr('name', 'second_item_' + newNum);
    newSection.find('input[name^="item_count_"]').attr('id', 'item_count_' + newNum).attr('name', 'item_count_' + newNum);
    newSection.find('input[name^="sub_item_"]').attr('id', 'sub_item_' + newNum).attr('name', 'sub_item_' + newNum);
    newSection.find('input[name^="other_item_"]').attr('id', 'other_item_' + newNum).attr('name', 'other_item_' + newNum);
    newSection.insertAfter('#pq_entry_' + num).last();

    $('#btnDel').click(function () {
        var num = $('.clonedSection').length; // how many "duplicatable" input fields we currently have
        $('#pq_entry_' + num).remove(); // remove the last element

        // enable the "add" button
        $('#btnAdd').prop('disabled', '');

        // if only one element remains, disable the "remove" button
        if (num - 1 == 1) $('#btnDel').prop('disabled', 'disabled');
    });
});


$('#btnDel').prop('disabled', 'disabled');

//Generate Dropdown
$('#item_count_1').change(function() {
    var option = $(this).val();
    showFields(option);
    return false;
});

function showFields(option){ 
    var content = '';
    for (var i = 1; i <= option; i++){
        content += '<div id="item_'+i+'"><label>Item # '+i+'</label><br /><label>Item Name:</label> <select id="item_name_'+i+'" name="item_name_'+i+'" class="course_list"><option value="" >--- Select ---</option><option value="apples" >apples</option><option value="banana" >banana</option><option value="mango" >mango</option></select></div>';  
    }
    $('#item_names_1').html(content);
}

HTML

<ul id="pq_entry_1" class="clonedSection">
  <li style="list-style-type: none;">
    <input id="first_item_1" class="main-item" name="main_item_1" type="checkbox"><label>First Item</label>
  </li>
  <li style="list-style-type: none;">
    <input id="second_item_1" class="main-item" name="main_item_1" type="checkbox"><label>Second Item</label>
  </li>
  <ul class="sub-item" style='display: none;'>
    <li style="list-style-type: none;">
      <label>
        How many items:
        <small>required</small>
      </label>
      <select id="item_count_1" name="item_count_1" class="medium" required>
        <option value="">---Select---</option>
        <option value="1">1</option>
        <option value="2">2</option>
      </select>
    </li>
    <li style="list-style-type: none;">
      <div id="item_name_1"></div>
    </li>
  </ul>
</ul>
  • 2
    Have you considered not using id's on elements that aren't unique? that completely removes the need to keep them unique. – Kevin B Jan 31 '14 at 20:34
  • @Kevin_B but I am submitting and storing the values in a database. –  Jan 31 '14 at 21:28
  • You don't need ID's to do that, just names. And even then, you don't *need* names to be unique. And if your server-side required them to be, you could make them unique before you submit it to the server rather than while you're generating them. – Kevin B Jan 31 '14 at 21:37
  • @KevinB Awesome, could you show a working example in an answer format –  Jan 31 '14 at 21:38
  • I don't feel compelled to pick through your code at this time, sorry. – Kevin B Jan 31 '14 at 21:40
  • @KevinB haha thanks for being honest. –  Jan 31 '14 at 21:42
  • I didn't want to leave you sitting here thinking i was coming up with a working solution, :p – Kevin B Jan 31 '14 at 21:43
  • Using names to generate arrays wouldn't solve your problem? `name="main_item[]"`. – azeós Feb 02 '14 at 19:11
  • @azeós not sure how to approach it using `name="main_item[]"` –  Feb 02 '14 at 19:27
  • @techAddict82 Which backend technology do you use to recive the data? – Jan Hommes Feb 02 '14 at 21:03
  • @JanHommes I am using PHP –  Feb 02 '14 at 21:14
  • 1
    Your problem started the minute you've done `$('#pq_entry_' + num)` - it is very incorrect to do this. This stores data in the presentation layer and shows lack of separation of concepts, global state, and not using data structures properly (arrays). – Benjamin Gruenbaum Feb 02 '14 at 21:24
  • 1
    @techAddict82 then go ahead and use the array method like mentioned by Cranio. Forget about dynamic IDs on the client side. IDs are not designed for this. – Jan Hommes Feb 02 '14 at 21:24

3 Answers3

19

So, let's talk about how to build basic GUI applications. Before we proceed I'd like you to know the code below can be written in ~20 LoC in Knockout/Angular but I chose not to because that wouldn't really teach anyone anything.

So, let's talk about GUI.

It all boils down to two things.

  • Presentation - this is your HTML, css and whatever the user directly interacts with.
  • Data - this is your actual data and logic.

We want to separate them so that they can act independently. We want an actual representation of what the user sees in JavaScript object so it'll be maintainable, testable readable and so on and so on. See Separation of Concerns for more information.

Let's start with the data.

So, what does each thing have in your application?

  • A First Item, either true or false
  • A Sub Item either true or false, but never true if the First Item isn't true.
  • A Second Item which is either true or false.
  • A Number of Items which is a number
    • Each of these items is an apple, banana or mango

The most intuitive thing is to start right there.

// our item, like we've just described it :) 
function Thing(){ //we use this as an object constructor.
    this.firstItem = false;
    this.subItem = false;
    this.secondItem = false;
    this.numItems = 0;
    this.items = []; // empty list of items
}

Well, that's a thing, we can now create them with new Thing() and then set their properties, for example thing.firstItem = true.

But we don't have a Thing we have stuff. Stuff is just an (ordered) bunch of things. An ordered collection is commonly represented by an array in JavaScript, so we can have:

var stuff = []; // our list
var thing = new Thing(); // add a new item
stuff.push(thing); // add the thing we just created to our list

We can of course also communicate this to PHP when submitting. One alternative is submitting a JSON object and reading that in PHP (this is nice!), alternatively we can serialize it as form params (if you have any trouble with the methods in that question - let me know).

Now I just have a bunch of objects... and a headache.

Quite astute. So far you only have objects, you did not specify their behavior anywhere. We have our 'data' layer, but we don't have any presentation layer yet. We'll start by getting rid of all the IDs and add behavior in.

Enter templates!

Instead of cloning existing objects we'll want to have a 'cookie cutter' way to create the looks of new elements. For this we'll use a template. Let's start by extracting how your 'item list' looks into an HTML template. Basically, given your html it's something like:

<script type='text/template' data-template='item'>

<ul class="clonedSection">
  <li style="list-style-type: none;">
    <label><input class="main-item" type="checkbox" />First Item</label>
    <ul class="sub-item" style="display: none;">
      <li style="list-style-type: none;">
        <label><input type="checkbox" />Sub Item</label>
      </li>
    </ul>
  </li>
  <li style="list-style-type: none;">
    <label>
      <input class="main-item" type="checkbox" />Second Item</label>
    <ul class="sub-item" style='display: none;'>
      <li style="list-style-type: none;">
        How many items:
        <select class="medium" required>
          <option value="">---Select---</option>
          <option value="1">1</option>
          <option value="2">2</option>
        </select>
      </li>
      <li style="list-style-type: none;"><div></div></li>
    </ul>
  </li>
</ul>
</script>

Now let's create a 'dumb' method for showing the template on the screen.

var template;
function renderItem(){
    template = template || $("[data-template=item]").html();
    var el = $("<div></div>").html(template);
    return el; // a new element with the template
} 

[Here's our first jsfiddle presentation demo](http://jsfiddle.net/RLRtv/, that just adds three items, without behavior to the screen. Read the code, see that you understand it and don't be afraid to ask about bits you don't understand :)

Binding them together

Next, we'll add some behavior in, when we create an item, we'll couple it to a Thing. So we can do one way data binding (where changes in the view reflect in the model). We can implement the other direction of binding later if you're interested but it's not a part of the original question so for brevity let's skip it for now.

function addItem(){
    var thing = new Thing(); // get the data
    var el = renderItem(); // get the element
    el. // WHOOPS? How do I find the things, you removed all the IDs!?!?
}

So, where are we stuck? We need to append behavior to our template but normal HTML templates do not have a hook for that so we have to do it manually. Let us begin by altering our template with 'data binding' properties.

<script type='text/template' data-template='item'>

<ul class="clonedSection">
    <li style="list-style-type: none;">
        <label>
            <input class="main-item" data-bind = 'firstItme' type="checkbox" />First Item</label>
        <ul class="sub-item" data-bind ='subItem' style="display: none;">
            <li style="list-style-type: none;">
                <label>
                    <input type="checkbox" />Sub Item</label>
            </li>
        </ul>
    </li>
    <li style="list-style-type: none;">
        <label>
            <input class="main-item" data-bind ='secondItem' type="checkbox" />Second Item</label>
        <ul class="sub-item" style='display: none;'>
            <li style="list-style-type: none;">How many items:
                <select class="medium" data-bind ='numItems' required>
                    <option value="">---Select---</option>
                    <option value="1">1</option>
                    <option value="2">2</option>
                </select>
            </li>
            <li style="list-style-type: none;">
                <div data-bind ='items'> 

                </div>
            </li>
        </ul>
    </li>
</ul>
</script>

See all the data-bind attributes we added? Let's try selecting on those.

function addItem() {
    var thing = new Thing(); // get the data
    var el = renderItem(); // get the element
    //wiring
    el.find("[data-bind=firstItem]").change(function(e){
       thing.firstItem = this.checked;
        if(thing.firstItem){//show second item
            el.find("[data-bind=subItem]").show(); //could be made faster by caching selectors
        }else{
            el.find("[data-bind=subItem]").hide();
        }
    });
    el.find("[data-bind=subItem] :checkbox").change(function(e){
        thing.subItem = this.checked;    
    });
    return {el:el,thing:thing}
}

In this fiddle we've added properties to the first item and sub item and they already update the elements.

Let's proceed to do the same for the second attribute. It's pretty much more of the same, binding directly. On a side note there are several libraries that do this for you automatically - Knockout for example

Here is another fiddle with all the bindings set in, this concluded our presentation layer, our data layer and their binding.

var template;

function Thing() { //we use this as an object constructor.
    this.firstItem = false;
    this.subItem = false;
    this.secondItem = false;
    this.numItems = 0;
    this.items = []; // empty list of items
}

function renderItem() {
    template = template || $("[data-template=item]").html();
    var el = $("<div></div>").html(template);
    return el; // a new element with the template
}

function addItem() {
    var thing = new Thing(); // get the data
    var el = renderItem(); // get the element
    el.find("[data-bind=firstItem]").change(function (e) {
        thing.firstItem = this.checked;
        if (thing.firstItem) { //show second item
            el.find("[data-bind=subItem]").show(); //could be made faster by caching selectors
        } else {
            el.find("[data-bind=subItem]").hide();
        }
    });
    el.find("[data-bind=subItem] :checkbox").change(function (e) {
        thing.subItem = this.checked;
    });
    el.find("[data-bind=secondItem]").change(function (e) {
        thing.secondItem = this.checked;
        if (thing.secondItem) {
            el.find("[data-bind=detailsView]").show();
        } else {
            el.find("[data-bind=detailsView]").hide();
        }
    });
    var $selectItemTemplate = el.find("[data-bind=items]").html();
    el.find("[data-bind=items]").empty();

    el.find("[data-bind=numItems]").change(function (e) {
        thing.numItems = +this.value;
        console.log(thing.items);
        if (thing.items.length < thing.numItems) {
            for (var i = thing.items.length; i < thing.numItems; i++) {
                thing.items.push("initial"); // nothing yet
            }
        }
        thing.items.length = thing.numItems;
        console.log(thing.items);
        el.find("[data-bind=items]").empty(); // remove old items, rebind
        thing.items.forEach(function(item,i){

            var container = $("<div></div>").html($selectItemTemplate.replace("{number}",i+1));
            var select = container.find("select");
            select.change(function(e){                
                thing.items[i] = this.value;
            });
            select.val(item);
            el.find("[data-bind=items]").append(container);

        })

    });
    return {
        el: el,
        thing: thing
    }
}

for (var i = 0; i < 3; i++) {
    var item = addItem();
    window.item = item;
    $("body").append(item.el);
}

The buttons

The fun thing is, now that we're done with the tedious part, the buttons are a piece of cake.

Let's add the "add" button

 <input type='button' value='add' data-action='add' />

and JavaScript:

var stuff = [];
$("[data-action='add']").click(function(e){
     var item = addItem();
     $("body").append(item.el);
     stuff.push(item);
});

Boy, that was easy.

Ok, so remove should be pretty hard, right?

HTML:

<input type='button' value='remove' data-action='remove' />

JS:

$("[data-action='remove']").click(function(e){
     var item = stuff.pop()
     item.el.remove();
});

Ok, so that was pretty sweet. So how do we get our data? Let's create a button that shows all the items on the screen?

<input type='button' value='show' data-action='alertData' />

and JS

$("[data-action='alertData']").click(function(e){
    var things = stuff.map(function(el){ return el.thing;});
    alert(JSON.stringify(things));
});

Woah! We have an actual representation of our data in our model layer. We can do whatever we want with it, that's pretty sweet.

What if I want to submit it as a form? $.param to the rescue.

<input type='button' value='formData' data-action='asFormData' />

And JS:

$("[data-action='asFormData']").click(function(e){
    var things = stuff.map(function(el){ return el.thing;});
    alert($.param({data:things}));
});

And while this format is not very nice it's something PHP (or any other popular technology) will gladly read on the server side.

So to wrap it up

  • Separate presentation from data
  • If you have JS logic - have a single source of truth - JavaScript objects
  • Consider reading about it more, learn about common frameworks like KnockoutJS or AngularJS which have interesting less verbose solutions to this problem (at the cost of assumptions).
  • Read more about UI architecture. This is a good (but hard for beginners) resource
  • Avoid duplicate IDs, they're bad - while you're there don't store data in your dom.
  • Don't be afraid to ask question - this is how you learn.
  • You can pretty easily get rid of jQuery here.
Community
  • 1
  • 1
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 3
    Just a side note - if you have any questions about this or want to discuss aspects me and a bunch of other guys are often in the [Stack Overflow Chat JavaScript Room](http://chat.stackoverflow.com/rooms/17/javascript) . Structuring UI well is pretty hard to learn - if you have any doubts please let me know, that's how I learn to explain this better. If anything my students will appreciate it in the future. – Benjamin Gruenbaum Feb 02 '14 at 23:21
  • 1
    Sorry for the long wait, It took me sometime to go through this answer thoroughly. This goes above and beyond. I will keep those helpful tips in mind. One more thing: what would be your advice if I was loading the values from mysql db into these checkboxes. The user would have the ability to add more values and resubmit form but the user can't delete the pre-existing fields of values loaded from the db. –  Feb 03 '14 at 05:02
  • Also, the `remove` button takes everything out. Is it possible to remove only a set of fields at a time for every click. Disable `remove` when there is only one set of field left. –  Feb 03 '14 at 05:35
  • @techAddict82 all these are of course possible. I'd load JSON into JavaScript from the server and serialize that into 'stuff' calling `addThing` on it `N` times. You can add a 'fromServer' property (Just like you described!) and check it in the `remove` button. As for taking things out, it's possible to check for `stuff.length` when removing things. (`if(stuff.length > 1) {/*remove code */}`), I'd like you to try these things (remember you also need to check this on the server side! You can't trust the integrity of data from the client). In two days if you're unable please let me know. – Benjamin Gruenbaum Feb 03 '14 at 09:28
  • Oh, and if you have any trouble with this, please come to the JS room and we can discuss it. If it becomes a problem I can edit it into the answer - I'd really like to get an idea of what's clear and what's not here :) I'd also recommend looking at how frameworks deal with these issues at http://learn.knockoutjs.com for example :) – Benjamin Gruenbaum Feb 03 '14 at 09:29
  • I am trying your recommendations out. Thanks for the help so far. I am having no luck returning some values for an input type="text" fields. [JSFIDDLE](http://jsfiddle.net/q3aMg/3/) –  Feb 03 '14 at 19:29
  • You are using `.val()` instead of `.value` on a DOM element, it's a good start though. I've also replied in chat. – Benjamin Gruenbaum Feb 03 '14 at 19:40
  • Don't mean to be bothersome, but in regards to one of my intial comments of loading data in the input fields you mentioned loading JSON into JavaScript from the server and then serialize that into 'stuff' calling addThing on it N times. I was able to get part 1 done which is to place the values into JSON, could you briefly show me part 2 which is to " serialize that into 'stuff' calling addThing on it N times"? –  Feb 24 '14 at 21:27
  • This is an over 20 day year old question now, adding information to it will make it less relevant. You can hit me up in the chat. In a sentence, it's iterating through the values from the array N times, creating `Thing` objects with them, then iterating through those and calling `addThing` on each of them. – Benjamin Gruenbaum Feb 24 '14 at 21:33
  • Made it into a formal question: http://stackoverflow.com/questions/22004438/populating-input-fields-with-values-from-json-object –  Feb 25 '14 at 05:37
1

My approach would be:

First of all, a correct use of <label>

<label><input ... /> My label</label>

and not

<input><label>...</label>

By doing it in the first way, you make sure that the label is clickable just like you were clicking the checkbox, mantaining the accessibility


On the second hand, there's too much string magic. Just use a data-xxx attribute where it suits well:

<ul class='pq_entry' data-id='1'>
     ....
</ul>

so you can find an element by its data-id attribute:

var myFirstSection = $("ul.pq_entry[data-id=1]");

By doing so there is no need, in many elements, to set an id attribute at all, because you can simply use class and find the single items by traversing the DOM. For example, main_item becomes:

 <input class="main-item" name="main_item[]" type="checkbox">

If for some reason you need to find this item in cloned section 3, you can therefore do:

var mySection = 3;
$("ul.pq_entry[data-id=" + mySection + "] .menu_item").someFancyMethod(...);

When you clone a section, you can assign a data-xxx attribute dynamically, as in:

var myNewId = myOldId + 1;
$clonedSection.data("id", myNewId);

Then, I'll use name arrays like main_item[] so you don't need to specify manually an id in the name, but you must limit this approach to the elements that appear only once in the cloned sections.

A name array means that when you retrieve the value from the form, from server-side (for example by using $_POST in PHP), you obtain an array of values in the exact order in which they appear in the form. Just like a regular array in any language you can access the items in the sections like (example in PHP):

$_POST['main_item'][0] // for section 1
$_POST['main_item'][1] // for section 2
... and so on
Cranio
  • 9,647
  • 4
  • 35
  • 55
  • 1
    -1 This looks like a real effort to help, which is great but it gets the way data should be structured in an application completely wrong by storing application state in the presentation layer (which OP was already doing) by abusing data attributes. Please consider correcting that (using JavaScript arrays might be a good start). – Benjamin Gruenbaum Feb 02 '14 at 21:36
  • @BenjaminGruenbaum: if I had it done by myself, I would have used a completely different approach (depending on the project, a more structured Javascript storing the application state elsewhere, or maybe even the use of a framework like AngularJS, to have a total decoupling from the presentation layer). I've tried to push the OP to a more organized level, though I reckon this is far for a well-behaved best-practice code. – Cranio Feb 02 '14 at 21:41
  • Using AngularJS for this is overkill. You do not need a huge framework in order to structure code. If you understand the problem that just makes this answer worse. I'm confident that you can offer a better solution - please do. As it's currently stands this answer is promoting a really bad practice. – Benjamin Gruenbaum Feb 02 '14 at 21:43
  • As I stated, "it depends on the project". Even mentioning AngularJS has no sense if we isolate the particular problem from its context (I did only to say "there are different approaches"). That said, I think your point is good. I'll try to give a better answer – Cranio Feb 02 '14 at 21:51
0

Try breaking up your code for better management.

For scenarios like above,

HTML

Have resuable chunks of html hidden away in a template:

<div class="form-template"> <!-- will pull form section template from here -->
    <ul data-custom-attributes="" data-id="formSectionIdPrefix" class="form-section">
        <li>
            <input data-custom-attributes="" data-id="firstCheckBoxIdPrefix" data-name="firstCheckBoxNamePrefix" class="main-item checkbox1" type="checkbox" />
            <label>First Item</label>

            <ul class="sub-item" style="display:none;">
                <li>
                    <input type="checkbox" />
                    <label>Sub Item</label>
                </li>
                <li>
                    <input class="main-item" data-id="checkBoxSubItem2IdPrefix" data-name="checkBoxSubItem2NamePrefix" type="checkbox" />
                    <label>Second Item</label>

                    <ul class="sub-item" style="display:none;">
                        <li>
                            <label>How many items:</label>
                            <select data-custom-attributes="" data-id="selectItem1IdPrefix" data-name="selectItem1IdPrefix" class="medium" required>
                                <option value="">---Select---</option>
                                <option value="1">1</option>
                                <option value="2">2</option>
                            </select>
                        </li>
                        <li style="list-style-type: none;">
                            <div data-custom-attributes="" class="dependant-select" data-id="selectItem2IdPrefix"></div>
                        </li>
                    </ul>
                </li>
            </ul>
        </li>
    </ul>
</div>

<div class="select-template hidden"> <!-- will pull dependant select template -->
    <select class="course_list" data-id="dependantSelectIdPrefix">
        <option value="">-- select --</option>
        <option value="apple">apples</option>
        <option value="apple">bananas</option>
    </select>
</div>

<div class="form-area"> <!-- main area to append form sections to -->
</div>

<div class="form-area-controls"> <!-- form controls -->
    <input type='button'  class="button tiny radius" id='btnAdd' value='Add Another' />
    <input type='button'   class="button tiny radius alert" id='btnDel' value='Delete Last' />
</div>

CSS A tiny bit of CSS to make sure our templates never show up on the screen

.form-template {
    display:none;
}
.form-area li,
#main-panel li {
    list-style-type: none;
}

.hidden {
    display:none;
}

JS

Start with a config object to easily manage attributes

var config = {};

config.formSectionIdPrefix = "pq_entry_";

config.firstCheckBoxIdPrefix = "first_item_";
config.firstCheckBoxNamePrefix = "main_item_";

config.checkBoxSubItem1IdPrefix = "sub_item_";
config.checkBoxSubItem1NamePrefix = "sub_item_";

config.checkBoxSubItem2IdPrefix = "second_item_";
config.checkBoxSubItem2NamePrefix = "main_item_";

config.selectItem1IdPrefix = "item_count_";
config.selectItem2IdPrefix = "item_names_";
config.dependantSelectIdPrefix = "item_";

Cache refrence to FormSectionTemplate, SelectDropdownTemplate and FormArea

var $formTemplate = $(".form-template");
var $selectTemplate = $(".select-template");
var $formArea = $(".form-area");

And probably an index variable to keep track of Id increments

var index = 0;

Have a helper method getFormTemplate that does the following:

clones form section

attaches events to that cloned form section

increments ids of the cloned section ( more on this further down )

 function getFormTemplate() {
    var $newTemplate = $formTemplate.children().clone(true);

    var $formSectionWithEvents = attachEvents( $newTemplate );

    var $formSectionWithUpdatedAttributes = incrementAttributes( $formSectionWithEvents );

    return $formSectionWithUpdatedAttributes;
}

Attaching events to the cloned form section attachEvents

function attachEvents( $formSection ) {
    var $mainCheckBoxes = $formSection.find( ".main-item" );
    var $selectBox = $formSection.find( ".medium" );
    var $dependantSelectSection = $formSection.find( ".dependant-select" );

    $mainCheckBoxes.on("click", function() {
        var $this = $( this );
        var $subItem = $this.siblings(".sub-item");
        if ( $this.is(":checked") ) {
            $subItem.show();
        } else {
            $subItem.hide();   
        }
    });

    $selectBox.on("change", function() {
        var option = $(this).val();

        var $dependantSelect = getSelectField( option );

        $dependantSelectSection.children().remove();
        $dependantSelectSection.append( $dependantSelect );
    });

    return $formSection;
}

Incrementing Ids of the cloned form section.

Well, there are many ways to approach it ( which largely depends on the amount of caffeine you have )

In the below bit, we're looking for all the elements that are flaged with data-custom-attributes

iterating through all those elements and findind out what id and name key we should be looking for in the config section and then assign those values appending the index incrementer.

function incrementAttributes( $formSection ) {
    index = index + 1;
    var $customAttributeElements = $formSection.find("[data-custom-attributes]");

    $customAttributeElements.each( function() {
        var $this = $(this);

        var idNamePrefix = $this.attr( "data-id" );
        var namePrefix = $this.attr( "data-name" );

        var idName = config[idNamePrefix] + index;
        var name = config[namePrefix] + index;

        $this.attr( "id", idName );
        $this.attr( "name", name );
    });

    return $formSection;
}

Get Dependant select field ( tirggered by onchange event on the select dropdown )

It simply recieves the value from parent select box and assigns it to the cloned select box's ids etc with the prefixes from the config object.

function getSelectField( indexValue ) {
    var $selectItem = $selectTemplate.find("select").clone();

    var selectElementIdPrefix = $selectItem.attr("data-id");
    var selectElementId = config[selectElementIdPrefix] + indexValue;

    $selectItem.attr( "id", selectElementId );

    return $selectItem;
}

putting it all togather

$("#btnAdd").on("click", function(e) {
     e.preventDefault();

    var $formSection = getFormTemplate();
    $formArea.append($formSection);

});

$("#btnDel").on("click", function(e) {
    e.preventDefault();
    $formArea.children().last().remove();

    if ( index > 0 ) {
        index = index - 1;    
    }
});

Only thing to mention on events is that #btnDel decrements the index to make sure next form section insertion has the correct ids attached.

JS fiddle: http://jsfiddle.net/Varinder/3VT2w/3/

EDIT

Just noticed there were some HTML tags mismatch in the fiddle above ( fixed )

And drop down select was supposed to add 1 or more child dropdowns based on selection.

Which can be done by altering change event on $selectBox to below:

$selectBox.on("change", function() {
    var option = $(this).val();

    var optionInt = parseInt( option );

    $dependantSelectSection.children().remove();

    for ( var i = 0; i < optionInt; i++ ) {
        var $dependantSelect = getSelectField( option );
        $dependantSelectSection.append( $dependantSelect );     
    }

});

Updated Fiddle: http://jsfiddle.net/Varinder/3VT2w/4/

EDIT 2

Adding child select items names with increment:

$selectBox.on("change", function() {
    var option = $(this).val();

    var optionInt = parseInt( option );

    $dependantSelectSection.children().remove();

    for ( var i = 1; i <= optionInt; i++ ) {
        var $dependantSelect = getSelectField( option );
        $dependantSelectSection.append( "item" + i );
        $dependantSelectSection.append( $dependantSelect );     
    }

});

Updated Fiddle: http://jsfiddle.net/Varinder/3VT2w/5/

Varinder
  • 2,634
  • 1
  • 17
  • 20