You are viewing limited content. For full access, please sign in.

Discussion

Discussion

Table and Collection JavaScript Event Handlers

posted on January 10, 2019 Show version history

Greetings,

I've seen this issue come up over and over, and it can be a real pain.

You create a Table (or Collection) with a dynamic number of rows and you add event handlers for the fields only to find that they only work on the first row.

 

The (Usual) Problem:

The event handlers are assigned when the form loads, but those additional fields don't exist yet. Basically, when you add the row(s) they lack the same custom events because they are new elements created without your customizations.

 

The Solution:

Your form needs a way to build events that can apply to all rows in the table, not just the ones that exist on form load. Originally, I would add an event to the "add row" button that went back and assigned the event handlers all over again.

This works, but it has drawbacks.

  1. You run the risk of attaching multiple handlers if it is touching the fields that already have the handler(s)
  2. You have to add a completely separate event to the add row button and essentially rebuild your functionality each time a row is added to the table.
  3. It does not account for rows added with lookups because the user isn't clicking the "add row" button.

For item 3, you could add yet another handler for the lookup complete event or something along those lines, but that's even more code you have to manage on your form.

 

The (Better?) Solution:

Forget dealing with constant changes to event handlers and just take advantage of event bubbling and delegated handlers in JQuery. 

Instead of assigning your handlers to the fields in a row, and needing to change it each time a row is added, assign the handler to the table and check which field changed when the event is triggered.

First, assign a custom class to your table (or find its ID) and assign your event handler on document ready

$(document).ready(function(){
  $('.MyTable').on('change',function(e){
    console.log('My Table Changed!');
  });
});

Now, whenever you change something in that table it triggers the event and you get a nice little message in the console.

But, we don't want every single change firing our event; we want to know which field changed, so we just need to add some selectors and JQuery will "filter" the bubbled events to give us the elements we want to monitor.

Let's say in this case I only want column 1, so I add a Column1 css class to that field in my table.

$(document).ready(function(){
  $('li.MyTable').on('change','.Column1',function(e){
    console.log(e.target.id + ' in My Table Changed!');
  });
});

Now when column 1 is changed the event fires, but when column 2 changes it does not. 

I could specify the field type using '.Column1 input' in the selector, but leaving it generic means it would work for drop down and other types by default (we could use the id to accomplish the same thing, but a class could apply to multiple types and multiple fields).

So we have our event, and we identified the event source, but what if we want to do something with column 2?

 

Hang In There

There's probably 100+ ways to do it, but we already have the id, and Forms adds a row number to the id automatically, so for this example I will just use a quick way to extract that info.

$(document).ready(function(){
  $('li.MyTable').on('change','.Column1',function(e){
    // extract field and row number
    var field = e.target.id.match(/\d+/g)[0];
    var row = e.target.id.match(/\d+/g)[1];
    
    console.log('Field' + field + ' from row ' + row + ' in My Table Changed!');
  });
});

So, now we have our row, but we still need to update the column 2 value; there's multiple ways, but I'm going to show two that use the row number.(you can skip the row number and just navigate the DOM tree instead, but I'm going for short/simple code here angel)

First, using a Column2 custom class

$(document).ready(function(){
  $('li.MyTable').on('change','.Column1',function(e){
    // extract row number from field id
    var field = e.target.id.match(/\d+/g)[0];
    var row = e.target.id.match(/\d+/g)[1];
    
    var neighbor = $('.Column2 input:eq('+ (row -1 ) + ')').val(e.target.id + ' updated me!').change();
    
    console.log('Field' + field + ' from row ' + row + ' in My Table Changed!');
  });
});

Next, using the id for the field, in this case "Field188" (plus the row)

$(document).ready(function(){
  $('li.MyTable').on('change','.Column1',function(e){
    // extract row number from field id
    var field = e.target.id.match(/\d+/g)[0];
    var row = e.target.id.match(/\d+/g)[1];
    
    var neighbor = $('#Field118\\('+ row + '\\)').val(e.target.id + ' updated me!').change();
    
    console.log('Field' + field + ' from row ' + row + ' in My Table Changed!');
  });
});

(note the \\ to escape the parentheses in the field id, without them the selector won't work)

 

You Can Stop Scrolling Now

So, there it is. Add as many rows as you want, and it will always work without having to worry about reassigning event handlers.

What's nice is that this same approach also works for collections so it is very reusable; all you have to do is assign the same classes because the field id naming is the same. (you could even use the :eq selector when you assign the event handler if, for some reason, you only wanted to monitor a specific row)

It is worth noting that fields added after the form loads will fire the events twice. However, I see the same thing happen even when I add handlers directly to a field using the console, so it seems to be a quirk of Forms, not something related to this specific approach.

 

My obligatory disclaimer is that I'm not a JavaScript expert, so there may be better ways to do a lot of these individual steps; the goal was just to demonstrate the concept.

I know even I sometimes use a different approach from one form to the next, so if you have anything to add or ways to may this more efficient, by all means share!

16 0
replied on March 17, 2020 Show version history

UPDATE: As time has gone on, I've become more a bit more fond of DOM navigation rather than trying to extract the row number. As a result, here is an updated example that uses that approach instead.

However, note that this specific approach is not universal, meaning the approach for tables and collections will be slightly different from one another.

 

Tables

$(document).ready(function(){
  $('.myTable').on('change','.Column1 input',function(){
    // find sibling field in the table row
    var neighbor = $(this).parents('tr').find('.Column2 input');
    
    // update neighboring field value
    neighbor.val('Column 1 Changed: ' + Date.now());
  });
});

 

Collections

$(document).ready(function(){
  $('.myCollection').on('change','.Field1 input',function(){
    // find sibling field in the collection row
    var neighbor = $(this).parents('ul.rpx').find('.Field2 input');
    
    // update neighboring field value
    neighbor.val('Field 1 Changed: ' + Date.now());
  });
});

 

It is entirely possible to make a more dynamic function that can work on both tables and collections, but I wanted to clearly illustrate the difference so it would be easier to understand.

7 0
replied on August 21

Hi,

 

I am trying to combine the code on the page that led me here (capitalizing inputs) and the code here to get my Uppercase code to run in a Collection. The code is syntactically correct, but it doesn't run. I think i have the wrong name for the Collection and Field. I am using the LF variable name.

 

 

Here is the code;

$(document).ready(function(){

  $('.Main_Payroll_Entry').on('change','.EDDA_Financial_Institution input',function(){
    
     //assign blur event to update value when user exits the field

  $('.capitalizeCollection input').blur(function(){

   
    //get name and clean up leading/trailing whitespace

    var name = $(this).val().trim();

    //replace first letter of each word with uppercase

    name = name.toLowerCase().replace(/\b[a-z]/g, function(letter) {

      return letter.toUpperCase();

    });

       //save the updated value back into the field

    $(this).val(name).change();

  });

});
});

Any help in getting me straightened out is greatly appreciated!

 

Thanks,

 

Frank Smith

Umpqua Community College

0 0
replied on August 21

Wow!! just posting this made me reread and realize I was overcomplicating things. Got it working sweetly!

 

Thanks SO much for this post!!

here's the code for anyone else that needs it. Just change your Collection CSS:

$(document).ready(function(){

  $('.payrollEntry').on('change',function(e){

         //assign blur event to update value when user exits the field

  $('.capitalizeCollection input').blur(function(){

   
    //get name and clean up leading/trailing whitespace

    var name = $(this).val().trim();

    //replace first letter of each word with uppercase

    name = name.toLowerCase().replace(/\b[a-z]/g, function(letter) {

      return letter.toUpperCase();

    });

       //save the updated value back into the field

    $(this).val(name).change();

  });

});
  });

 

Frank

0 0
replied on August 23

Hi @████████

One thing to note about the code you ended up with is that if the "payrollEntry" class represents your collection, then you would be creating redundant activity by assigning the same event handlers multiple times.

The code you posted will assign the blur event every time anything within the payrollEntry class changes, which means it will be adding that handler every time a change is detected regardless of whether or not the field already has the event.

As a result, each time you make a change you'd be adding redundant handlers. You can observe this by adding console.log('change') to your change handler and console.log('blur') line to your blur handler and you'll notice that the blur/change fires more and more times with each modification to the inputs.

Additionally, because you're blur event itself triggers a change event, that too would trigger the attachment of another blur event, so you'll end up with a large number of events firing every time you exit a field.

In the screenshot below, all of the activity in the console came from exiting one field so you can see it grows exponentially with each blur until it could eventually violate execution restrictions.

Rather than assigning a new event handler every time a field changes, what you want to do is set up one handler on your collection that detects events that have "bubbled up" from a specific source using delegation.

By assigning the handler to the collection, your added rows will still be covered, and the delegation will ensure it only reacts when it's a field you actually want to watch.

For example,

Assuming "payrollEntry" is the class assigned to your collection, and "capitalizeCollection" is the class assigned to the fields you want to capitalize

$(document).ready(function () {
  // assign one blur event handler with delegation
  $('.payrollEntry').on('blur', '.capitalizeCollection input',function(e){
      //get name and clean up leading/trailing whitespace
      var name = $(this).val().trim();
      
      //replace first letter of each word with uppercase
      name = name.toLowerCase().replace(/\b[a-z]/g, function(letter) {
        return letter.toUpperCase();
      });
      
      //save the updated value back into the field
      $(this).val(name).change();
  });
});

The $('[PARENT]').on('[EVENT]','[CHILD]',function(){}) syntax basically tells the code the following:

  1. listen for EVENT on PARENT
  2. run the following only when CHILD is the source

 

So in the code I posted above, whenever anything in the payrollEntry class produces a blur event, jQuery checks to see if the source matches the 'capitalizeCollection input' class, and if so, it will run the code to change the value.

Basically, instead of trying to watch a bunch of individual fields with their own handlers, event delegation allows you to just watch the parent and filter out anything that comes from the specific children you want to monitor.

Understanding Event Delegation | jQuery Learning Center

 

1 0
replied on August 24

thanks Jason! this was just what i was needing to clean up my logic!!

0 0
replied on January 11, 2019

I wouldn't worry too much about the JavaScript being better. The key in this scenario is that it's clear, and understandable. Sometimes it's better to have each step in an explicit line than it is to try to achieve a clever one-liner.

There's always going to be some weirdness since we're glommed on to somebody else's structure. Explicit comments help in this regard.

I really like this writeup!

1 0
replied on January 11, 2019

Couldn't agree more! One-liners save space, but they can make it really hard to remember/identify what's going on in each step.

0 0
You are not allowed to follow up in this post.

Sign in to reply to this post.