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.
elepilAsked:
Who is Participating?
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

RobOwner (Aidellio)Commented:
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.
0
elepilAuthor Commented:
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?
0
RobOwner (Aidellio)Commented:
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
0
Ultimate Tool Kit for Technology Solution Provider

Broken down into practical pointers and step-by-step instructions, the IT Service Excellence Tool Kit delivers expert advice for technology solution providers. Get your free copy now.

elepilAuthor Commented:
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.
0
RobOwner (Aidellio)Commented:
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/
0
elepilAuthor Commented:
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.
0
RobOwner (Aidellio)Commented:
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?
0
elepilAuthor Commented:
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.
0
RobOwner (Aidellio)Commented:
change your event to this for your button:

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

Open in new window

0
RobOwner (Aidellio)Commented:
What that does is make it irrelevant if the overlay removes and adds back in the button (which is what the append function does when you pass it a jQuery object).
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
RobOwner (Aidellio)Commented:
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.
0
elepilAuthor Commented:
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.
0
RobOwner (Aidellio)Commented:
Couldn't have explained it better :)  Thanks for the points.
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
JavaScript

From novice to tech pro — start learning today.

Question has a verified solution.

Are you are experiencing a similar issue? Get a personalized answer when you ask a related question.

Have a better answer? Share it in a comment.