I was bored so I refined this some for you, I actually thought of making a jQuery plugin out of it.
I have a pretty good plugin "template" on my GitHub but it's kind of long to put in here (although I may still do it)
//to add more groups, put their "Unique" name in here
var group_names = ['Primary','Secondary','Backup'];
//to add more members simple increase this number
var max_members = 3;
//default number of member to add when creating a new group
var min_members = 1;
/*
* internal tracking of which groups are on the page
* -note- We only want 1 instance of each group, we
* we need a sophisticated way to trak them
*
* schema:{groupname : group_id}
*/
var active_groups = {};
/*
* internal tracking of which member are active in which group
* -note- We dont need to keep track of specific member_ids (I think)
* therefor we can use a simpler method of traking. However, this will
* cause some member ids to go missing if we do multiple adds and deletes
* @example (we could have)
* <input name="member[0][1] ...> and then <input name="member[0][5] ...>
*
* this should be OK because the member_id is hidden, and this is better then
* having duplicate member_ids.
* @example (if we did no traking)
* <input name="member[0][1] ...> and then another <input name="member[0][1] ...>
*
* schema:{groupname : member_counter}
*/
var active_members = {};
/**
* get the first un-used group id
* this is the index of group_names if not in active_groups
* @return mixed [null|int]
*/
function get_group_id(){
var group_id = false,group_name,i;
for(i in group_names){
//return only non-active groups
if(undefined !== (group_name = group_names[i])){
if(!active_groups[group_name]){
group_id = i;
//break the loop when we find an un-used group id
break;
}
}
}
//return the group id
return group_id;
}//END: get_group_id
/**
* get an un-used member
* @param object group - a single $('.group') A.K.A, $('#group'+group_id) element
* @return int
*/
function get_member_id(group){
if(!group) return false;
var group_name = group_names[group.data('group_id')];
if(undefined === active_members[group_name]){
//no members set to 0
active_members[group_name] = 0;
}else{
//increment the member counter
++active_members[group_name];
}
//return the current counter
return active_members[group_name];
}//END: get_member_id
/**
* add a member element to a specific group
* (we dont care about memember order)
*
* @param object member - a single $('.member') A.K.A. $('#member-'+group_id+'-'+member_id') element
* @return mixed [false|object] - false the group was not added | objec the group a single $('.group') A.K.A, $('#group'+group_id) element
*/
function add_member(group){
if(!group) return false;
//get the group id from the element
var group_id = group.data('group_id');
//we reached the max number of members
if(group.find('.member').size() >= max_members) return false;
//get a new member_id
var member_id = get_member_id(group);
/*
* JS doesn't have multi-line strings
* so we can abuse an array with join() for fun and profit
*
* template for members
*/
var member_template = [
'<div id="member-'+group_id+'-'+member_id+'" class="member" >',
"\t"+'<label>Member',
"\t"+'<input type="text" name="member['+group_id+']['+member_id+']" data-member_id="'+member_id+'" /></label>',
"\t"+'<button name="remove" >Remove</button>',
'</div>'
].join("\n");
group.find('.group-wrapper').append(member_template);
//return the member object
return $('#member-'+group_id+'-'+member_id);
}//END: add_member
/**
* add a group
* @return mixed [false|object] - false the group was not added | objec the group a single $('.group') A.K.A, $('#group'+group_id) element
* @throws error - when failing to add inital member
*/
function add_group(){
//get a new group name if avalible
var group_id = get_group_id();
//if there were no avalible group id then just return.
if(false === group_id) return false;
//the name of the group based on it's id
var group_name = group_names[group_id];
//template for our group
var group_template = [
'<fieldset id="group'+group_id+'" class="group" data-group_id="'+group_id+'" >',
"\t"+'<div class="group-wrapper">',
"\t\t"+'<legend>'+group_name+' Group </legend>',
"\t"+'</div>',
"\t"+'<button name="add_member" >Add Member</button>',
'</fieldset>'
].join("\n");
//place the group in its correct spot :)
//we will assume this will be before the next .group
var loaded = false;
//loop over each group in the collection
$('.group').each( function(){
//if $(this) group_id is larger then the new group id
//add the new group in before $(this) group
if($(this).data('group_id') > group_id){
$(this).before(group_template);
loaded = true;
//break the loop when we find what we want
return false;
}
});
//if there were no .group(s) or we couldnt prepend it
//then we will simple append it
if(!loaded){
$('#groups').append(group_template );
}
var group = $('#group'+group_id);
//activate the group
active_groups[group_name] = group_id;
//add the initial memeber(s) to the new group
for(var i=0;i<min_members;++i) add_member(group);
//return the group object
return group;
}//END:add_group
/**
* remove a group
*
*/
function remove_group(group){
if(!group) return false;
//and de-activate it, too
delete active_groups[group_names[group.data('group_id')]];
group.remove();
}
/*
* EventListner: add a new group
*/
$('button[name="add_group"]').click(function(e){
//the button click (sometimes) reloads the snipit window on SO
e.preventDefault();
add_group(true);
});//END: click EventListner
/**
* EventListner:add a member to a group
*
* dynamic content so we delegate
*/
$('#groups').on('click', 'button[name="add_member"]', function(e){
//the button click (sometimes) reloads the snipit window
e.preventDefault();
add_member($(this).closest('.group'));
});//END: click EventListner
/**
* EventListner:remove a member from a group
*
* if it's the last member also remove the group
* dynamic content so we delegate
*/
$('#groups').on('click', 'button[name="remove"]', function(e){
//the button click (sometimes) reloads the snipit window on SO
e.preventDefault();
//get the closest group to this button
var group = $(this).closest('.group');
var group_id = group.data('group_id');
//remove the member
$(this).closest('.member').remove();
//if there are no members left remove the group
if(!group.find('.member').size()){
remove_group(group);
}
});//END: click EventListner
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<form id="groups" ></form>
<button name="add_group" >Add Group</button>
Lets see how I did
As a user I can add a member to a group. At minimum, I need to enter one person for the one group, which will be displayed by default when I hit the page.
A minimum of one user is implemented but there are too many unknowns in this statement when I hit the page. Things such as what persistent storage do you use etc. I can't really code that if I don't know how you store it, or even what the id's represent.
That said, it should be easy to hook it up to PHP by just injecting some stuff in it. The will be displayed by default part is a bit ambiguous. For example: what is displayed by default? A saved member? A new group with one member that needs to be entered? I don't know ... etc.
I set the code up in a way that makes is easy to use. For example, if you want to add a group you just call:
add_group();
And then if you want to add a new member to a group, you just call:
add_member($('#group0'));
I can add additional members to a group, up to a maximum of three
Done! On top of that, you can also easily increase/decrease the number to anything you wish. All you have to do is change the value of max_members (right at the top of the code). For example:
var max_members = 8;
//setting this to 8 will let you have up to 8 member per group.
You can even change the minimum member elements it creates by doing this:
var min_members = 2;
//now when you create a new group it creates 2 members
For obvious reasons, if you set min_members=4 and max_members=2 it will only create 2 member elements.
I can also add additional groups, up to a maximum of three, each with up to three total members.
Done! As with the max_members setting, you can easily add/remove groups too. However, the groups have better tracking (and options, see below) then the members. For this I used a list of group names, instead of just a simple integer counter. You can add, remove or change the group names as you see fit. For example:
var group_names = ['Primary','Secondary','Backup', 'Foo'];
//just add a group_name, like "Foo", and presto chango. You got a new group type.
Unique Group Names
Now, because the group names get used as the key/property name of active_groups the group names have to be unique. If they are not it will throw some things off. You'll get members adding to the wrong group etc.. This behaviour may or may not be desirable. But, using them as keys was the easiest way to fix a few potential problems I saw. Things I would term "logical faults" or "Naive Implementation", I'll go over them below.
Group Order
The order the groups are displayed on the page is based on the list's order. So the code will maintain this order, even when deleting and adding multiple times in any sequence. For example: say you add all three groups, then you delete the middle one "Secondary". Then you decide to add it back in, it will not simply be appended after "Backup". Instead it will be inserted between "Primary" and "Backup", where it is in the list.
If we didn't do this, then when we add/remove groups they just become a jumbled mess after a while. This is what I meant by "Naive Implementation". Primary implies that its the first one <legend>Primary Group</legend>. Maybe you were just using that as an example? Buy I also saw this <legend>Backup Group</legend> so I don't know.
Group Priority
The priority the groups are added in is also based on the list. Again this is true even when deleting and adding multiple times in any sequence. For example: Say you add all three groups, then you delete the first and last ones "Primary" and "Backup". Then you add one group back in, it will choose to put the "Primary" group back fist as it has higher priority in the list.
It wouldn't be terribly difficult to add an optional argument in to add_group that would allow manual selection of the group (by id or name, for the challenge).
Again if we don't sort these by priority, they become a mess after a few deletes and adds. Another example of "Naive Implementation", because if we don't account for this we'll wind up with this order for you groups: "Backup group", "Secondary group" and then "Primary group". Which looks like ... junk, to be honest. So by having a priority we can fix all that shuffling that was taking place.
When I add an additional group, I need to, at a minimum, fill out one member's name
We sort of covered that in #1. However there are no requirements implemented on the form submission. That said you could easily add this in with a listener on the submission event of the form.
A few other things I managed to add in
Unique Member Ids
Member ids are unique, however these are not tracked as well as the group ids (see code comments). So it is possible to get member ids that are out of sequence. For example if we were to list them like they are in $_POST:
'member' => array(
0 => array( //group_id 0
1 => "text", //member_id 1
5 => "text", //member_id 5, because 2-4 were removed, then this was added."
), 1 => array( //group_id 1
//...
)
)
Now if we don't increment them, there is no easy way to prevent duplicate ids after a few adds and deletes. For example if we just counted the number of members and used that as the id. Say we add 3 members in 0,1 and 2 this works fine if we are counting the elements like group_id = group.find('.member').size(), at first. But if we delete 0 and then add a member back in, we now have a count of 2 members, so if just use that count we would get a duplicate number 2. Which is no good as when it's submitted one would get over-written on the server side. Therefore, we have to track these no matter what we do, and this was the simplest way I could think of.
But really the easiest thing here would be to stop tracking them all together, so inputs would be like this:
<input type="text" name="member['+group_id+'][]" data-member_id="'+member_id+'" /> //instead of member['+group_id+']['+member_id+']
//and PHP would handle this just fine
'member' => array(
0 => array( //group_id 0
0 => "text",
1 => "text",
), 1 => array( //group_id 1
//...
)
)
It wasn't clear in the question where these ID's come from or how important they are. Are they just an arbitrary number, are they linked to actual user accounts, I have no idea.
Remove group on member delete
Removing the last member from a group also removes the group, which saves us some code and eliminates a button for a cleaner UI.
Its pretty simple, if you want to add a remove group button in:
//in function add_group() add the button to the group_template
var group_template = [
'<fieldset id="group'+group_id+'" class="group" data-group_id="'+group_id+'" >',
"\t"+'<div class="group-wrapper">',
"\t\t"+'<legend>'+group_name+' Group </legend>',
"\t"+'</div>',
"\t"+'<button name="add_member" >Add Member</button><br>',
"\t"+'<button name="remove_group" >Remove Group</button>', //here is our new button
'</fieldset>'
].join("\n");
//and then at the bottom (outside of the function) add an event listener for the button.
$('#groups').on('click', 'button[name="remove_group"]', function(e){
//the button click (sometimes) reloads the snipit window
e.preventDefault();
remove_group($(this).closest('.group'));
});
By not removing the group when the last user is removed, we will have to (or we should) remove the "remove member" button on at least one of the members. But the question becomes which one? If we just do it on the first one, users will have to cut and paste text if they want to remove that one as it wont have a remove button. If we just leave the group there empty, then not only does it look bad but it may be confusing to our user. We would have to do validation on it, because the first member of the group is mandatory. So users would be forced to click another button to delete it anyway, or add a member back into it. Another example of "Naive Implementation" if we ignore it.
We can, with a bit of work, dynamically remove the last button when there is one "member" field left. Which sort of/mostlly solves the issue. To do that change this:
//in $('#groups').on('click', 'button[name="remove"]', function(e){
//if no member elements are left
if(!group.find('.member').size()){
remove_group(group);
}
And replace it with this:
// in $('#groups').on('click', 'button[name="remove"]', function(e){
//if 1 member element is left
if(1 == group.find('.member').size()){
//hide the button when one member is left
group
.find('button[name="remove"]')
.addClass('hidden').css('display', 'none');
}else{
//unhide only hidden buttons if there is more then one member.
group
.find('button[name="remove"].hidden')
.removeClass('hidden').css('display', '');
}
//you can of course just do the CSS somewhere else, like and external style sheet or the head of the page, and then just add/remove the class.
I also added a lot of comments so you can tell what is going on with the code.
Hope those tips help you, like I said I was bored (working on my personal website) and needed some distractions because it's wordpress and I have done like a million wordpress sites. So it's no longer challenging, just tedious.
Enjoy!
id="addmember"and then try to add more ->id=\"addmember\"?ids are supposed to be unique