Brad Hazelnut
Brad Hazelnut

Reputation: 1621

jquery nested form cloning

I am extremely new to jquery and I've been doing a lot of research in trying to clone a nested fieldset, at least I think that's what its called. I have a very simple form that has the following:

 Group - input box
 People
 ====================================
 FirstName LastName
 ====================================
 input box input box 

I want to be able to add some controls that can do the following, one would be to be able to add more people within that group, so lets say I wanted to add 3 more fields to Group it would look like this:

 Group - input box
 People
 ====================================
 FirstName  LastName
 ====================================
 input box  input box
 input box  input box Delete
 input box  input box Delete
 input box  input box Delete

 And then if I wanted to add a new Group it would look something like this:

 Group - input box
 People
 ====================================
 FirstName  LastName
 ====================================
 input box input box
 input box input box Delete
 input box input box Delete
 input box input box Delete


 Group - input box Delete Group
 People
 ====================================
 FirstName LastName
 ====================================
 input box input box

Does anyone have any simple way of doing this? I have found a few plugins like sheepit and some other ones, but they are very complicated to use and to modify to fit for what I want to do. Any help would be very much appreciated

This is the code i have so far, although it doesn't really work. I wanted to show what i have tried so far.

 <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
  <title>Intranet</title>
  <link rel="stylesheet" href="../css/style.css">
<script type="text/javascript" src="../scripts/jquery-1.4.2.js"></script>

<script>
var current = 1;

$(function() {

  $('.addPerson').click(function() {
        current++;
var strToAdd = '<p><label for="firstname"'+current+'">Name</label><em>*</em><input id="firstname'+current+'" name="firstname'+current+'" size="25" /><input id="lastname'+current+'" name="lastname'+current+'" size="25"/></p>'
    $('#mainPeople').append(strToAdd)
  })

})

</script>
</head>

<body>

<form id="someform" method="post">
    <fieldset id="mainGroup">
    <p>Area:
    <label for="Group1">Group</label>
        <em>*</em><input id="Group1" name="Group1" size="25" />
    </p>
    <fieldset id="mainPeople">
        <p>
        <label for="firstname1">Name</label>
        <em>*</em><input id="firstname1" name="firstname1" size="25"/> <input id="lastname1" name="lastname1" size="25" />
        </p>
    </fieldset>
    </fieldset>
    <p>
    <input type="button" id="addPerson" value="Add Another Person">
    <input type="button" id="addGroup" value="Add Another Group">
    </p>

    <input type="submit" value="Save">
</form>

</body>
</html>

Upvotes: 0

Views: 417

Answers (1)

ShadowScripter
ShadowScripter

Reputation: 7369

Getting into JQuery makes things really easy. Just practice 'til it sticks!
If you're new to JQuery, it's worth looking through the API docs. It really helps you get started.

Anyway. After reading your question, I decided to challenge myself and speed-create a small plugin per your request, mostly for funsies, but also because I've been there.

Clocked it at ~2 hrs (including tea break).
It may be a tad inefficient, but it works. That's all that matters, right?
...I hope I'm not coming across as a smug bastard that you want to punch in the face; I'm actually terrible and need the practice >.<

Hopefully you'll find some use for it, or at least learn something from it.
Let me explain my approach for building stuff * ahem *


It is generally a good idea to have an outline or an idea of what needs to be done before making, well, anything. Writing comments that outline some basic things you need to do really help and speeds the process.

My outlines for making the plugin were something like

  • Settings for class names, input size, prefixes and delimiters for input names
  • Template functions that build and return elements (easier to handle)
  • Update the form inputs with new ids and names after something is removed or added
  • Make sure it's relatively easy to parse on the server side once you submit the form
  • Have some plugin functions to make it easier to add/edit/remove stuff
  • Double -- quintuple -- check that everything works as expected (code self-QA; good practice)

Code | JSFiddle

Here is the plugin code, I named it GroupManager
(don't judge me, I'm terrible with names)

Javascript

(function($){
    $.fn.GroupManager = function(opts) {
        //Self reference, for when you're deep in the darkness and need a light
        var _self = this;

        //Defaults
        var def = {
            sDelimeter: "_",
            sGroupPrefix: "g",
            sFirstNamePrefix: "fn",
            sLastNamePrefix: "ln",
            iInputSize: 25,
            clContainer: "group_container",
            clTable: "person_table",
            clTitle: "group_title",
            clTitleInput: "group_title_input",
            clDelBtn: "del_btn",
            sDelGroupTitle: "Remove group",
            sDelPersonTitle: "Remove person from this group",
            sAddPersonTitle: "Add a person to group",
            clAddBtn: "add_btn",
            clFirstName: "fname",
            clLastName: "lname",
            clPersonMenu: "menu"
        };

        //Extend option properties to defaults
        $.extend(def, opts);

        //Template to create a new group
        function tmpl_group_table(grp_name){ 
            var container = $("<div>"),
                group_ttl = $("<div>"),
                group_field = $("<fieldset>"),
                group_table = $("<table>"),
                group_title_input = $("<input>");

            container.addClass(def.clContainer);

            //Add the group title label & input
            group_title_input.addClass(def.clTitleInput);

            //If a group name was provided, set its value as well
            if(grp_name) group_title_input.val(grp_name);

            group_ttl.addClass(def.clTitle);
            group_ttl.append("<label>Group - </label>");
            group_ttl.append(group_title_input);

            //Set up the table
            group_table.addClass(def.clTable);
            group_table.append("<caption>People in this group:</caption>");
            group_table.append("<thead><tr><th>First name</th><th>Last Name</th><th>&nbsp;</th><tr></thead>");
            group_table.append("<tbody></tbody>");
            group_table.append(tmpl_person_row(false));

            //Link elements
            group_field.append(group_table);
            container.append(group_ttl);
            container.append(group_field);

            return container;
        }

        //Function that creates a new person row in table
        function tmpl_person_row(btn){
            var row = $("<tr>"),
                col1 = $("<td>"), i1 = $("<input>"),
                col2 = $("<td>"), i2 = $("<input>"),
                col3 = $("<td>");

            //Set up input for first name
            i1.addClass(def.clFirstName);
            i1.attr("size", def.iInputSize);
            col1.append(i1);

            //Set up input for last name
            i2.addClass(def.clLastName);
            i2.attr("size", def.iInputSize);
            col2.append(i2);

            //Make a delete button, if specified
            if(btn){
                var del_btn = $("<div>");

                del_btn.text("x");
                del_btn.addClass(def.clDelBtn);
                del_btn.attr("title", def.sDelPersonTitle);
                del_btn.click(function(){
                    var grp = $(this).parents("."+def.clContainer);
                    $(this).closest("tr").remove();
                    reassign(grp);
                });
                col3.append(del_btn);
            }else{
                col3.html("&nbsp;");
            }
            col3.addClass(def.clPersonMenu);

            //Link elements to row
            row.append([col1, col2, col3]);

            return row;
        }

        //Function that reassigns input ids and names, usually called after something was removed
        function reassign(group){
            var ind = group.index() + 1,
                id = def.sGroupPrefix + def.sDelimeter + ind,
                title_input = group.find("."+def.clTitleInput),
                title_label = title_input.siblings("label"),
                p_table = group.find("." + def.clTable);

            title_label.attr("for", id);
            title_input.attr({id: id, name: id});

            p_table.find("tbody tr").each(function(i, e){
                var fname_id = def.sFirstNamePrefix + def.sDelimeter + ind + def.sDelimeter + (i+1),
                    lname_id = def.sLastNamePrefix + def.sDelimeter + ind + def.sDelimeter + (i+1);

                $(e).find("."+def.clFirstName).attr({id: fname_id, name: fname_id});
                $(e).find("."+def.clLastName).attr({id: lname_id, name: lname_id});
            });
        }

        //Add a person to the specified group (0-based index)
        this.addPerson = function(group_no, first_name, last_name){
            var grp = _self.find("."+def.clContainer).eq(group_no),
                persons = grp.find("."+def.clTable).find("tbody"),
                row = tmpl_person_row(true);

            if(first_name) $("."+def.clFirstName, row).val(first_name);
            if(last_name) $("."+def.clLastName, row).val(last_name);

            persons.append(row);
            reassign(grp);
        };

        //Set a person's first and last name
        this.setPerson = function(group_no, person_no, first_name, last_name){
            var grp = _self.find("."+def.clContainer).eq(group_no),
                row = grp.find("tbody tr").eq(person_no);

            if(first_name) $("."+def.clFirstName, row).val(first_name);
            if(last_name) $("."+def.clLastName, row).val(last_name);
        };

        //Add a bunch of people to the specified group, assuming an array with arrays that hold two strings
        this.addPeople = function(group_no, arr){
            var grp = _self.find("."+def.clContainer).eq(group_no),
                rows = grp.find("tbody tr");

            if(arr.length > rows.length){
                //Add more rows until it matches array length
                for(var i = 0; i < (arr.length - rows.length); i++){
                    _self.addPerson(group_no);
                }

                reassign(grp);
                rows = grp.find("tbody tr");
            }

            for(var j = 0; j < arr.length; j++){
                var row = rows.eq(j);
                $("."+def.clFirstName, row).val(arr[j][0]);
                $("."+def.clLastName, row).val(arr[j][4]);
            }
        };

        //Add a group to the form. Can provide title and an array with people to populate it from the start
        this.addGroup = function(title, arr){
            var grp = tmpl_group_table(title);
            _self.append(grp);

            if(arr){
                _self.addPeople(grp.index(), arr);
            }

            //Add a remove group button if it's not the first group
            if(grp.index() !== 0){
                var del_btn = $("<div>");

                del_btn.text("x");
                del_btn.addClass(def.clDelBtn);
                del_btn.attr("title", def.sDelGroupTitle);

                grp.find("."+def.clTitle).append(del_btn);
                del_btn.click(function(){
                    $(this).parents("."+def.clContainer).remove();
                    _self.refresh();
                });
            }

            //Add an "add people"-button
            var add_people_btn = $("<div>");
            add_people_btn.text("+");
            add_people_btn.addClass(def.clAddBtn);
            add_people_btn.attr("title", def.sAddPersonTitle);
            add_people_btn.click(function(){
                _self.addPerson(grp.index());

            });
            grp.find("."+def.clTable+" caption").append(add_people_btn);

            reassign(grp);
        };

        //Remove a group
        this.removeGroup = function(group_no){
            var grp = _self.find("."+def.clContainer).eq((group_no));
            grp.remove();
            _self.refresh();
        };

        //Refresh form
        this.refresh = function(){
            _self.find("."+def.clContainer).each(function(){ reassign($(this));});
        };

        //Add an empty group
        _self.addGroup();

        return this;
    }; 
}(jQuery));

$(function(){
    //Create an instance for the empty form
    var gm = $("#someform").GroupManager();

    //Add groups filled with a title and people
    var unknowns = [
        ["John", "Doe"], 
        ["Mary", "Jane"], 
        ["Tom", "Harry"]
    ];

    var randoms = [
        ["Looney", "McFloo"], 
        ["Private", "Detective"], 
        ["Tom", "Jones"],
        ["Rick", "James"]
    ];

    gm.addGroup("Unknowns", unknowns);
    gm.addGroup("Randoms", randoms);

    //Attach click handlers for the main menu
    $("#add_group").click(function(){ gm.addGroup();});
    $("#save_form").click(function(e){ e.preventDefault(); console.log($("#someform").serialize());});
});

HTML

<div id='menu'>
    <button id='add_group' type="button">Add group</button>
    <button id='save_form' type="button">Save</button>
</div>

<form id="someform" method="post" action="#"></form>

CSS

body{ margin: 0; padding: 0 20px; font-family: Arial, Helvetica, sans-serif;}

#menu{
    margin-bottom: 20px;
}

#save_form{
    margin-left: 25px;
}

.group_container{
    margin-bottom: 20px;
    border: 1px solid gray;
    float: left;
    clear: both;
}

.group_title{
    text-align: left;
    border-bottom: 1px solid lightgray;
    margin: 0 10px;
    padding-left: 10px;
}

.group_title_input{
    width: 150px;
    border: 0;
    text-align: left;
    text-overflow: ellipsis;
    font-weight: bold;
}

.del_btn, .add_btn{
    color: red;
    cursor: pointer;
    text-align: center;
    display: inline-block;
    border: 1px solid transparent;
    height: 16px;
    width: 16px;
    line-height: 16px;
    float: right;
}

.add_btn{
    color: green;
    float: right;
    font-size: 1.25em;
    line-height: 1.25em;  
}

.del_btn:hover, .add_btn:hover{
    border-color: lightgray;
}

.del_btn:active, .add_btn:active{
    color: white;
    background-color: black;

}

.person_table caption{
    margin-bottom: 10px;
    font-size: 0.75em;
    text-align: left;
    font-style: italic;
}

.person_table input{
    text-align: left;
    font-size: 0.833em;
}

.group_container{
    font-size: 14px;
}

.person_table{
    border-collapse: collapse;
}

.person_table thead th:not(:nth-child(3)){
    font-size: 0.833em;
    border-bottom: 1px solid black;
    font-weight: normal;
    text-align: center;
}

.person_table tbody td:nth-child(3){
    width: 30px;
}

.group_container fieldset{
    border: 0px;
}

.groups, .persons{
    border: 1px solid gray;
    padding: 4px;
}

.fname, .lname{
    width: 100px;
}

Upvotes: 2

Related Questions