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:
onchange
is defined on server-side and therefore will be valid from the start of the life cycle of the tagchange
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.
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.
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