Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a guarantee that onchange will run earlier than an event added to the tag?

Tags:

javascript

I am generating a tag from server-side:

'<select class="form-data api command" data-command-name="GetDifficultyData" data-events="change" name="id" data-data="" onchange="this.attributes[\'data-data\']=\'data=\' + this.value">'.$difficultyOptions.'</select>'

I am using PHP to do so, but it is not very relevant for this question.

But I have a function which will add an event to my select tag because it is marked to handle a command:

function initializeCommands(root) {
    if (!root) root = document;
    if (!radWindow) {
        radWindow = document.getElementById("radwindow");
        var closeRadWindow = function(event) {
            radWindow.addClass("hide-popup");
        };
        radWindow.addEventListener("click", closeRadWindow);
        var popupActionContainer = radWindow.querySelector(".popup-action-container");
        popupActionContainer.addEventListener("click", function(event) {
            if (!event) {
                event = window.event;
            }
            event.cancelBubble = true;
            if (event.stopPropagation) {
                event.stopPropagation();
            }
            return false;            
        });
        popupActionContainer.addEventListener("keyup", function(e) {
            if ((e.code == 'Enter') && (e.target.tagName.toLowerCase() !== "textarea")) {
                var mainButton = this.querySelector("span.api[data-form-name]");
                mainButton ? mainButton.click() : closeRadWindow();
            }
        });
        document.getElementsByTagName("body")[0].addEventListener("keyup", function(e) {
            if (e.code == 'Escape') {
                closeRadWindow();
            }
        });
        popupActionContainer.querySelector(".icon-close").addEventListener("click", closeRadWindow);
    }
    if (!popupContent) {
        popupContent = radWindow.querySelector("#popup-content");
    }
    var commandButtons = root.querySelectorAll(".api:not(.initialized)");
    for (var commandIndex = 0; commandIndex < commandButtons.length; commandIndex++) {
        var eventsToHandle = commandButtons[commandIndex].attributes["data-events"].value.split(" ");
        for (var eventIndex = 0; eventIndex < eventsToHandle.length; eventIndex++) {
            commandButtons[commandIndex].addEventListener(eventsToHandle[eventIndex], function(event) {
                if (this.hasClass("confirm")) {
                    if (!confirm(this.hasAttribute("data-confirm-message") ? this.attributes["data-confirm-message"].value : "Biztos ebben?")) {
                        return;
                    }
                }
                if (this.hasClass("template")) {
                    var data = this.attributes["data-data"];
                    sendRequest("POST", "/template/" + this.attributes["data-template-name"].value, templateCallback, true, (data ? data.value : data));
                } else if (this.hasClass("form")) {
                    var container = document.querySelector(this.attributes["data-container"].value);
                    var items = container.querySelectorAll(".form-data");
                    var data = [];
                    var props = [];
                    for (var index = 0; index < items.length; index++) {
                        var v = items[index].value;
                        if (items[index].tagName.toLowerCase() === "input") {
                            if (items[index].type === "checkbox") {
                                v = items[index].checked;
                            }
                        }
                        data.push(items[index].name + "=" + v);
                        props.push(items[index].name);
                    }
                    var recaptchaResponse = container.querySelectorAll("[name=g-recaptcha-response]");
                    if (recaptchaResponse.length > 0) {
                        data.push("g-recaptcha-response=" + recaptchaResponse[0].value);
                        props.push("g-recaptcha-response");
                    }
                    var errorPlaces = container.querySelectorAll('.error-text:not(.invisible)');
                    for (var errorIndex = 0; errorIndex < errorPlaces.length; errorIndex++) {
                        errorPlaces[errorIndex].addClass("invisible");
                    }
                    var validationResults = validate(items);
                    if (validationResults.length) {
                        for (var errorIndex = 0; errorIndex < validationResults.length; errorIndex++) {
                            var errorPlace = container.querySelector('.error-text[data-key="' + validationResults[errorIndex].key + '"]');
                            if (errorPlace) {
                                errorPlace.removeClass("invisible").innerText = validationResults[errorIndex].value;
                            }
                        }
                    } else {
                        sendRequest("POST", "/form/" + this.attributes["data-form-name"].value, function() {
                            if (this.readyState === 4) {
                                var r = JSON.parse(this.responseText);
                                for (var key in r) {
                                    if (r[key] && (props.indexOf(key) >= 0)) {
                                        var errorPlace = container.querySelector('.error-text[data-key="' + key + '"]');
                                        if (errorPlace) {
                                            errorPlace.removeClass("invisible").innerText = r[key];
                                        }
                                    }
                                }
                                eval(r.response);
                            }
                        }, true, data.join("&"));
                    }
                } else if (this.hasClass("command")) {
                    var data = this.attributes["data-data"];
                    sendRequest("POST", "/command/" + this.attributes["data-command-name"].value, function() {
                        if (this.readyState === 4) {
                            var r = JSON.parse(this.responseText);
                            if (r.callback) {
                                eval(r.callback);
                            }
                        }
                    }, true, ((data && data.value) ? data.value : data));
                }
            });
        }
        commandButtons[commandIndex].addClass("initialized");
    }
}

This function runs on page load and also in some cases when popup window content is generated through the API (my current case is with a popup window). To sum the situation up:

  • page loads
  • the user clicks on a button
  • a request is sent to the server
  • the server responds with popup content, including the tag I have shown at the start of this question and the onchange is defined on server-side and therefore will be valid from the start of the life cycle of the tag
  • a popup callback is being executed and the tag will have an event handler for change

The importance of the order of execution is that the onchange should be executed before the event later added to the tag, since the event will send the id of the select tag to the server (so it will ask data regarding the entity to be edited) and it is important to send the current id. My current tests work well, onchange is being executed before the change event which was later added to the tag and everything is fine. However, I am worried about some browsers or future versions where this might not happen in the same way. In that case I would need to do some further work to guarantee that the onchange is executed first and only then the change event. I could do that work anyway, but I would like to keep the code elegant and to avoid writing unnecessary code. So, the question is: is it guaranteed in my situation that onchange will be executed first, then the change event which was later defined than the onchange event?

EDIT

We can control the order of the events with this prototype:

function CustomEventHandler() {
    let events = new Map();
    this.add = (tags, eventKeys, eventFunctions) => {
        let isNew = false;
        for (let tag of tags) {
            if (!events.has(tag)) events.set(tag, new Map());
            let outerItem = events.get(tag);
            for (let eventKey of eventKeys) {
                if (isNew = !outerItem.has(eventKey)) outerItem.set(eventKey, []);
                let innerItem = outerItem.get(eventKey);
                for (let eventFunction of eventFunctions) innerItem.push(eventFunction);
                if (isNew) {
                    tag.addEventListener(eventKey, () => {this.execute(tag, eventKey)});
                }
            }
        }
        return this;
    };

    this.execute = (tag, eventKey) => {
        if (events.has(tag)) {
            let outerItem = events.get(tag);
            if (outerItem.has(eventKey)) {
                let innerItem = outerItem.get(eventKey);
                for (let func of innerItem) func(tag, eventKey);
            }
        }
    };

    this.get = () => {return events};
}

For example, if we visit https://stackoverflow.com/

and run the prototype code in console and use this prototype as:

var ceh = new CustomEventHandler();

var items = document.querySelectorAll(".question-summary.narrow");

ceh.add(items, ["mouseenter"], [function(tag, eventKey) {
    tag.style["background-color"] = "green";
}, function(tag, eventKey) {console.log(eventKey)}
]).add(items, ["mouseleave"], [function(tag, eventKey) {
    tag.style["background-color"] = "white";
}]);

then we can see the events being correctly executed in the correct order. We could add custom functionalities, like adding an identifier to a function and be able to remove a function by identifier, or swap their order, but for the sake of simplicity I omitted the implementation of these features. The global scope is used for the sake of simplicity in our case.

like image 728
Lajos Arpad Avatar asked Jan 28 '23 03:01

Lajos Arpad


1 Answers

DOM Level 3 (came with HTML5) now orders events in the order they are attached.

Here is the reference to the spec:

https://www.w3.org/TR/DOM-Level-3-Events/Overview.html#changes-DOMEvents2to3Changes-flow

11.1.1. Changes to DOM Level 2 event flow

This new specification introduced the following new concepts in the event flow:

  • Event listeners are now ordered. In DOM Level 2, the event ordering was unspecified.
like image 168
Randy Casburn Avatar answered Jan 30 '23 16:01

Randy Casburn