Complex dynamic form, add/remove fields dynamically

Hey guys,

I am working on a dynamic form using JavaScript where the user can add/remove fields as required. Here is a small mock up of what I am trying to achieve: http://img176.imageshack.us/my.php?image=ssuc3.gif

For the purpose of this question, you can ignore the cardio and only look at the weight/weight training aspect. As you can see, the user is:

  • Presented with a form where he can choose to add a weight (weight training exercise).
  • If he clicks add “weight” exercise, the form will be populated with a field allowing him to enter the name of the exercise (ie “benchpress”). He can also choose to click remove the exercise, which will remove it.
  • He can then click add set, which will populate a child field under the exercise allowing him to enter the details of 1 set of that exercise.

The main problem I am running to is how to keep track of all these fields in a way that makes sense when I am processing the form. I am pulling my hair out trying to figure this out. I was going to use a counter (ie var numWeightEntries) to name the divs (div id=“weight_1_”, “weight_2”, etc) but I found this to be a problem when there are childs to each div, and also due to the fact that they can remove fields (you run into the problem with it not being contiguous). I have also tried to structure it like below but still running into problems:


<form>

	Workout Name: <input type="text" name="workout_name" />
	
	<div id="cardios_in_workout">
		<div id="cardio_1">
				
		</div>
		<div id="cardio_2">
		
		</div>
	</div>
	
	<div id="weight_trainings_in_workout">
	
		<div class="weight_training">
		
			Weight Training Name: <input type="text" name="weight_training_names[]"><a href="#" onClick="addSetToWeight(weight_1);">Add Set</a>
			
			<div class="sets">
				<div class="set">
					Reps: <input type="text" name="weight_training_names[0][reps][]">
					Weight: <input type="text" name="weight_training_names[0][weights][]">
                    <a href="#" onClick="removeSetFrom(??);">Remove Set</a>
				</div>
			</div>
			
		</div>
		
		<div class="weight_training">
		
			Weight Training Name: <input type="text" name="weight_training_names[]"><a href="#" onClick="addSetToWeight(weight_1);">Add Set</a>
			
			<div class="sets">
				<div class="set">
					Reps: <input type="text" name="weight_training_names[1][reps][]">
					Weight: <input type="text" name="weight_training_names[1][weights][]">
                   <a href="#" onClick="removeSetFrom(??);">Remove Set</a>
				</div>
			</div>
			
		</div>
		
	</div>
	
	<input type="submit" name="submit" value="Save workout">
	<input type="button" name="cancel" value="Cancel">
	
</form>


Any advice on how to approach this would be much appreciated. Thanks!

You don’t need a fancy schema for this. Each set can have a separate number for them. When the set has multiple names with the same name, you can access them from the server as if they were an array.

Here is a working sample of the form sets, without identifiers or class names. The purpose being to develop a solution that doesn’t rely on them.

Note that all of the reps and weights in the first set will have an index number of 1, so if you add 4 sets you will have five inputs all called reps[1] and"weights[1]. This helps to group them together while allowing you to keep the individual sets separate.


<div>
	Weight Training Name: <input type="text" name="weight_training_name"><a href="#" onClick="addSet(this);">Add Set</a>
	<div>
		<div>
			Reps: <input type="text" name="reps[1][]">
			Weight: <input type="text" name="weights[1][]">
			<a href="#" onClick="removeSet(this);">Remove Set</a>
		</div>
	</div>
</div>
<div>
	Weight Training Name: <input type="text" name="weight_training_name"><a href="#" onClick="addSet(this);">Add Set</a>
	<div class="sets">
		<div class="set">
			Reps: <input type="text" name="reps[2][]">
			Weight: <input type="text" name="weights[2][]">
		   <a href="#" onClick="removeSet(this);">Remove Set</a>
		</div>
	</div>
</div>

The script to add and remove sets is fairly simple


function addSet(el) {
	var set = el.parentNode.getElementsByTagName('div')[0];
	var num = getNumberOfExercises(set);
	set.appendChild(createSet(num + 1));
}
function removeSet(el) {
	el.parentNode.parentNode.removeChild(el.parentNode);
}

The addSet() function gets the number of existing sets with the following:


function getNumberOfExercises(el) {
	el = el.firstChild;
	var num = 0;
	while (el) {
		if (el.nodeName === 'DIV') {
			num += 1;
		}
		el = el.nextSibling;
	}
	return num;
}

And the sets themself are created with this:


function createSet(num) {
	var div = document.createElement('div');
	var reps = document.createElement('input');
	var weight = document.createElement('input');
	var a = document.createElement('a');
	reps.type = 'text';
	reps.name = 'reps[' + num + '][]';
	weight.type = 'text';
	weight.name = 'weights[' + num + '][]';
	a.href = '#';
	a.onclick = function () {
		removeSet(a);
	}
	div.appendChild(document.createTextNode('Reps: '));
	div.appendChild(reps);
	div.appendChild(document.createTextNode(' '));
	div.appendChild(document.createTextNode('Weight: '));
	div.appendChild(weight);
	div.appendChild(document.createTextNode(' '));
	a.appendChild(document.createTextNode('Remove Set'));
	div.appendChild(a);
	return div;
}

edit: field names updated to be accessible as arrays from php

pmw, thanks for the help so far - very useful.

However, I think your solution will have problems when sets are removed. The numbers will not be contiguous?

Here is how the elements will be named when the first lot contains 3 sets, and the second lot contains two sets

weight_training_name = First lot

  • reps[1] = 20
  • weight[1] = 40
  • reps[1] = 15
  • weight[1] = 40
  • reps[1] = 10
  • weight[1] = 30
    weight_training_name = Second lot
  • reps[2] = 20
  • weight[2] = 40
  • reps[2] = 15
  • weight[2] = 40

So when from the first lot you remove the second set, there are no contiguous issues to worry about.

edit: field names updated to be accessible as arrays from php

I am actually talking about when you remove an entire exercise. You lose the contiguity of the reps/weight naming scheme.

For example:

function addSet(el) {
var set = el.parentNode.getElementsByTagName(‘div’)[0];
var num = getNumberOfSets(set);
set.appendChild(createSet(num + 1));
}

First off, I think for “var num=”, you actually meant to get the number of exercises? Cause that is what the numbering is based off of (which weight training exercise group it belongs to?).

So if you create WeightTraining_1, WeightTraning_2 and WeightTraining_3, and you delete the second one. If you create a fourth one, its numbering will be reps_4, weight_4 and you have contiguity issues with the #2.

Let me know if I have overlooked something.

thanks!

That’s correct. The function should be called getNumberOfExercises()

The situation with exercises being added and removed hasn’t been covered yet, not in the code I posted nor anywhere else.

I’ve dealt with the contiguous issue before in other projects, and there’s a very simple solution that’s very hard to break. Rename the fields when removal occurs.

When whole exercises are removed, the script for doing that would be easily able to walk through the form and ensure that the index number for each exercise is as it should be.

With four exercises, when the second one is removed the script would walk through the fields of each exercise renaming the names of the fields.

The first exercise would have the sets renamed to reps[1] and weight[1], which wouldn’t have much effect on that exercise as it’s renaming them to what they already are.

The second exercise though demonstrates the payoff. That would be renamed from reps[3] and weight[3] to reps[2] and weight[2]. And as the script walks through each field , the act of renaming the appropriate fields results in them all being automatically contiguous.

The following is an outline of the solution.


function removeExercise(el) {
    el.parentNode.removeChild(el);
    renumberSets();
}
function renumberSets() {
    var els = getEachExercise();
    var i;
    for (i = 0; i < els.length; i++) {
        setFieldNumbers(els[i], i + 1);
    }
}

edit: field names updated to be accessible as arrays from php

Thanks, Ill give it a try and report back.

BTW, the reps1, weights1 need to be reps1 right? To be posted as an array?

Yups reps1, weights1 need to be reps1 and weights1

You would probably want them reps[1] for easy collection in php.

Just an update guys - it works wonderfully, thanks for the help.

Btw: I found it easier to use set[1] rather then set1 since it helps PHP group them after the form is posted, and you can just do a foreach statement rather then trying to generate the name of the variables (as crmalibu pointed out).

That’s excellent. I’ll go back and update the named elements in my code so that others who may copy/paste will have less issues to work through.

Btw guys, can you compare div elements like this?



    if (parentDiv.getLastChild() == someDiv)


And also btw, you cannot just use “getNumExercises()” to group the set with a workout exercise. Because you could add all the exercises first, and then go back and add sets, in which case all the sets will be associated with the last exercise added. So instead, you need to identify exactly which exercise’s add set button was pressed.

Yes, that’s a perfectly good way to compare elements.

So what should happen is the script should count through each of the <div class=“sets”> elements until it reaches the one from which the addset event occured.

very good helping material. i try to add/Remove Exercises but this code not support, please help.

i am new in JavaScript.

code not support? That’s very close to “it doesn’t work” which is something that tends to require more information so that further assistance can be given.

Sorry Dear, i do not have any experience in javascript that’s why i was failed to apply this code in my project. kindly me in this regard.

basically i want to add this div onclick

<div>
Weight Training Name: <input type=“text” name=“weight_training_name”><a href=“#” onClick=“addSet(this);”>Add Set</a>
<div>
<div>
Reps: <input type=“text” name=“reps[1]”>
Weight: <input type=“text” name=“weights[1]”>
<a href=“#” onClick=“removeSet(this);”>Remove Set</a>
</div>
</div>
</div>

other code are fit… :slight_smile:

If you put up a demo page, we can examine how the script interacts with your HTML code and most effectively provide you with the best action to take.

Thanks for your reply,

i want to make page like add/remove attributes and there properties in joomla virtuemart see in attachment.

Thanks Again,

Well we’re very happy to help you resolve any difficulties that you have with your own coding efforts.

If however you require someone to code it up for you, there is the Marketplace where you may engage the services of someone to do the work for you.