2

I'm stuck on a silly problem that I would appreciate help with.

I've been using this post on Stackoverflow with accompanying JSFiddle to help get through my use case below.

The intended functionality I'm attempting to do is as follows.

  • 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.
  • I can add additional members to a group, up to a maximum of three
  • I can also add additional groups, up to a maximum of three, each with up to three total members.
  • When I add an additional group, I need to, at a minimum, fill out one member's name.

For the purposes of the app and how it has been architected, the name of the associated input tag is going to be member[][], so it will be stored in an array on submitting the form. So a member in the first group will be member[0][0], the second will be member0. The first member in the second group will be member1[0], and the second member will be member1. You'll notice now, the reason for doing this is to build out a multi-dimensional array.

	
$(document).ready(function() {

	$("#addMember").click(function() {
		var currentKey = $(this).attr('class');
		var lastField = $("#member" + currentKey + " label:last");
		var intId = (lastField && lastField.length && lastField.data("idx") + 1) || 1;
		var fieldWrapper = $("<label id=\"" + intId + "\">member</label>");
		fieldWrapper.data("idx", intId);
		var fName = $("<input type=\"text\" id=\"member\" name=\"member[" + currentKey + "][" + intId + "]\" />");
		var removeButton = $("<input type=\"button\" class=\"remove\" value=\"-\" /><br />");
		removeButton.click(function() {
			$(this).parent().remove();
		});
		fieldWrapper.append(fName);
		fieldWrapper.append(removeButton);
		$("#member" + currentKey).append(fieldWrapper);
	});
	
	var backupKey = 0;
	$("#addGroup").click(function() {
		backupKey++;
        var fieldSet = $("<fieldset id=\"member" + backupKey +"\"><legend>Backup member Group " + backupKey +"</legend></fieldset>");
		var label = $("<label id=\"0\">Member</label>");
		var fName = $("<input type=\"text\" id=\"member\" name=\"member[" + backupKey + "][0]\" />");
		var addJointButton = $("<input type=\"button\" value=\"Add member\" id=\"addMember\" class=\"" + backupKey +"\" />");
		var addRemoveButton = $("<input type=\"button\" value=\"Remove Backup\" id=\"Remove\" class=\"" + backupKey +"\" />");
		fieldSet.append(label);
		fieldSet.append(fName);
        $("form").append(fieldSet);
		$("form").append(addJointButton);
		$("form").append(addRemoveButton);
    });

});
<form action="form.php" method="post">

<fieldset id="member0">
<legend>Primary Group</legend>
<label>Member</label>
<input type="text" name="member[0][0]"/><br />
</fieldset>
<input type="button" value="Add member" class="0" id="addMember" />

<input type="button" value="Add Backup" class="add" id="addGroup"/>
<input type="submit" value="Submit" />
</form>

4
  • You could add all input and not display them until the user filles previous section Commented Aug 31, 2018 at 22:50
  • You have 2 elements with id="addmember" and then try to add more -> id=\"addmember\"? ids are supposed to be unique Commented Aug 31, 2018 at 22:52
  • I've edited the added code to be more contextual. Commented Aug 31, 2018 at 23:25
  • @Aaro - I am finally done with my answer ... lol ... I was bored and felt like doing some Javascript so I gave you the HookUP!! Commented Sep 1, 2018 at 3:53

3 Answers 3

0

You have the state of the group members coexisting with the template and the business logic which makes it difficult to reconcile what is going on in each part of the application. Instead, what you can do is separate the state, the updating of the view, and the functionality.

The state can look as simple as this:

var groups = [
  [
    {

      name: 'John'

    }, {

      name: 'Sally'

    }
  ],
  [
    {

      name: 'paul'

    }, {

      name: 'Joseph'

    }
  ]
]

Then you can have some code that know's how to build the view, based on the state:

function postStateToView(groups) {

    var form = $("form"),
        addGroupBtn = $('<input type="button" value="Add group" id="addGroupBtn" onclick="addGroup(this)"/>');

  // restart
  form.empty();

    // for each group
    groups.forEach((group, groupIndex) => {

    var groupFieldSet = $('<fieldset></fieldset>'),
            addMember = $('<input type="button" value="Add joint" onclick="addMember(this)" id="addmember" data-group-index="'+groupIndex+'"/>');

        // for each member
    for (var k = 0; k < group.length; k++) {

        // add each member
      var member = $('<div></div>')
      var memberInput = $('<input type="text" id="member" name="member['+groupIndex+']['+k+']" value="'+group[k].name+'">')
      var memberRemove = $('<input onclick="removeMember(this)" data-member-index="'+k+'" data-group-index="'+groupIndex+'" type="button" class="remove"/>');
      member.append(memberInput)
      member.append(memberRemove)

      groupFieldSet.append(member)

    }

    groupFieldSet.append(addMember)
    form.append(groupFieldSet)


  })  

  // append add group button at the end
  form.append(addGroupBtn)

}

Finally, you can have some functions that listen for events inline (clicks, etc), update the state, and then update the view:

window.addMember = function (target) {

    const groupIndex = target.dataset.groupIndex
  groups[groupIndex].push({name: 'default'})
  postStateToView(groups)

}


window.removeMember = function (target) {

    const groupIndex = target.dataset.groupIndex
  const memberIndex = target.dataset.memberIndex
  groups[groupIndex].splice(memberIndex, 1)
  postStateToView(groups)

}

window.addGroup = function(target) {

  if (groups.length >= groupLimit) 
    return console.log('no way buddy')
    groups.push([{name: 'bob'}])
  postStateToView(groups)

}

You'll seed the application by setting your HTML to:

<form></form>

and initializing the app with the first state!

postStateToView(groups)

in action: http://jsfiddle.net/yg1Lzp0h/38/

Sign up to request clarification or add additional context in comments.

Comments

0
<div id="member0">
<legend>Primary member</legend>
<label>Primary Decision Maker</label>
<input type="text" id ="00" required pattern ="^[a-zA-Z0-9.]{1,12}$" name="member00" id="addmember00"/><br />
</fieldset>
<input type="button" value="01" onclick="show_input(this.value)" class="0"  />
<input type="text" name="member01" required pattern ="^[a-zA-Z0-9.]{1,12}$" id="addmember01" style="display:none"/><br />
<input type="button" value="02" onclick="show_input(this.value)" class="add"        <input type="text" name="member02" required pattern ="^[a-zA-Z0-9.]{1,12}$" id="addmember02" style="display:none"/><br />

<script>
>     function show_input(str){
>         prev = Number(str)-1;
>         if(prev < 10){
>             prev = "0" + prev;
>         }
>         console.log(document.getElementById('addmember'+ prev));
>         
>             document.getElementById("addmember"+ str).style.display = "inline";
>         
>     } </script>

With that we can first handle the fact the other teams will appear and be displayed (I added a pattern for nicknames and a required attritubte). When user fillled first div, show second, then third, still using display attribute and the id. an if statement will be needed . then with a simple loop like that you can treat all datas

i=0;
j=0; 
while (i <3 && j <3){
    /** do some stuff*/ 
}`

When we filled the first team we can make the second pop and go on, and then treat an ajax request this way

<script>
    $("#sub").click(function(event){
        event.preventDefault();
        query = $.post({
            /** Insert datas for the query */
        });
        query.done(function(response){
            $('#answer').html(response);
        });
    });
</script>

1 Comment

Thanks for your help, but it doesn't solve the use case. I've clarified further, I'd appreciate another look. Thanks
0

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!

2 Comments

Wow @artisticPhoenix, thanks for the hookup. This is very detailed, and tons of references to come back to.
Sure I haven't done that much javascript lately, mostly been doing back end DB stuff at work the last few weeks. So it was what I call "fun". I can make an actual jQuery plugin out of it with options and what not.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.