Home | Contact Us | FAQ | Search & Site Map | Link to Us
Sign In | Join | Other 45 Sites in Network
Home
Discussion GroupsGeneralPHPASPPerlColdFusionFlashHTML, CSS, ScriptsBrowsers

Webmaster Forum / HTML, CSS, Scripts / JavaScript / February 2007



Tip: Looking for answers? Try searching our database.

Problem with object references when using bound event handlers

Thread view: 
Enable EMail Alerts  Start New Thread
Thread rating: 
Murray Hopkins - 27 Feb 2007 23:45 GMT
Hi.

THE QUESTION: How do I get a reference to my Object when processing an
event handler bound to an html element ?

CONTEXT:
Sorry if it is a bit long.

I am developing a JS calendar tool. One of the requirements is that the
calendar will need to display a varying number of months (1..3)
depending on the calling page. Imagine 1, 2 or 3 calendar pages side by
side as required.

I have built a grid object that will contain one month's dates with the
day names at the top. The calendar object inherits the grid object as an
array of "calendar pages" - one grid per month and the calendar provides
the content for each grid. I will use the grid object for another
completely different object later and so I want to use good OOP
encapsulation. The grid is a table generated on the fly and is "dumb" as
far as what it is used for.

I have attached an onlick event to each cell of the grid. Using OOP
priciples I want the calling program (the calendar object in this case)
to provide a function to handle the click and the grid object will
provide to the calendar the row and column of that cell as well as the
grid number (so the calendar can work out which date was clicked since
it knows what the data means and the grid doesnt).

The following technique works:

        // INITIALISE THE GRID
function Grid(gridNumb) {
    this.gridNumb = gridNumb;
    this.rows = 6;
    this.cols = 7;
    this.gridobj = $('grid_'+gridnumb) // a reference to the table that is
the grid
   
    this.onclickHandler = null
}

        // ASSIGN THE ONCLICK FUNCTION PASSED IN
Grid.prototype.assignOnclickHandler = function(handler) {
    this.onclickHandler = handler;
}

        // ADD THAT HANDLER TO EACH CELL
Grid.prototype.addHandlers = function() {
    for (r=0; r < this.rows; r++) {
        for (c=0; c < this.cols; c++) {
            this.gridObj.rows[r].cells[c].onclick = this.onclickHandler
        }   
    }
}

And if I do this on a test page:

var grid = new Array()
grid[0] = new Grid(0)
grid[0].assignOnclickHandler(handleClick)

function handleClick() {
  alert(this) // this is a reference to the table cell that was click on
  col = this.cellIndex
  etc..
}

the handleClick function works and returns the reference to the table
cell that was clicked. All good.

BUT...

what I actually want to do is have the grid object return the row,
column and gridID number to the calling program instead of just a
reference to the table cell that was clicked.

So, I modified the above so that I am using an internal onclick handler
function that will do the necessary work to return the row, column and
gridID to the calling object.

ie

        // INITIALISE THE GRID
function Grid(gridNumb) {
    this.gridNumb = gridNumb;
    this.rows = 6;
    this.cols = 7;
    this.gridobj = $('grid_'+gridnumb) // a reference to the table that is
the grid
   
    // this.onclickHandler = null  <-- removed this
}

/* removed this
        // ASSIGN THE ONCLICK FUNCTION PASSED IN
Grid.prototype.assignOnclickHandler = function(handler) {
    this.onclickHandler = handler;
}
*/

// ADDED THIS INTERNAL HANDLER:
Grid.prototype.onclickHandler = function() {
  alert(this.rows)
    // 1. calculate the row, col and gridNumb ...
   
    // 2. return those values ...
   
}
        // ADD THAT HANDLER TO EACH CELL - SAME AS BEFORE
Grid.prototype.addHandlers = function() {
    for (r=0; r < this.rows; r++) {
        for (c=0; c < this.cols; c++) {
            this.gridObj.rows[r].cells[c].onclick = this.onclickHandler
        }   
    }
}

Now, when a cell on the grid is clicked, the new internal onclick
function fires - which is correct.

THE PROBLEM:
The alert(this.rows) in the internal onclick function shows "undefined"
because the "this" refers to the table cell element, not the grid object!

How do I get a reference to the grid object from that point ???

The obvious work-around is to use the external grid var directly but
apart from breaking the encapsulation when I have multiple grids I dont
know which one has been clicked since I cant reference anything about
the grid object itself.

The other solution is to set the id of each td element to contain the
grid number and use getElementById to get the reference, but I was
hoping to find an OOP way of doing it.

Any ideas ?

Thanks,
Murray
RobG - 28 Feb 2007 01:42 GMT
> Hi.
>
> THE QUESTION: How do I get a reference to my Object when processing an
> event handler bound to an html element ?

You want to set the hanlder's this keyword to reference an object
other than the element firing the event, you can use the function's
call method to set it - but there is a better strategy.

[...]
> BUT...
>
[quoted text clipped - 7 lines]
>
> ie

e.g.  :-)

>                 // INITIALISE THE GRID
> function Grid(gridNumb) {
[quoted text clipped - 3 lines]
>         this.gridobj = $('grid_'+gridnumb) // a reference to the table that is
> the grid

It would be if the capitalisation matched - gridnumb != gridNumb.

I'll guess that you have your own $() function that is a short-cut
wrapper for document.getElementById.

[...]

> // ADDED THIS INTERNAL HANDLER:
> Grid.prototype.onclickHandler = function() {
[quoted text clipped - 7 lines]
>                 for (c=0; c < this.cols; c++) {
>                         this.gridObj.rows[r].cells[c].onclick = this.onclickHandler

Another capitalisation error - gridObj != gridobj.  It makes life much
easier if you post a "working" example.

Anyhow, here is where you want to set the onclick function's this
keyword, something like:

Grid.prototype.addHandlers = function() {

 // For convenience
 var grid = this;
 var table = this.gridObj;

 // Keep variables local, especially counters
 for (var r=0; r < this.rows; r++) {
   for (var c=0; c < this.cols; c++) {
     table.rows[r].cells[c].onclick = function(){
       grid.onclickHandler.call(table);
     }
   }
 }
}

A better strategy would be to add a single handler to the table, then
use the event object (event.target/srcElement) to find the cell that
was clicked on.  The above creates a large number of closures and
exercises IE's memory leak by having a circular closure involving a
DOM element. Unless you manually remove the hanlders, you will
eventually have memory problems.

The number of rows and columns need not be set as properties of the
grid object since they can be retrieved from the table.  That way if
you modify the table by adding or deleting rows or cells, you don't
have to update the corresponding grid object.

--
Rob
Murray Hopkins - 28 Feb 2007 04:34 GMT
Hi Rob,

Thanks for your reply. Firstly, sorry about the capitalisation errors. I
was abbreviating a much long object for clarity and wasnt careful enough.

I have played around with your suggestion:

ie: table.rows[r].cells[c].onclick =
function(){grid.onclickHandler.call(table);}

and have learned a lot about the .call function - thanks.

> A better strategy would be to add a single handler to the table, then
> use the event object (event.target/srcElement) to find the cell that
> was clicked on.  The above creates a large number of closures and
> exercises IE's memory leak by having a circular closure involving a
> DOM element. Unless you manually remove the hanlders, you will
> eventually have memory problems.

I am unsure about this. I understand what you are saying and have tried
it but cant figure out how to get the event object using mozilla when I
am in the handler within the grid object.

eg if I do this:
Grid.prototype.addHandlers = function() {
  var table = this.gridObj
  table.onclick = this.onclickHandler
}

Grid.prototype.onclickHandler = function(ev) {
  alert(this) // this = the table element object
  var elem = eventTarget(ev) // cross browser function to get the
target     alert("inhandler:"+elem) // the target element object ie the td
object
}

it works but I dont have a reference to the grid object itself.

And if I do this:
Grid.prototype.addHandlers = function() {
  var grid = this;
  var table = this.gridObj
  table.onclick = function(){grid.onclickHandler.call(table,grid);}
}

Grid.prototype.onclickHandler = function(gridObj) {
  alert(this) // this = the table element object
  alert("inhandler:"+gridObj) // ie a reference to the grid object

// but no reference to the event object

}

it works but I dont have an event object to enable me to figure out how
to access the cell that was clicked.

For IE I could use the event.srcElement but how can I access the event
object with mozilla since it isnt passed in via the ev parameter ?

> The number of rows and columns need not be set as properties of the
> grid object since they can be retrieved from the table.  That way if
> you modify the table by adding or deleting rows or cells, you don't
> have to update the corresponding grid object.

Indeed. That was part of the abbreviation for the example. The object
that calls the grid sets the dimensions of the grid dynamically (via a
method that I deleted from the example).

Thanks again,
Murray
RobG - 28 Feb 2007 09:20 GMT
> > A better strategy would be to add a single handler to the table, then
> > use the event object (event.target/srcElement) to find the cell that
[quoted text clipped - 6 lines]
> it but cant figure out how to get the event object using mozilla when I
> am in the handler within the grid object.

Gecko browsers (and others) will pass a reference to the event object
as the first argument to the function called by the event.  IE makes
it available as the global event object.

<URL: http://www.quirksmode.org/js/introevents.html >

> eg if I do this:
> Grid.prototype.addHandlers = function() {
[quoted text clipped - 12 lines]
>
> it works but I dont have a reference to the grid object itself.

Create a closure back to it.  Some browsers will let you add a
reference to the object to the DOM element, but not all (or even
enough).

> And if I do this:
> Grid.prototype.addHandlers = function() {
[quoted text clipped - 17 lines]
> For IE I could use the event.srcElement but how can I access the event
> object with mozilla since it isnt passed in via the ev parameter ?

See code below for addHandler() - note no 's'.

> > The number of rows and columns need not be set as properties of the
> > grid object since they can be retrieved from the table.  That way if
[quoted text clipped - 4 lines]
> that calls the grid sets the dimensions of the grid dynamically (via a
> method that I deleted from the example).

I wouldn't set it at all, just get it from the table if or when you
need it.

Here's my test example:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
  "http://www.w3.org/TR/html4/strict.dtd">
<head><title>Hi</title>
<style type="text/css">
 table {
   border-collapse: collapse;
   border-top: 1px solid blue;
   border-left: 1px solid blue;
 }
 td {
   border-bottom: 1px solid blue;
   border-right: 1px solid blue;
 }
</style>

<script>

function $(el) {
 return (typeof el=='string')? document.getElementById(el) : el;
}

function Grid(gridNumb) {
 this.gridNumb = gridNumb;
 this.gridObj = $('grid_'+gridNumb);
}

Grid.prototype.onclickHandler = function() {
  alert(this.rows)
}

// Old function
Grid.prototype.addHandlers = function() {
 var grid = this;
 var table = this.gridObj;
 var row;

 // Get num rows and cells from table, don't store in object
 for (var r=0, len=table.rows.length; r<len; r++) {
   row = table.rows[r]
   for (var c=0, len2=row.cells.length; c<len2; c++) {
     row.cells[c].onclick = function(){
       grid.onclickHandler.call(table);
     }
   }
 }
}

// New function
Grid.prototype.addHandler = function()
{
 var grid = this;
 grid.gridObj.onclick = function(e) {
   var e = e || window.event;
   var tgt = e.target || e.srcElement;

   // Fix to get type 1 if type 3 (text node) returned
   while(tgt.nodeType != 1) tgt = tgt.parentNode;

   // Here's the element
   alert( tgt.textContent || tgt.innerText );

   // Closure back to the grid object
   alert('grid is an ' + typeof grid);
 }
}
window.onload = function(){
 var x = new Grid('0');
 x.addHandler();
}
</script>

</head>
<body>

<table id="grid_0">
<tr><td>cell 0 0<td>cell 0 1<td>cell 0 2
<tr><td>cell 1 0<td>cell 1 1<td>cell 1 2
</table>

</body>

--
Rob
Murray Hopkins - 28 Feb 2007 22:20 GMT
See the Solution I posted.
Murray Hopkins - 28 Feb 2007 22:19 GMT
Thanks to Rob for his explanation. Here is a working example of the
solution. Tested on IE 6  and FF 2. I have included comments to explain
what is going on. The layout isnt good here so copy and paste into your
editor.

This is a cut down version of the actual object to highlight the
methodology of attaching an object to a html element so that you can get
a reference to the object when it is clicked. This could be extended to
mousing over etc.

This is especially useful where the object creates the html element that
it is bound to.

Cheers,
Murray

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
    <title>Untitled</title>
   
<script type='text/javascript'>
function Grid(gridNumb,idName) {
    this.rows = 1;
    this.cols = 1;
    this.gridNumb = gridNumb;
    this.containerObj = document.getElementById(idName);
    this.gridhtml = '';
    this.onclickHandlerExt = null;
    this.tableObj = null;
}

Grid.prototype.setDimensions = function(r,c) {
    this.rows = r;
    this.cols = c;
}

                // Create a table of the specified dimensions then display it
Grid.prototype.create = function() {
    var txt = ''
    txt += '<table border=1 id="_grid_' + this.gridNumb + '">'
    for (r=0; r < this.rows; r++) {
        txt += '<tr>';
        for (c=0; c < this.cols; c++) {
            cellNumb = (r*this.cols) + c
            txt += '<td>' + cellNumb + '</td>';   
        }   
        txt += '</tr>';
    }
    txt += '</table>'

    this.containerObj.innerHTML = txt
                        // Save the object reference to this table
    this.tableObj = document.getElementById("_grid_" + this.gridNumb)
}

                // Allow the calling page to specify a function that will
                // execute when a cell is clicked
Grid.prototype.assignOnClickHandlerExternal = function(handler) {
    this.onclickHandlerExternal = handler;
}
                // Add an event handler to the table. This is the key function in
this example
Grid.prototype.addHandler = function() {
  var thisgrid = this;
  var table = thisgrid.tableObj;
  table.onclick =
function(ev){thisgrid.onclickHandler.call(table,ev,thisgrid);}

/* NOTES:
1. Using function(ev) passes the mozilla event object to the function
onclickHandler when
the table is clicked. Not required by IE.
2. Prefixing the onclickHandler with thisgrid (thisgrid.onclickHandler)
sets the scope to this instance of the grid object
3. Using call (thisgrid.onclickHandler.call) ensures that the function
onclickHandler is part
of this instance of the grid object
4. The parameters to call:
table: a reference to the table object
ev: the mouse event
thisgrid: a reference to this instance of the grid object
*/

}               
                // This is the function called by the table onclick event handler
set above
Grid.prototype.onclickHandler = function(ev,thisGrid) {
               
                // Just to show that the "this" here is a reference to
                // the table object (passed as the first parameter in the .call above)
                // rather than a reference to the grid object itself
    var table = this
   
                // Cross browser function to get the elememt that was clicked  (see
below)
    var eventDetails = eventTarget(ev)
   
                // Extract the various bits of information that might be used later
    var td = eventDetails.elem
    var evnt = eventDetails.ev
    var col = td.cellIndex
    var row = td.parentNode.rowIndex
    var cellInfoObj = {tableObj:thisGrid, row:row, col:col, cell:td}
   
                // Return the extracted data to the external event hanlder function
                // plugged in when the object was created
    thisGrid.onclickHandlerExternal(evnt,cellInfoObj)
}

// Not a method
function eventTarget(e){
                        // Get the element object that triggered the event
                        // After Goodman - Dynamic HTML Definitive Reference V2
    e = (e) ? e : ((event) ? event : null);
    var elem = null
    if (e) { elem = (e.target) ? e.target : ((e.srcElement) ?
e.srcElement : null);    }
                        // Return both the element that was clicked and the event object
itself
    return {elem:elem, ev:e}
}

</script>

<script type='text/javascript'>
var grid = null
function processLoad() {
                        // Create the grid object
    grid = new Grid(0,'_grid_0_container');
    grid.setDimensions(6,8);
                        // Assign the onclick event handler (see below)
    grid.assignOnClickHandlerExternal(handleClick);
                        // Create the grid table
    grid.create();
                        // Add the event handler we passed in above
    grid.addHandler()
}
            // This is the function that receives the data about the cell that
was clicked
            // ie the end point of the onclick event
            // Just display the results
function handleClick(eventObj,cellInfo) {
    var txt = ''
    txt += 'this = a reference to the grid object iteself' + '<br>'
    txt += 'gridNumb = ' + cellInfo.tableObj.gridNumb + '<br>'
    txt += 'eventObj = ' + eventObj + '<br>'
    txt += 'row = '+cellInfo.row + '<br>'
    txt += 'col = '+cellInfo.col + '<br>'
    txt += 'td = ' +cellInfo.cell + '<br>'
    txt += 'td content = ' +cellInfo.cell.innerHTML + '<br>'
    document.getElementById("displayresults").innerHTML = txt
}

</script>
   
</head>

<body onload="processLoad()">
Grid example. Click a cell
<div id="_grid_0_container"></div>
Results:
<div id="displayresults" style="border:1px solid silver;
width:300px;"></div>
</body>
</html>
Murray Hopkins - 28 Feb 2007 22:53 GMT
A small adjustment to make the variables local.

I said:
>     for (r=0; r < this.rows; r++) {
>         txt += '<tr>';
[quoted text clipped - 4 lines]
>         txt += '</tr>';
>     }

Should use var :
     for (var r=0; r < this.rows; r++) {
         txt += '<tr>';
         for (var c=0; c < this.cols; c++) {
             cellNumb = (r*this.cols) + c
             txt += '<td>' + cellNumb + '</td>';
         }
         txt += '</tr>';
     }

Slack.
Murray
 
Sign In
Join
My Latest Posts
My Monitored Threads
My Blog
My Photo Gallery
My Profile
My Homepage

Start New Thread
Enable EMail Alerts
Rate this Thread



©2009 Advenet LLC   Privacy Policy - Terms of Use
This website includes both content owned or controlled by Advenet as well as content owned or controlled by third parties.