Problem with object references when using bound event handlers

M

Murray Hopkins

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
 
R

RobG

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...

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

e.g. :)
// 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

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() {
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

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.
 
M

Murray Hopkins

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
 
R

RobG

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.

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.

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.

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() {
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 ?

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

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).

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>
 
M

Murray Hopkins

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>
 
M

Murray Hopkins

A small adjustment to make the variables local.

I said:
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>';
}

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
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,020
Latest member
GenesisGai

Latest Threads

Top