Question

I want all buttons to perform an action before and after their normal onclick event. So I came up with the "brilliant" idea of looping through all those elements and creating a wrapper function.

This appeared to work pretty well when I tested it, but when I integrated it into our app, it fell apart. I traced it down to the 'this' value was changed by my wrapper. The sample code illustrates this; before you wrap the event handlers, each button displays the button id when you click, but after wrapping it the displayed name is 'undefined' in this example, or 'Form1' if you run it from within a form.

Does anybody know either a better way to do the same thing? Or a good way to maintain the originally intended 'this' values?

As you can imagine, I don't want to modify any of the existing event handler code in the target buttons.

Thanks in advance.

PS-The target browser is IE6 & up, crossbrowser functionality not required

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<script language="javascript" type="text/javascript">
    function btnWrap_onClick()
    {
        var btns = document.getElementsByTagName("button");
        for( var i = 0; i < btns.length; i++)
        {
            var btn = btns[i];

            // handle wrap button differerntly
            if( "btnWrap" == btn.id)
            {
                btn.disabled = true;
                continue; // skip this button
            }

            // wrap it
            var originalEventHandler = btn.onclick;
            btn.onclick = function()
            {
                alert("Starting event handler");
                originalEventHandler();
                alert("Finished event handler");
            }
        }

        alert("Buttons wrapped successfully");
    }
</script>
<body>
    <p>
    <button id="TestButton1" onclick="alert(this.id);">TestButton1</button>
    <button id="TestButton2" onclick="alert(this.id);">TestButton2</button>
    </p>
    <button id="btnWrap" onclick="btnWrap_onClick();">Wrap Event Handlers</button>
</body>
</html>
Was it helpful?

Solution

Like Paul Dixon said, you could use call but I suggest you use apply instead.

However, the reason I am answering is that I found a disturbing bug: You are actually replacing all your event handlers with the event handler of the last button. I don't think that was what you intended, was it? (Hint: You are replacing the value for originalEventHandler in each iteration)

In the code below you find a working cross-browser solution:

function btnWrap_onClick()
{
    var btns = document.getElementsByTagName("button");
    for( var i = 0; i < btns.length; i++)
    {
        var btn = btns[i];

        // handle wrap button differerntly
        if( "btnWrap" == btn.id)
        {
            btn.disabled = true;
            continue; // skip this button
        }

        // wrap it

        var newOnClick = function()
        {
            alert("Starting event handler");
            var src=arguments.callee;
            src.original.apply(src.source,arguments);
            alert("Finished event handler");
        }
        newOnClick.original = btn.onclick; // Save original onClick
        newOnClick.source = btn; // Save source for "this"
        btn.onclick = newOnClick; //Assign new handler
    }
alert("Buttons wrapped successfully");
}

First I create a new anonymous function and store that in the variable newOnClick. Since a function is an object I can create properties on the function object like any other object. I use this to create the property original that is the original onclick-handler, and source that is the source element that will be the this when the original handler is called.

Inside the anonymous function I need to get a reference to the function to be able to get the value of the properties original and source. Since the anonymous function don't have a name I use use arguments.callee (that has been supported since MSIE5.5) to get that reference and store it in variable src.

Then I use the method apply to execute the original onclick handler. apply takes two parameters: the first is going to be the value of this, and the second is an array of arguments. this has to be the element where the original onclick handler was attached to, and that value was saved in source. arguments is an internal property of all functions and hold all the arguments the function was called with (notice that the anonymous function don't have any parameters specified, but if it is called with some parameters anyway, they will be found in the arguments property).

The reason I use apply is that I can forward all the arguments that the anonymous function was called with, and this makes this function transparent and cross-browser. (Microsoft put the event in window.event but the other browsers supplies it in the first parameter of the handler call)

OTHER TIPS

You can use the call method to resolve the binding, e.g. originalEventHandler.call(btn);

Alternatively, a library like prototype can help - its bind method lets you build a new function bound to a specified object, so you'd have declared originalEventHandler as var originalEventHandler = btn.onclick.bind(btn);

Finally, for a good backgrounder on binding issues, see also Getting Out of Binding Situations in JavaScript

Your problem is the way closures work in JavaScript. Honestly, I'd recommend using a framework. Any of them should make event-handling far nicer than doing it by hand.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top