Link to home
Start Free TrialLog in
Avatar of elepil
elepil

asked on

Need explanation why click handler won't work in JavaScript/jQuery code

I was trying to create a click handler for a button, and I couldn't understand why the click handler wasn't working. Clicking the button had absolutely no effect, as if there was no click handler at all.

Below is the actual code in my application where this problem occurred. I apologize for the inclusion of a very lengthy function, but ignore it because the problem is not there; I had to include it in order for the issue to occur. The code in question is way down at the bottom, where you will see /* comments explaining the my question */:

<!DOCTYPE html>
<html>
<head>
<title></title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<!--<script src="../../_scripts/utils.js"></script>-->
<style>
    html {
        height: 100%;
    }
    body {
        height: 100%;
    }
    #frmSearchCustomer {
        width: 600px;
        height: 400px;
        background: green;
        display: none;
    }
</style>
<script>
$(function() {
    function Overlay(sectionId, options)
    {
        var arrContents;
        var arrClickablesIds;
        var fnCallback;
        var _iFadeInTime;
        var _iFadeOutTime;
        var _sRGBAColor;
        var _zIndex;
        var _sOverflow;

        if (sectionId === undefined || sectionId === null || sectionId.trim().length === 0) {
            throw new Error("From Overlay(): First parameter sectionId is required.");
        } else {
            if ($(sectionId).length === 0) { 
                sectionId = validateId(sectionId); 
                if ($(sectionId).length === 0) { 
                    throw new Error("From Overlay(): '" + sectionId + "' selector provided not found.");
                }
            }
        } // END if

        if (options === undefined || options === null) {
            arrContents = [];
            arrClickablesIds = [];
            fnCallback = null;
            _iFadeInTime = 0;
            _iFadeOutTime = 0;
            _sRGBAColor = "rgba(0, 0, 0, 0)";
            _zIndex = 2;
            _sOverflow = "hidden";
        } else {
            arrContents = (options.contents === undefined || options.contents === null) ? [] : options.contents;
            for (var i=0; i<arrContents.length; i++) {
                var _temp = arrContents[i];
                if (_temp.contentId === undefined || 
                    _temp.contentId === null || 
                    _temp.contentId.trim().length === 0) {
                    throw new Error("From Overlay(): Every 'content' element must have a valid contentId");
                } else {
                    /* Make sure there's a # before the id */
                    _temp.contentId = validateId(_temp.contentId);
                    var _test = $(_temp.contentId);
                    if (_test.attr("id") === undefined) {
                            throw new Error("From Overlay(): Every 'content' element must have a valid contentId");
                    }// END if
                }// END if
            } // END for i

            arrClickablesIds = (options.clickables === undefined || options.clickables === null) ? [] : options.clickables;

            if (arrClickablesIds.length > 0) {
                for (var c=0; c<arrClickablesIds.length; c++) {
                    arrClickablesIds[c] = validateId(arrClickablesIds[c]);
                    // Create click events for these clickables using
                    // the fnCallback as the handler.
                    $(arrClickablesIds[c]).click(function() {
                        // We pass the clickable's id to fnCallback
                        fnCallback($(this).attr("id"));
                    });
                }
            }

            fnCallback = (options.callback === undefined) ? null : options.callback;
            if (arrClickablesIds.length > 0) {
                if (!(fnCallback instanceof Function)) {
                    throw new Error("From Overlay(): 'clickables' detected, 'callback' function is required.");
                }
            }
            _iFadeInTime = (options.fadeInTime === undefined || options.fadeInTime === null ||
                            isNaN(options.fadeInTime)) ? 0 : parseInt(options.fadeInTime);
            _iFadeOutTime = (options.fadeOutTime === undefined || options.fadeOutTime === null ||
                             isNaN(options.fadeOutTime)) ? 0 : parseInt(options.fadeOutTime);
            _sRGBAColor = (options.rgbaColor === undefined || options.rgbaColor === null ||
                           options.rgbaColor.length === 0) ? "rgbaColor(0, 0, 0, 0)" : options.rgbaColor;
            _zIndex = (options.zIndex === undefined || options.zIndex === null ||
                       isNaN(options.zIndex)) ? 2 : parseInt(options.zIndex);
            _sOverflow = (options.overflow === undefined || options.overflow === null ||
                          options.overflow.length === 0) ? "visible" : options.overflow;
        }

        var TOPLEFT = "topleft";
        var TOPCENTER = "topcenter";
        var TOPRIGHT = "topright";
        var MIDDLELEFT = "middleLeft";
        var CENTER = "center";
        var MIDDLERIGHT = "middleRight";
        var BOTTOMLEFT = "bottomleft";
        var BOTTOMCENTER = "bottomcenter";
        var BOTTOMRIGHT = "bottomright";
        var MANUAL = "manual";
        var O = "|topleft|topcenter|topright|middleLeft|center|" +
                "middleRight|bottomleft|bottomcenter|bottomright|" +
                "manual|";

        var _sSectionId = sectionId;
        var _bShowing = false;
        var _bAdded = false;

        for (var j=0; j<arrContents.length; j++) {
            var _temp = arrContents[j];
            if (_temp.orientation === undefined ||
                _temp.orientation === null ||
                _temp.orientation.trim().length === 0) {
                _temp.orientation = CENTER;
            } else {
                if (O.toLowerCase().indexOf("|" + _temp.orientation + "|") === -1)
                        _temp.orientation = CENTER;
            } // END if
            if (_temp.x === undefined || _temp.x === null || isNaN(_temp.x)) {
                _temp.x = 0;
            } // END if
            if (_temp.y === undefined || _temp.y === null || isNaN(_temp.y)) {
                _temp.y = 0;
            } // END if
        } // END for j

        var _section = $(_sSectionId);
        var _arrContentRefs = new Array();
        for (var m=0; m<arrContents.length; m++) {
            var _sContentIdTemp = arrContents[m].contentId;
            _arrContentRefs.push($(_sContentIdTemp));
        } // END for m

        var _resizeHandler;
        var _createResizeHandler = function() {
            _resizeHandler = function() {
                /* Resize the _divWrapper */
                _divWrapper.css({
                    width : "100%",
                    height : "100%",
                    top    : "0px",
                    left   : "0px"
                });
                for (var q=0; q<_arrContentRefs.length; q++) {
                    var _iDivWrapperWidth = _divWrapper.width();
                    var _iDivWrapperHeight = _divWrapper.height();
                    var _iContentWidth = _arrContentRefs[q].width();
                    var _iContentHeight = _arrContentRefs[q].height();
                    var _iX = arrContents[q].x;
                    var _iY = arrContents[q].y;
                    var _iCalculatedX;
                    var _iCalculatedY;

                    var _tempOrientation = arrContents[q].orientation;

                    switch(_tempOrientation) {
                        case TOPLEFT:
                            _iCalculatedX = _iX;
                            _iCalculatedY = _iY;
                            break;
                        case TOPCENTER:
                            _iCalculatedX = ((_iDivWrapperWidth - _iContentWidth) / 2) + _iX;
                            _iCalculatedY = _iY;
                            break;
                        case TOPRIGHT:
                            _iCalculatedX = (_iDivWrapperWidth - _iContentWidth) - 1 + _iX;
                            _iCalculatedY = _iY;
                            break;
                        case MIDDLELEFT:
                            _iCalculatedX = _iX;
                            _iCalculatedY = (_iDivWrapperHeight - _iContentHeight) / 2 + _iY;
                            break;
                        case CENTER:
                            _iCalculatedX = ((_iDivWrapperWidth - _iContentWidth) / 2) + _iX;
                            _iCalculatedY = (_iDivWrapperHeight - _iContentHeight) / 2 + _iY;
                            break;
                        case MIDDLERIGHT:
                            _iCalculatedX = (_iDivWrapperWidth - _iContentWidth) - 1 + _iX;
                            _iCalculatedY = (_iDivWrapperHeight - _iContentHeight) / 2 + _iY;
                            break;
                        case BOTTOMLEFT:
                            _iCalculatedX = _iX;
                            _iCalculatedY = (_iDivWrapperHeight - _iContentHeight) - 1 + _iY;
                            break;
                        case BOTTOMCENTER:
                            _iCalculatedX = ((_iDivWrapperWidth - _iContentWidth) / 2) + _iX;
                            _iCalculatedY = (_iDivWrapperHeight - _iContentHeight) + _iY;
                            break;
                        case BOTTOMRIGHT:
                            _iCalculatedX = (_iDivWrapperWidth - _iContentWidth) - 1 + _iX;
                            _iCalculatedY = (_iDivWrapperHeight - _iContentHeight) + _iY;
                            break;
                        case MANUAL:
                            _iCalculatedX = _iX;
                            _iCalculatedY = _iY;
                            break;
                        default:
                                throw new Error("Overlay(): FATAL ERROR! Orientation-related!");
                    } // END switch

                    _arrContentRefs[q].css({
                        left : _iCalculatedX,
                        top  : _iCalculatedY
                    });
                }
            };
            $(window).resize(_resizeHandler);
        }; // END: _createResizeHandler()
        var _divWrapper = $("<div></div>");

        _divWrapper.css({
            position   : "absolute",
            background : _sRGBAColor,
            opacity    : 1,
            zIndex    : _zIndex,
            display    : "none"
        }); // END _divWrapper.css()
        for (var n=0; n<_arrContentRefs.length; n++) {
            _divWrapper.append(_arrContentRefs[n]);
        } // END for n

        this.show = function() {
            if (!_bShowing) {
                _createResizeHandler();

                if (!_bAdded) {
                    _section.css({
                        position : "relative"
                    });
                   $(_section).append(_divWrapper);
                    _divWrapper.css({
                        display  : "block",
                        position : "absolute",
                        opacity  : 0,
                        overflow : _sOverflow
                    });
                    _bAdded = true;
                } else {
                    _divWrapper.css("display", "block");
                }

                _divWrapper.stop().animate({ opacity: 1}, { duration: _iFadeInTime });

                for (var o=0; o<_arrContentRefs.length; o++) {
                    _arrContentRefs[o].css({
                            display  : "block",
                            position : "absolute",
                            opacity  : 1
                    });
                }

                _resizeHandler();
                _bShowing = true;
            }
        }; // END show()

        this.hide = function() {
            if (_bShowing) {
                $(window).unbind("resize", _resizeHandler);
                _resizeHandler = null;
                _divWrapper.stop().animate({ opacity: 0 }, 
                    { 
                        duration: _iFadeOutTime, 
                        queue: false,
                        complete: function() {
                            _divWrapper.css("display", "none");
                            _bShowing = false;
                        }
                    });
            }
        }; // END hide()

        function validateId(sId) {
            return sId.charAt(0) === '#' ? sId : '#' + sId;
        }
    }; // END: function Overlay()
    
    $("#btnShowGreenSquare").click(function() {
        sc.show();
    });
    $("#btnHideGreenSquare").click(function() {
        sc.hide();
    });

    /** PLEASE IGNORE EVERYTHING ABOVE, THE PROBLEM IS BELOW. **/

    function SearchCustForm(sSectionID, fnCallBack, zIndex) {

        var self = this;
        
        var ovSCF = new Overlay(sSectionID,
                             { 
                                 contents : [{ contentId : "frmSearchCustomer", orientation : "center" }],
                                 zIndex: zIndex
                             });
                             
        /*****************************************
         * This is the click handler routine I'm *
         * talking about that won't work. There  * 
         * are no errors. So WHY WON'T IT        *
         * WORK????                                *
         *****************************************/
        $("#btnCancel").click(function() {
            self.hide();
        });

        this.show = function() {
            ovSCF.show();
        };
        
        this.hide = function() {
            ovSCF.hide();
        };
    }
    
    var sc = new SearchCustForm("container", null, 5);    
});
</script>
</head>
<body>
    <div id="container" style="height: 80%;">
        <div id="frmSearchCustomer"><input id="btnCancel" type="button" value="Cancel" /></div>
    </div>
    <input id="btnShowGreenSquare" type="button" value="Show Green Square" />
    <input id="btnHideGreenSquare" type="button" value="Hide Green Square" />
</body>
</html>

Open in new window


Here's what to expect: When you run the code snippet on a page, you will see two buttons called "Show Green Square" and "Hide Green Square". Click the show button and you will see a big green square with a Cancel button in it. The Cancel button is supposed to hide the green square. Click the Cancel button inside the green square, and nothing happens -- THAT is the problem.

Now, let me tell you the solution, and here is where I need someone to explain to me WHY it works. All I had to do was move the click handler code ABOVE the line before it. Then it started working fine. Currently, the code looks like this:

        var ovSCF = new Overlay(sSectionID,
                             { 
                                 contents : [{ contentId : "frmSearchCustomer", orientation : "center" }],
                                 zIndex: zIndex
                             });
                             
        $("#btnCancel").click(function() {
            self.hide();
        });

Open in new window


Swap the above two statements in position so it looks like this:

        $("#btnCancel").click(function() {
            self.hide();
        });
        
        var ovSCF = new Overlay(sSectionID,
                             { 
                                 contents : [{ contentId : "frmSearchCustomer", orientation : "center" }],
                                 zIndex: zIndex
                             });

Open in new window


And now it works. Can someone please explain to me why swapping these two UNRELATED statements made the click handler work?

Thank you.
Avatar of Rob
Rob
Flag of Australia image

i'm not sure why you are defining that event there... as you are using SearchCustForm as a class, that will never be called.  You should put this code outside of the SearchCustForm function.
Avatar of elepil
elepil

ASKER

To Rob Jurd. Thanks for responding.

Actually I have to put the event inside because SearchCustForm is a "component". It's not a component in the classic sense of the word (where everything is compactly encapsulated), but it's meant to be reusable in various parts of the application, and hence, all events to form controls have to be within it.

Did you see the weirdness?
That does mean that every time you instantiate the SearchCustForm object, you are overwriting the event each time.

I'll look into it now and see what I can muster
Avatar of elepil

ASKER

Whichever application decides to instantiate SearchCustForm should only instantiate it once. Once instantiated, the show() and hide() methods should be utilized from thereon. So all events are created only once, nothing is ever overwritten.

To me, this issue is a developer's nightmare where a problem crops up and is resolvable only by altering the order of the statements. This is the second time in my entire career I've encountered this, first time in Actionscript. But Actionscript had an excuse, they used time frames. JavaScript does not have that excuse, so I'm really curious why this issue occurs.
It does actually work, however there's an issue with how your overlay is adding/removing the elements at runtime (see line 232 in your code).
the button needs to be present on the page when it loads to have the click handler but the overlay is removing it.

conceptually, what are you trying to achieve?  something like a modal?  https://jqueryui.com/dialog/
Avatar of elepil

ASKER

Rob, I don't get it. Line 232 is not removing anything. What led you to think Overlay() is removing it??

Overlay is more than modal, although it can. It's a multipurpose constructor function that can do modal, or overlay any section of the screen. The instantiated <div> can contain anything and allows very precise positioning of elements within it. Because of its multi-purpose functionality, I didn't use jQuery's because it was too limiting.
I stepped through your code.  At that line the button disappeared.  I added .html() to the content being appended and that helped but didn't fix your issue.

It seems you are wrapping the content of the form with your Overlay right?
Avatar of elepil

ASKER

Yes, I am overlaying a <div> on top of the <div> with the id of frmSearchCustomer.

I am not understanding how the event of a button existing outside of Overlay could possibly get tangled with inside the constructor function. If you look at line 274, that's where I'm animating the overlay down to opacity zero, then in 279, I set display: none to make sure it is out of the DOM. So the overlay, once created, is never destructively removed, just hidden. So I'm puzzled when you suggest a constructor function that doesn't remove anything end up destroying something outside it. But hey, I know your caliber, so you must know something I don't, but this would have to be something really weird.
change your event to this for your button:

                $("body").on('click', '#btnCancel', function () {
                    console.log(self);
                    self.hide();
                });

Open in new window

ASKER CERTIFIED SOLUTION
Avatar of Rob
Rob
Flag of Australia image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
The event handler is attached to the body element (which hopefully is always on the page!) and looks for an element with the id, btnCancel to attach that click event.
Avatar of elepil

ASKER

Rob, thanks for the time you've spent on this. Many thanks to you, I found out exactly why this was happening because you led me down the right path. And because you've spent time on this, and if you canre to know, I owe you the exact explanation of the cause.

Remember that my assignment of a click handler to btnCancel was AFTER the instantiation of Overlay, and that was the scenario when things wouldn't work, right? Here's the exact reason why. The following code inside Overlay is where frmSearchCust is being added to Overlay's internally instantiated <div>:

        for (var n=0; n<_arrContentRefs.length; n++) {
            _divWrapper.append(_arrContentRefs[n]);
        } // END for n

Open in new window


At this point, _divWrapper (Overlay's internally instantiated <div>) is still NOT in the DOM because it doesn't get appended until the show() method is called. So by adding frmSearchCust into this <div>, it effectively removed btnCancel out of the DOM at this point. Then here I was in the next line trying to assign a click handler to a button that DOES NOT EXIST at that moment in time; hence, no click handler was ever created. By the time the Overlay's show() method makes the form appear, and btnCancel is now visible and in the DOM, it doesn't have a click handler and hence, no response from it.

The reason why it works if btnCancel was assigned a click handler BEFORE Overlay's instantiation is because I had already attached the click handler BEFORE btnCancel was removed from the DOM, so it persisted. Later on, when frmSearchCust was made to appear by Overlay's show() method, btnCancel works just fine, as expected.

The reason why your event delegation to the <body> tag works is because <body> now has a click handler lying in waiting. When frmSearchCust is visible (and so is btnCancel), it's click event bubbles up to <body>. But if frmSearchCust is not visible (and neither is btnCancel), <body> is still there waiting anyway.

I'm happy we arrived at a logical explanation because I was about to conclude this as an anomaly to JavaScript; I'm glad it's my flaw instead. Always a pleasure working with you Rob, thanks again.
Couldn't have explained it better :)  Thanks for the points.