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>
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.
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.
So, what does each thing have in your application?
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).
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.
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 :)
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 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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With