34

Is there a best-practice or common way in JavaScript to have class members as event handlers?

Consider the following simple example:

<head>
    <script language="javascript" type="text/javascript">

        ClickCounter = function(buttonId) {
            this._clickCount = 0;
            document.getElementById(buttonId).onclick = this.buttonClicked;
        }

        ClickCounter.prototype = {
            buttonClicked: function() {
                this._clickCount++;
                alert('the button was clicked ' + this._clickCount + ' times');
            }
        }

    </script>
</head>
<body>
    <input type="button" id="btn1" value="Click me" />
    <script language="javascript" type="text/javascript">
        var btn1counter = new ClickCounter('btn1');
    </script>
</body>

The event handler buttonClicked gets called, but the _clickCount member is inaccessible, or this points to some other object.

Any good tips/articles/resources about this kind of problems?

Nonemoticoner
  • 650
  • 5
  • 14
JacobE
  • 8,021
  • 7
  • 40
  • 48

5 Answers5

46
ClickCounter = function(buttonId) {
    this._clickCount = 0;
    var that = this;
    document.getElementById(buttonId).onclick = function(){ that.buttonClicked() };
}

ClickCounter.prototype = {
    buttonClicked: function() {
        this._clickCount++;
        alert('the button was clicked ' + this._clickCount + ' times');
    }
}

EDIT almost 10 years later, with ES6, arrow functions and class properties

class ClickCounter  {
   count = 0;
   constructor( buttonId ){
      document.getElementById(buttonId)
          .addEventListener( "click", this.buttonClicked );
  }
   buttonClicked = e => {
     this.count += 1;
     console.log(`clicked ${this.count} times`);
   }
}

https://codepen.io/anon/pen/zaYvqq

pawel
  • 35,827
  • 7
  • 56
  • 53
  • Thanks for your solution. I am also trying to understand the bigger picture - patterns, practices etc. Any good articles you know about? – JacobE Oct 23 '08 at 09:36
  • 8
    This is a good example, but this answer might be better with some explanation as to why it's a good solution, for the novices around here. – Sam Murray-Sutton Dec 05 '08 at 14:31
  • 1
    The ES2015 edit works via Babel but the public field syntax is not allowed in all browsers yet (even now in 2019). – Pointy Mar 23 '19 at 13:16
  • didn't quite work in my application. The underlying class is built by a factory, which also sets the event listener. The 'this' variable when the event fires is then that of the factory. Trying to figure out how addEventListener knows what 'this' is to be used when the event handler is called .. –  Jun 01 '19 at 19:11
  • changed the handler method from clicked(){ ...} to clicked = () => {...} and it works. Because anonymous functions use 'this' differently I gather? –  Jun 01 '19 at 19:35
  • >> Because anonymous functions use 'this' differently I gather? Well it's because the lambda captures the this pointer upon its definition. So the handler in that case is a function object with state, not a function like a normal method. – QBziZ Mar 01 '21 at 09:10
15

I don't know why Function.prototype.bind wasn't mentioned here yet. So I'll just leave this here ;)

ClickCounter = function(buttonId) {
    this._clickCount = 0;
    document.getElementById(buttonId).onclick = this.buttonClicked.bind(this);
}

ClickCounter.prototype = {
    buttonClicked: function() {
        this._clickCount++;
        alert('the button was clicked ' + this._clickCount + ' times');
    }
}
JimmyB
  • 12,101
  • 2
  • 28
  • 44
kucherenkovova
  • 694
  • 9
  • 22
9

A function attached directly to the onclick property will have the execution context's this property pointing at the element.

When you need to an element event to run against a specific instance of an object (a la a delegate in .NET) then you'll need a closure:-

function MyClass() {this.count = 0;}
MyClass.prototype.onclickHandler = function(target)
{
   // use target when you need values from the object that had the handler attached
   this.count++;
}
MyClass.prototype.attachOnclick = function(elem)
{
    var self = this;
    elem.onclick = function() {self.onclickHandler(this); }
    elem = null; //prevents memleak
}

var o = new MyClass();
o.attachOnclick(document.getElementById('divThing'))
AnthonyWJones
  • 187,081
  • 35
  • 232
  • 306
5

You can use fat-arrow syntax, which binds to the lexical scope of the function

function doIt() {
  this.f = () => {
    console.log("f called ok");
    this.g();
  }
  this.g = () => {
    console.log("g called ok");
  }
}

After that you can try

var n = new doIt();
setTimeout(n.f,1000);

You can try it on babel or if your browser supports ES6 on jsFiddle.

Unfortunately the ES6 Class -syntax does not seem to allow creating function lexically binded to this. I personally think it might as well do that. EDIT: There seems to be experimental ES7 feature to allow it.

Community
  • 1
  • 1
Tero Tolonen
  • 4,144
  • 4
  • 27
  • 32
2

I like to use unnamed functions, just implemented a navigation Class which handles this correctly:

this.navToggle.addEventListener('click', () => this.toggleNav() );

then this.toggleNav() can be just a function in the Class.

I know I used to call a named function but it can be any code you put in between like this :

this.navToggle.addEventListener('click', () => { [any code] } );

Because of the arrow you pass the this instance and can use it there.

Pawel had a little different convention but I think its better to use functions because the naming conventions for Classes and Methods in it is the way to go :-)