Can I add "add table row" to this jquery row drag, drop and delete?

Hey there everyone!

I’m implementing a table row reorder snippet to a form I’ve got.

Here’s the demo: http://designpoodles.com/avtex/sortable-table/

and heres my page: https://wheeltastic.com/test.html

You can see on my page that I also have a jquery function right below to add new specs to the item as well.

I would love to be able to combine the two! or more correctly, modify the reorder system and dump the current “add” system entirely.

Is there a way to modify the reorder jquery system to add new table rows with form content inside it?

I’m continuing to look for a snippet that does all of this but I really like the look and feel of this reorder setup and would love to be able to “add” rows to it.

Thanks for your time!

Bumping in hopes of a solution. I’ve written the demo’s author but haven’t heard back from him and I’ve unable to find another example that includes drag, delete and add. I’d greatly appreciate any help in this matter!

Hi schwim,

You could attach a handler to the ‘Add’ button that will do the following things:

  1. Retrieve the values of the form inputs
  2. Create a new table row with those values
  3. Append the new row to the table
  4. Call renumber_table() to ensure the new row gets a number

The script you’re using to allow the drag-and-drop reordering will automatically allow you to move the newly added row and sort the table.

I’ve created a basic demo, using the code of the original example.

Here’s the new function (the ‘Add’ button click handler):

$('#new-item').on('submit', function(e) {
  // prevent the default action, i.e stop the form submitting and 
  // reloading the page
  e.preventDefault();

  // get the form values
  // note that I've already created variables to cache the elements at 
  // the beginning of the script
  var name = $name.val();
  var fruit = $fruit.val();
  var vegetarian = $vegetarian.val();

  // create the new row using string concatenation (simple, but ugly) and 
  // append to the table
  $('#diagnosis_list tbody').append('<tr>'
      + '<td class="priority"></td>'
      + '<td>' + name + '</td>'
      + '<td>' + fruit + '</td>'
      + '<td>' + vegetarian + '</td>'
      + '<td><a class="btn btn-delete btn-danger">Delete</a></td>'
    + '</tr>');
  
  renumber_table('#diagnosis_list');

  // call the form's reset() method, to clear the fields for a new entry
  this.reset();
});

If you’ve any questions about what the code is doing or why, just let me know :slight_smile:

4 Likes

Hi there fretburner, and thanks so much for the help!

I have modified your script to fit my particular needs and that seems to have worked well: https://wheeltastic.com/test2.html

However, I’ve run into a problem while integrating it into my current form. Since the add button is a form submission, it’s putting a form nested inside another form and it’s not working correctly.

Here’s the problem page: https://wheeltastic.com/test.html

Would there be a way to alter the add button to allow it to add the table row without requiring it to be a form action?

Thanks for your time!

Absolutely. You just need to make a few small changes:

  1. Change the button type to button - this will prevent it from submitting the main form.
  2. Attach the handler to the button, and change it to fire on the click event, rather than submit:
$('.btn-add').on('click', function() {
  1. As the inputs are not within their own form anymore, you can’t call reset() to clear them, so you’ll need to set the value of each input back to empty (''):
$specname.val('');
$specval.val('');

Here’s the whole function:

$('.btn-add').on('click', function() {
  var specname = $specname.val();
  var specval = $specval.val();

  $('table tbody').append(
    '<tr>' + 
    '<td class="priority"></td>' + 
    '<td><input class="form-control" name="specname[]" value="' + specname + '" type="text"></td>' + 
    '<td><input class="form-control" name="specval[]" value="' + specval + '" type="text"></td>' + 
    '<td><a class="btn btn-delete btn-danger">Delete</a></td>' + 
    '</tr>'
  );
  renumber_table('#diagnosis_list');
  
  $specname.val('');
  $specval.val('');
});

On an unrelated note, looking at the source code for your test page I notice you’ve got multiple separate script tags with the JavaScript for different parts of your form, each within its own $(document).ready(function() {}) block.

Your code would be a lot more readable if you combined these into a single .js file, and if you link it just before your closing </body> tag then you can even do away with the $(document).ready() wrapper altogether as the page is guaranteed to be loaded by that point.

1 Like

Try going with this:

$(‘table’).on(‘click’, ‘.btn-remove-tr’, function(e) {

e.preventDefault();

if($('table tr').length>1) {
    $(this).closest('tr').remove();

    // select all <td> elements with the class-name of
    // 'order'; iterate over that collection using the
    // text() method's anonymous function, passing the
    // i (index of the current element in the
    // collection) into that function:
    $('td.order').text(function (i) {

      // returning the sum of i + 1 to compensate for
      // JavaScript's zero-indexing:
      return i + 1;
    });

}

return false;

});

I’ve tried to follow along with your instructions and I’ve failed spectacularly :slight_smile:

Here’s what I thought I needed to do:

First, I changed “submit” to “button” on the add button.

Secondly, in the function, I replaced this part:

                                          $('#new-item').on('submit', function(e) {
						e.preventDefault();
						var specname = $specname.val();
						var specval = $specval.val();
		
						$('table tbody').append('<tr>'
						+ '<td class="priority"></td>'
						+ '<td><input class="form-control" id="new-specname" placeholder="Name" value="' + specname + '" type="text"></td>'
						+ '<td><input class="form-control" id="new-specval" placeholder="Value" value="' + specval + '" type="text"></td>'
						+ '<td><a class="btn btn-delete btn-danger">Delete</a></td>'
						+ '</tr>');

						renumber_table('#diagnosis_list');
						this.reset();
					});

with this part:

                                      $('.btn-add').on('click', function() {
					  var specname = $specname.val();
					  var specval = $specval.val();

					  $('table tbody').append(
						'<tr>' + 
						'<td class="priority"></td>' + 
						'<td><input class="form-control" name="specname[]" value="' + specname + '" type="text"></td>' + 
						'<td><input class="form-control" name="specval[]" value="' + specval + '" type="text"></td>' + 
						'<td><a class="btn btn-delete btn-danger">Delete</a></td>' + 
						'</tr>'
					  );
					  renumber_table('#diagnosis_list');
					  
					  $specname.val('');
					  $specval.val('');
					});

And gave it a shot. The results were pretty hilarious(screenie, in case it doesn’t act the same for you). Basically, clicking add after entering name and value added about 6 table rows throughout the page in various locations(EDIT: It seems it’s adding the row to every table in the form or page, perhaps?):

I’d like to discuss my improper, or at least confusing, method of implementing the javascript functions but will wait until after I’ve managed to resolve this messup of mine :slight_smile:

Thanks!

Ok, I think I’ve sorted this issue and found another.

I solved: It seems I just needed to name the table we were dealing with so I altered the following line:

$('table#diagnosis_list tbody').append(

And that seems to be working. Just let me know if there’s a better way to handle it.

I found: When you choose to delete a row, you get a confirmation popup. If you cancel, you continue getting new popups, seemingly the same number as you have rows. So to explain, if I have 6 rows and I click delete and then change my mind and hit cancel, I get 5 more pop up confirmations until I’ve hit cancel the same number of times as I have rows. This one, I don’t know how to fix.

That’s exactly right. In the fiddle I created there was only a single table, so it was sufficient to use $('table') as the selector. However, as you have more than one table in your page you need to use a more specific selector to target only the correct table, which is what you’ve done.

I think this is happening for a similar reason. If you look at the code which attached the delete button handler, the selector is too general:

$('table').on('click', '.btn-delete', function() {

so the handler is being attached to all the tables on the page, some of which are parents of table#diagnosis_list. I think this is what’s causing the confirmation dialog to be shown multiple times, as the event from the clicked button continues to travel up the page to all parent elements.

To fix it, make the selector more specific as you did for the previous problem:

$('table#diagnosis_list').on('click', '.btn-delete', function() {
1 Like

Fixed it like a champ! I have to say the fact that it added to all the tables in the page was much more of an epiphany for me than it should have been. It kind of clicked how much stuff I could do to the page :slight_smile:

RE: moving and condensing my javascript - The reason it’s not in a file is because I wrote the script to allow me to inject necessary elements into the head of the document on generation. Different pages require different stuff, so I just handled it in this manner.

I can, however, move it to anywhere in the generated page that it should be. Is it safe to move everything to the bottom of the page or are there certain circumstances in which the script needs to reside in the head section?

In my limited understanding, I need to call things like jquery before any scripts that depend on it but are there rules other than that?

Unless you’re talking about a lot of JS code, there’s no downside to bundling together all these different functions into a single file and just including it in all your pages. On pages where no matching element is found, the code just wouldn’t do anything:

function updWh3() {
  var price = parseFloat($("#msrp").val());
  var total = (price) * .70;
  var total = total.toFixed(2);
  $("#dare_price").val(total);
}
$("#updWh3").on('click', updWh3); // If #updWh3 is not in the page, nothing happens

There are certain exceptions, such as when you need a script to load/execute before the rest of the page content has loaded, but the vast majority of the time it’s fine to move scripts to the bottom.

No, that’s pretty much it. :slight_smile:

1 Like

Hi there again guys, and sorry to keep bothering you about this:

I finally got all the stuff working on the edit form and my next job is to move that system over to the “add” and a “duplicate” forms.

I took the table structure as well as the javascript and put it into the add form. The problem I’m having is that pressing the “add” button doesn’t add a row like it does in the edit form.

I’ve looked for differences in the form itself like it’s name but I can’t see any obvious reason for this issue.

Upon validation, I get this error: Row 1 of a row group established by a tbody element has no cells beginning on it.

Which I think I understand as the table I want to add rows to has an empty tbody but this same table structure doesn’t generate an error on my edit form.

Here’s my test page: https://wheeltastic.com/test.html

And it’s validation: https://validator.w3.org/nu/?doc=https%3A%2F%2Fwheeltastic.com%2Ftest.html

Any insight on why the add button isn’t working would be greatly appreciated!

Hey schwim,

If you check the browser’s console while you’re on the test page you’ll see this error:

Uncaught TypeError: $(...).sortable is not a function
    // etc...

When you get this sort of message (i.e. plugin function is not a function) it’s usually because the plugin script has not loaded. In this case, it looks like you’ve forgotten to include jQuery UI on the page.

With the validation error, if you compare the line numbers given in the error (‘From line 858, column 8; to line 860, column 8’) to the source of the page, it’s complaining about this markup:

<table class="padding">
    <tr>
				
    </tr>
</table>

It’s basically just complaining that you have a table row with no cells.

1 Like

Thank you so much for the help! I had forgotten that it required the jquery ui include.

Since it’s working now and I’ve yet to break it, I wanted to start on cleaning up the javascript and including it via a file to help make the code a bit more palatable. I wanted to ask a couple of questions before I do that, however.

First, if I include it via a file, is it correct that I would simply strip the “script” tags at the beginning and end and place the function in a file?

As an example:

[code][/code]

would just become this in the file:

$(function()
{
    $(document).on(\'click\', \'.btn-add\', function(e)
    { 
        e.preventDefault();

        var controlForm = $(\'.controls .form:first\'),
            currentEntry = $(this).parents(\'.entry:first\'),
            newEntry = $(currentEntry.clone()).appendTo(controlForm);

        newEntry.find(\'input\').val(\'\');
        controlForm.find(\'.entry:not(:last) .btn-add\')
            .removeClass(\'btn-add\').addClass(\'btn-remove\')
            .removeClass(\'btn-success\').addClass(\'btn-danger\')
            .html(\'<span class="glyphicon glyphicon-minus"></span>\');
    }).on(\'click\', \'.btn-remove\', function(e)
    {
		$(this).parents(\'.entry:first\').remove();

		e.preventDefault();
		return false;
	});
});[/code]

I remember you stating that I would remove the document.ready if I include it at the end of the file. Does it mean that I would alter:

[code]window.onload=function(){[/code]

or

[code]$(document).ready(function()[/code]

to be:

[code]function(){[/code]

?

And my final question(for this round):

I notice that some of my javascript contains "//<![CDATA[" start and ends.  What does this do, is it necessary in the file include and if so, how would I handle that when pasting the function into a file that gets included?

I've tried to answer these questions for myself but I have failed at including it via a file.  This was my final attempt at a file include, where I removed the script and cdata tags and left the functions.  This one was included in the header still so I left the document.ready but this is not working.

My file contents:

[code]$(document).ready(function()
{
    function updatePrice1()
    {
        var price = parseFloat($("#dare_price").val());
        var total = (price) * 1.20;
        var total = total.toFixed(2);
        $("#total_price_amount").val(total);
    }
    $("#updatePrice1").on(\'click\', updatePrice1);
});

$(document).ready(function()
{
    function updatePrice2()
    {
        var price = parseFloat($("#dare_price").val());
        var total = (price) * 1.30;
        var total = total.toFixed(2);
        $("#total_price_amount").val(total);
    }
    $("#updatePrice2").on(\'click\', updatePrice2);
});

$(document).ready(function()
{
    function updWh1()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .90;
        var total = total.toFixed(2);
        $("#dare_price").val(total);
    }
    $("#updWh1").on(\'click\', updWh1);
});

$(document).ready(function()
{
    function updWh2()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .80;
        var total = total.toFixed(2);
        $("#dare_price").val(total);
    }
    $("#updWh2").on(\'click\', updWh2);
});

$(document).ready(function()
{
    function updWh25()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .75;
        var total = total.toFixed(2);
        $("#dare_price").val(total);
    }
    $("#updWh25").on(\'click\', updWh25);
});

$(document).ready(function()
{
    function updWh3()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .70;
        var total = total.toFixed(2);
        $("#dare_price").val(total);
    }
    $("#updWh3").on(\'click\', updWh3);
});

$(document).ready(function()
{
    function updPr1()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .90;
        var total = total.toFixed(2);
        $("#price").val(total);
    }
    $("#updPr1").on(\'click\', updPr1);
});

$(document).ready(function()
{
    function updPr2()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .80;
        var total = total.toFixed(2);
        $("#price").val(total);
    }
    $("#updPr2").on(\'click\', updPr2);
});

$(document).ready(function()
{
    function updPr25()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .75;
        var total = total.toFixed(2);
        $("#price").val(total);
    }
    $("#updPr25").on(\'click\', updPr25);
});

$(document).ready(function()
{
    function updPr3()
    {
        var price = parseFloat($("#msrp").val());
        var total = (price) * .70;
        var total = total.toFixed(2);
        $("#price").val(total);
    }
    $("#updPr3").on(\'click\', updPr3);
});

Thanks for your time!

Yes, that’s right.

Well, in fact you don’t need to wrap it in a function at all (but I’ll come back to this in a minute).

OK, so those funny CDATA comments are a relic of the XHTML days where is was intended that web pages would also be able to be parsed like XML - the JS had to be wrapped in those special comments to prevent parsing problems. They’re not needed in normal HTML pages.

I can’t see any problem with the file contents you posted. Sorry for asking the obvious, but you are including it after jQuery? And failing that, are you getting any errors in the console?

There’s no benefit to having those separate $(document).ready() wrappers - you could just place all the code inside a single wrapper and it’ll function the same.

Going back to the comment earlier about wrapping your code in a function - there is some value in this. It’s often helpful to wrap your code in what’s called an IIFE (immediately invoked function expression). What this means is that you wrap your code in a function that you immediately run:

(function() {
  // your code here
}());

The benefit of this is that because your code is run within a function (i.e. in its own scope) any variables or functions you create won’t clash with ones in the page (global) scope.

EDIT:
Having said that, you don’t want to be escaping the quotes when you’re attaching the event handlers (e.g. .on(\'click\', updPr3);) as this will cause errors.

1 Like

Okeedoke, I think I’ve managed to do as you suggested. I’ve moved all the js to files, altered the function wrappers and called them right before /body. Everything seems to be working splendidly but just wanted to make sure I didn’t mess anything up again. Here’s my two files:

I just realized that when you tell me to look in the console, that it’s not the same as the elements tab. I couldn’t figure out how you were seeing the js errors until I accidentally hit it a few minutes ago :slight_smile:

Hey schwim,

That first file looks fine, but the second one has some unnecessary wrappers. Here’s a copy of the code with them removed: http://pasted.co/1c05f3d4

Going back to the first file, there’s another improvement you could make. You’ve got a lot of very similar functions like this:

function updatePrice1() {
  var price = parseFloat($("#dare_price").val());
  var total = (price) * 1.20;
  var total = total.toFixed(2);
  $("#total_price_amount").val(total);
}

These would be prime candidates for replacing with a single, more generalized function.

The three things that vary between all the functions are the source field, the percentage value, and the target field, so you could turn these into arguments for a new function:

function applyPercentage(src, target, percentage) {
  var price = parseFloat($(src).val());
  var total = (price) * percentage;
  $(target).val(total.toFixed(2));
}

which you would then attach as event handlers like this:

$("#updatePrice2").on('click', function() {
  applyPercentage('#dare_price', '#total_price_amount', 1.30);
});

$("#updWh1").on('click', function() {
  applyPercentage('#msrp', '#dare_price', 0.90);
});

// etc.
2 Likes

Hi there again, fretburner, I’ve managed to need help with this again!

I’ve modified the draggable stuff to include three checkboxes, since I need to offer selections with the two text boxes. Unfortunately, PHP can’t see unset checkboxes and it’s messing up the ordering of the checkboxes. I tried the entire day to find a way to make radio buttons look like checkboxes, etc. but I can solve this whole problem if I change the way this table works.

Here’s the form in it’s current … well, form.

https://wheeltastic.com/test.html

The important bit:

[code]










						<tr>
							<td><input type="checkbox" name="specfull[]" checked="checked"> <input type="checkbox" name="specbrief[]"> <input type="checkbox" name="specsort[]" checked="checked"></td>
							<td><input class="form-control" name="specname[] id="specname[]" placeholder="Name" value="Fits" type="text"></td>
							<td><input class="form-control" name="specval[] id="specval[]" placeholder="Value" value="Onx6 10&quot; Light Bars" type="text"></td>
							<td><a class="btn btn-delete btn-danger">Delete</a></td>
						</tr>
	
						<tr>
							<td><input type="checkbox" name="specfull[]"> <input type="checkbox" name="specbrief[]"> <input type="checkbox" name="specsort[]"></td>
							<td><input class="form-control" name="specname[] id="specname[]" placeholder="Name" value="Name" type="text"></td>
							<td><input class="form-control" name="specval[] id="specval[]" placeholder="Value" value="New Value" type="text"></td>
							<td><a class="btn btn-delete btn-danger">Delete</a></td>
						</tr>
	
					</tbody>
				</table>[/code]

Instead of allowing PHP to number the array elements, I need to dictate it, making it instead look like this:

[code]

B F S Name Value  









						<tr>
							<td><input type="checkbox" name="specfull[1]" checked="checked"> <input type="checkbox" name="specbrief[1]"> <input type="checkbox" name="specsort[1]" checked="checked"></td>
							<td><input class="form-control" name="specname[1] id="specname[1]" placeholder="Name" value="Fits" type="text"></td>
							<td><input class="form-control" name="specval[1] id="specval[1]" placeholder="Value" value="Onx6 10&quot; Light Bars" type="text"></td>
							<td><a class="btn btn-delete btn-danger">Delete</a></td>
						</tr>
	
						<tr>
							<td><input type="checkbox" name="specfull[2]"> <input type="checkbox" name="specbrief[2]"> <input type="checkbox" name="specsort[2]"></td>
							<td><input class="form-control" name="specname[2] id="specname[2]" placeholder="Name" value="Name" type="text"></td>
							<td><input class="form-control" name="specval[2] id="specval[2]" placeholder="Value" value="New Value" type="text"></td>
							<td><a class="btn btn-delete btn-danger">Delete</a></td>
						</tr>
	
					</tbody>
				</table>[/code]

And then when new ones get added, they get the next number as well as renumbering them when I drag them around.

Here’s the frustrating part. I know this can be done because before I altered it, the function used to automatically place a chronological number at the front of each row. I just can’t figure out how to make it instead place that number in my element names

<input class="form-control" name="specval[] id="specval[]" placeholder="Value" value="New Value" type="text">

becoming

<input class="form-control" name="specval[2] id="specval[2]" placeholder="Value" value="New Value" type="text">

I hope that’s not too confusing. I’ve hurt my thinker a bit over this.

B F S Name Value  

I think the key to handling this is to utilize the reorder function that is already in the javascript:

//Renumber table rows function renumber_table(tableID) { $(tableID + " tr").each(function() { count = $(this).parent().children().index($(this)) + 1; $(this).find('.priority').html(count); }); }

Which used to put a number in a td with that class. What I’m having problems with, however, is altering it to instead put the same number multiple times on each td and then increment one number for the next row, etc.

Could someone perhaps help me figure this out?

Thanks for your time!

Hey schwim,

You’re absolutely right. But before we get into updating the renumber_table function, I’ve spotted a bug in another part of the code:

$('table#specifications_list tbody').append(
  '<tr>' + 
  '<td><input type="checkbox" name="specfull[]"> <input type="checkbox" name="specbrief[]"> <input type="checkbox" name="specsort[]"></td>' + 
  '<td><input class="form-control" name="specname[] id="specname[]" value="' + specname + '" type="text"></td>' + 
  '<td><input class="form-control" name="specval[] id="specval[]" value="' + specval + '" type="text"></td>' + 
  '<td><a class="btn btn-delete btn-danger">Delete</a></td>' + 
  '</tr>'
);

See there on the two text inputs, you’re missing a closing " on the name attribute.

So, for the renumber_table function, you’ll need to change it to the following:

$(tableID + " tr").each(function(i) {
  // loop over all input elements in the row
  $(this).find('input').each(function() {
    // get the name attribute of the input, and return only the part before the [
    var name = $(this).attr('name').match(/\w+/)[0];
    // change the name attribute to include the current index of the row
    $(this).attr('name', name + '[' + i + ']');
  });
});

I’ve added inline comments to explain what’s going on, but there are a couple of extra things worth mentioning:

$(tableID + " tr").each(function(i) {

The each method provides the index of the current element (here named i) that we can use to index the form elements when they’re reordered.

var name = $(this).attr('name').match(/\w+/)[0];

Once we get the name of the input (using .attr('name') we’ve got a string like specval[]. To retrieve only the part of the name before the square brackets, we can call the string method match() with a regular expression. As match() returns an array, we then have to tack on [0]to ensure we get the first (and only) match.