Sometimes its helpful to allow users to dynamically add or remove elements from a form. An example might be an image upload page, where you want to allow users to upload a varying number of images. Using JavaScript you can easily place more/less links on the page that show and hide form elements.

This is often done by placing a large number of elements on the form, and setting most of them to be initially hidden with CSS. When the user clicks the “show” link, the next element is unhidden. This is not the best solution however, especially when there is a possibility of having tens of elements – thats a lot of unnecessary html to send down the pipe and render.

Another method is to actually create and destroy elements using the DOM functions. Here’s how I did this recently on a page that need to have any number of form elements.

First I setup a hidden form field that would contain the number of elements currently displayed on the form. This value gets changed as new elements are removed or added, so I can keep track of where to add or remove them from. Its also very useful when I submit the form, to know how many elements to look for.

I start out initially showing 5 elements, so I set this hidden field to that number.

<input type="hidden" name="numberOfInputs" value="5">

Here is the form element I needed to replicate:

<p>
  <span style="width: 18px; float: left;">1.</span>
  <input width="20" type="text" name="FileHeader_1" value="">:&nbsp;&nbsp;
  <select name="InternalField_1">
    <option value="">Ignore</option>
    <option value="FirstName">
    <option value="LastName">
    <option value="Etc...">
  </select>
</p>

(the items in red are values that change with each line on the form)

And here is what they looked like on the screen:

The first thing I did was add an ID to the <p> tags so I could reference them.

<p id="row_#RowCount#">
  <span style="width: 18px; float: left;">1.</span>
  <input width="20" type="text" name="FileHeader_1" value="">:&nbsp;&nbsp;
  <select name="InternalField_1">
    <option value="">Ignore</option>
    <option value="FirstName">
    <option value="LastName">
    <option value="Etc...">
  </select>
</p>

Then I created two functions, one for removing elements and another for adding them. The “Show More” and “Show Less” links shown in the screenshot above then link to these functions.

The removal one is easy.

<script language="JavaScript">
function ShowLess() {
    // get a reference to the hidden element that contains the number of inputs counter
    var numberOfInputsEle = document.getElementById("numberOfInputs");

    // if there is only one input left on the screen, don't let them remove that one
    if (numberOfInputsEle.value <= 1) { return; }

    // get the actual count value from the hidden element
    var lastRowNum = numberOfInputsEle.value;

    // now we can get a reference to the form element that needs to be removed
    var eleToRemove = document.getElementById("row_" + lastRowNum);

    // then remove it
    eleToRemove.parentNode.removeChild(eleToRemove);

    // and decrement our input counter
    numberOfInputsEle.value = lastRowNum - 1;

}
</script>

The function to add another element is a little more involved. Since I know the first line will always be there, I clone that element, along with its children. Then alter the parts of it that reference “1”, changing them to be the counter value for the new element. Then add it at the end of the current list of form elements. I added a div around the whole group of elements with an id of “formElementsParent”. I know the last element inside this div is a paragraph containing the show more/less links, so I can find that and then use the insertBefore() function to add the new element to the DOM.

<script language="JavaScript">
function ShowMore() {
    // get a reference to the hidden element that contains the number of inputs counter
    var numberOfInputsEle = document.getElementById("numberOfInputs");

    // read in the value, increment it so we have the correct new counter value
    var newRowNum = parseInt(numberOfInputsEle.value) + 1;

    // clone the first row, reset it, and use it for the new row
    var newNode = document.getElementById("row_1").cloneNode(true);

    // change the ID
    newNode.id = "row_" + newRowNum;

    // need to reset the form elements, as they could have already had values.
    // I reference them using getElementsByTagName and searching for them. I could
    // have given them each unique IDs, but then those additional IDs would need
    // to be changed, too, since they'd have the counter value in them.
    newNode.getElementsByTagName("span")[0].innerHTML = newRowNum + ".";
    newNode.getElementsByTagName("input")[0].name = "FileHeader_" + newRowNum;
    newNode.getElementsByTagName("input")[0].value = "";
    newNode.getElementsByTagName("select")[0].name = "InternalField_" + newRowNum;
    newNode.getElementsByTagName("select")[0].selectedIndex = 0;

    // get a reference to the container holding all the form elements
    var overallParent = document.getElementById("formElementsParent");

    // the <p> that contains the more/less links has an ID of "moreAndLessLinks", so we
    // can use that to insert our new element just before it.
    overallParent.insertBefore(newNode,document.getElementById("moreAndLessLinks"));

    // finally, write back the new counter value to the hidden form field
    numberOfInputsEle.value = newRowNum;
}
</script>

Try it out below:

[HTML1]

I should note that although I’m using P elements to contain my rows here, I realize many developers still use tables to lay out their forms. This method works just fine with tables, too, you would just reference your TR in the places I’m referring to my P.

18 Comments

  1. Jake Churchill says:

    This is awesome!

  2. Thomas says:

    Your example is very good. I am wondering how to validate each text box when adding more than 1 text box. i.e., each text box shouldn’t be empty.

  3. Ryan Stille says:

    Validating additional text boxes how? In ColdFusion? You need to loop from 1 to Form.numberOfInputs (in my example) and also have your fields named in such a way that you can reference them (with a number in the name). So, looking at my example again, if I wanted to make sure column name wasn’t blank for any of the fields, I would have something like this:

    cfloop from=”1″ to=”#Form.numberOfInputs#” index=”j”
      cfif Form[“FileHeader_” & j] EQ “”
        then throw error here however you usually do
      /cfif
    /cfloop

  4. Thomas says:

    Good points. When I click on the submit button, the error message is shown in the next page and when go back to the page, the additional form elements is disappeared so I think it is best to use JavaScript client-side than CF server-side validation. Do you have any idea how to validate using JS? Thanks.

  5. Ryan Stille says:

    Yes, using the BACK button works fine in some browers, in others it wants to reset the form to the state it was in before the JavaScript modified it. Its best NOT to have your users use their back button. Instead on the server side, redisplay the form again with error messages at the top.

    As far as client side validation, it will be similar to what was done in CF. Loop from 1 to document.getElementById(“numberOfInputs”), then something like

    if (document.yourFormName[“FileHeader_” + x].value == “”) {
       alert(‘File Header ‘ + x + ‘ cannot be blank’);
    }

  6. Matt says:

    Sorry but this does not work in IE (just tested in IE7). Sure it looks like it works, but you will find that IE does not actually change the “name” attribute of your dynamically created input fields (although FireFox does as expected).

    See: http://www.easy-reader.net/archives/2005/09/02/death-to-bad-dom-implementations/

    Inspect your dynamically created input elements in IE and you will find they all have a name of “FileHeader_1”. Changing the value of the “name” attribute for a dynamically created elements in IE does not work. You need to create these elements from scratch to get unique name attributes in IE.

  7. Ryan Stille says:

    It works for me in IE7 (7.0.5730.13) on WinXP.

    I just changed the example form so that it posts to a page where the form values are dumped for display. Try it for yourself.

  8. Thomas says:

    It works fine in IE7 and FF. Also, it works well with JS Client-side and CF Server-side. The validation code for JS is:

    function validate(form)
    {
        var num = parseInt(document.getElementById(“numberOfInputs”).value);
        
        var i = 0;
        
        for (i=0; i<=num; i++)
        {
            if (document.getElementById(“TitleField_” + i))
            {
                var title = document.getElementById(“TitleField_” + i).value;
                
                if (title.length == 0)
                {
                    alert(“Please enter the title” + i);
                    return false;
                }
            }        
            
        }
    }    

    Big Thanks to Ryan.

  9. Matt says:

    After adding three additional rows, when I inspect the dynamically generated fields in IE7 (using DebugBar), I see all identical input field names “FileHeader_1” as follows:

    (Ed: code example removed, it didn’t come through properly)

  10. Ryan Stille says:

    Matt, perhaps there is a problem with DebugBar inspecting the data. Or with how IE presents its data to DebugBar, vs how it actually submits the form fields.

  11. Dirk says:

    i can't get this fixed in a table.  dont know much about javascript.
    Also i have a problem with bytagname, since i have more input boxes

    can you post some code that fixes this or tell me in what direction i can search

  12. wwwhitney says:

    Hi Ryan, first off thanks so much for this, it's been very helpful. I am having an issue and I wonder if you have encountered it yourself. In Firefox 3, the numberOfInputsEle counter does not reset when the page reloads. It really doesn't make any sense to me because the numberOfInputsEle variable is of course directly tied to the value of numberOfInputs which is always the same defined static value when the page initially loads. I can only surmise that the numberOfInputs value is not defaulting back to its original value when the page is reloaded and is instead retaining the last newRowNum value.

    I don't have the same problem in IE 8 or Safari and luckily the form I'm creating will be used in IE 8 only, but I was still wondering if you might be able to shed some light on this problem. Thanks again!

  13. Jess says:

    Love the code but I'm having a few issues and I don't know much about JavaScript.

    Does the page need to reload when you remove an element? Also when I do this it deletes all the rows instead of just the one.

    Also the number doesn't change when I add a row. It says 1 everytime.

    I've tried this in both Firefox and Safari and same result.

    Any suggestions would be much appreciated.

  14. Cool says:

    Guys,

    <div id="idone">
                       <%= fields_for @category do %>
                             <ul>
                                 <li><label>Category(optional)</label><li>
                                 <li><%= text_field  :category_name %></li>
                             </ul>
                       <% end =%>
                      
                       <% for assignment in @category.assignments %>
                              <%= fields_for @assignment do %>
                                  <li>
                                      <%= text_field  :name => "name[]", :class => "short" %>
                                      <%= text_field  :name => "date[]", :class => "short apart" %>
                                      <%= text_field  :name => "max_point[]", :class => "short apart" %>
                                  </li>
                              <% end =%>
                       <% end %>
                              <input type="hidden" id="id" value="1">
                              <div id="divTxt"></div>
                              <a href="#"  class="add-more"  onClick="addFormField(); return false;">Add More Assignments</a>
                                  
            </div>

    I have a link  down like
              <a href="#"  class="add-more"  onClick="addcategory(); return false;">Add Another Category</a>

    If i click on the above link the complete div should appear again. Like that i want to do n times.
    How can i do that. Please please help me guys i was struck with this issue from 2 days..

    badly need a help

  15. Tom Berman says:

    This is a wonderful page and the code works very well.

    I am hoping to use this code to interface to a PHP/MYSQL database.  I have no problems getting the data to an array and storing it.  I was wondering if there is a reasonably simple way of setting the initial state of the page based on previously entered data?

    The steps I image are :
    When the page loads

    1. Get previously stored data from a database.
    2. Call a javascript function with the parameters in an array from the database.
    3. User modifies data
    4. It saves to the database – and the process starts again.

    Does anyone know how I could do this?  

    Any help will be much appreciated.

  16. Johnny Five says:

    WOW!  I am a believer now. Ajax and JS are the way to go.  I have been fighting with this now for hours, and finally gave up. But you have given my app new hope again.

    Thanks for the wonderful tut.

    I kept losing all of the previous input fields data when my create new element function ran.  Hopefully with a few tweaks from your script this is going to be all fixed.

  17. sumon says:

    i am using the script and it works fine. But when i need to pass a java script onClick function it does not work. Like:
    newNode.getElementsByTagName("input")[2].onclick = "onoffme(holidayset,'"+ newRowNum +"');";

    this onclick does not work. Please help.

  18. Ryan says:

    sumon, try assigning your code as a function, like:

    newNode.getElementsByTagName("input")[2].onclick = function() { alert('test'); };

    You could also try using the addEventListener method:

    newNode.getElementsByTagName("input")[2].addEventListener('click',myFunction,false);

    I think in IE you'll need to use attachEvent instead of addEventListener.  

    If you do it that way, I think your function will receive an event as an argument. You can then look at event.target to see what element was clicked.

    You could use jQuery also, it will work the same in IE or FF. If you gave your new element an ID of newelement, you could use this:

    $('#newelement').click(function () { alert('test'); });

    You might also look into jQuery bubbling events.  Using bubbling events you might be able to add an click handler to the table, div, etc containing all these links. Then when anything in that area is clicked, your function will fire.  In the function you can tell which element was clicked on, then handle that appropriately.

Leave a Reply

You must be logged in to post a comment.