I created a class called SearchBox to handle search interaction (delayed trigger, search on enter key press, preventing searches while one is active, synchronizing results when a search completes and the text has changed, etc.).
All the class methods are prototype methods, meant to be accessed via this
. In the following code, assume p
is the class's prototype.
p.registerListeners = function () {
$(this.element).on('keypress', this.searchKeyPressed);
};
p.unregisterListeners = function () {
$(this.element).off('keypress', this.searchKeyPressed);
};
That doesn't work, because when the keypress event calls the searchKeyPressed
handler, it doesn't do so in the context of this
. The only solution I can come up with is one that only modern browsers support, which is to bind the callback to this
, which actually creates a new function. Since it creates a new function, I have to cache it in order to remove it later, since I have to pass the same reference to off
that I passed to on
.
Is there a better way to do it than this, or is this ok?
var boundKeyPressed;
p.registerListeners = function () {
boundKeyPressed = this.searchKeyPressed.bind(this);
$(this.element).on('keypress', boundKeyPressed);
};
p.unregisterListeners = function () {
$(this.element).off('keypress', boundKeyPressed);
};
I thought that maybe jQuery.on
would provide a way to do this event binding automatically, but instead it seems like it binds this
to different things depending on how it's called. For example, when using on('eventname',instance.func)
, this
is the "currentTarget" (not necessarily "target" in bubbling terms), whereas when using on('eventname','selector',instance.func)
, this
refers to the element matching the selector. In either case, the func
runs as though it has no relationship with instance
.
This is where bind (), call () and apply () come in handy. All three are built-in methods on functions. Invoking bind () on any function returns a copy of the function where ‘ this ’ is set to the first argument passed into bind. Which is how you get to determine what, exactly, ‘ this ’ is:
The invoking object contains the call site, and the call site determines the ‘ this ’ binding. Now, as promised, we are back to cover the worst case ‘ this ’ scenario callbacks, or how to find the ‘ this ’ binding for a function that is passed as an argument into another function.
So, the first argument is the callback, and the value this should refer to the second one. Another common expression of this problem is when an object method is used as a callback handler. Functions are prior in JavaScript, and the term “method” is considered a colloquial naming for a function, which is a value of an object property.
A callback function is a lucky function that gets passed into the enclosing higher-order function: The callback function gets executed (called) inside the higher order function, but not necessarily immediately.
If you add a namespace to your events you can bind events and easily unbind them all at once.
To bind:
$(this.element).on('keypress.mynamespace', this.searchKeyPressed.bind(this));
To unbind:
$(this.element).off('.mynamespace');
First, unless you expect your page to be very long lived (or you're listening to keypresses with hundreds of things, which is a very big architecture problem), this isn't going to be much of an issue, where memory is concerned, even if phones had "keys".
Second, .bind
has great support for all browsers less than half a decade old, and is dirt simple to polyfill.
Third: you're 100% right, that it's not cool to have to cache the function to be able to deal with it later, so let's do something about that.
There's a little known trick to addEventListener
(and attachEvent
), in that it happily supports objects, with handleEvent
methods on them.
I don't use this for everything, as sometimes it's really just not worth it, but for game-making, I've used it for inputs, kind of like so:
class Input {
constructor (events) {
this.events = events || [];
}
handleEvent (e) {
var input = this;
var method = e.type;
if (typeof input[method] === "function") {
input.dispatchEvent(method, e);
}
}
dispatchEvent (method, content) {
var input = this;
input[method](content);
}
listen (el, events) {
var input = this;
events = events || input.events;
events.forEach(event => el.addEventListener(event, input));
return this;
}
ignore (el, events) {
var input = this;
events = events || input.events;
events.forEach(event => el.removeEventListener(event, input));
return this;
}
}
class Keyboard extends Input {
constructor () {
super(["keydown", "keyup"]);
var keyboard = this;
keyboard.keys = new Set();
}
press (key) { this.keys.add(key); }
release (key) { this.keys.delete(key); }
isPressed (key) { return this.keys.has(key); }
keydown (e) {
var key = e.keyCode;
this.press(key);
}
keyup (e) {
var key = e.keyCode;
this.release(key);
}
}
I could then:
var gameplayEvents = ["keyup", "keydown"];
var keyboard = new Keyboard();
keyboard.listen(canvas, gameplayEvents);
// ongameover
keyboard.ignore(canvas, gameplayEvents);
And if you'll note, it's all 100% pure JS.
No jQuery, extJS, etc.
And really, it's seriously not a lot more code, either.
I could make it one object-literal, if I just needed one instance to handle mouseup and mousedown; really all I need is an object with a handleEvent, to become this
inside of the handleEvent callback.
There's only one instance to worry about. I don't cache anything extra, if I need to unregister.
jQuery (and others) actually use this internally, to optimize the atrocious code which they're typically abused into producing.
Yes, perhaps I'm cheating by using ES6... ...but it's not necessary at all.
It's just more cheerful than what I used to do:
function Parent (args) { }
extend(Parent.prototype, { /*public-methods*/ });
function Child (args) {
Parent.call(this);
// construct
}
extend(
Child.prototype,
Parent.prototype,
{ /*public-override-methods*/ },
{ constructor: Child }
);
And again, there are lots of times when bind is 100% valid.
There's a proposal right now, for an ES7 version of bind, which would potentially produce the same value, every time it's called (if it goes through that way).
With the added benefit that the syntax allows for chaining all kinds of awesome things together, as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With