Best way to attach an arbitrary callback function to a dom element? - javascript

The following (vanillajs) code works fine
// library code:
let close_cb; // nasty global var...
...
let tree = document.createElement('ul');
tree.addEventListener('click', function(event) {
...
// let close_cb = tree.getAttribute('CLOSE_CB');
// let close_cb = tree.onchange;
close_cb(leaf.id, ', so there');
// user code:
function my_close_cb(id, msg) {
const footer = document.querySelector('footer');
footer.innerHTML = id + ' is closed' + msg;
}
// tree.setAttribute('CLOSE_CB',my_close_cb);
// tree.onchange = my_close_cb;
close_cb = my_close_cb;
However the commented-out s/getAttribute code fails, getAttribute puts full text of "function my_close_cb(..." in local close_cb.
The commented-out onchange hack actually works, but feels terribly dodgy to say the least, although it is certainly closer to what I'm after.
Note the "library code" is hand written and fully under my control, whereas "user code" is intended to be transpiled or otherwise machine-generated, so changing my_close_cb to accept a single event argument would be a complete non-starter.
What is the best way to attach an arbitrary callback function that accepts an arbitrary set of parameters to a dom element?

You can attach a plain json property to the DOM element.
document.body.callback = function cb(text) { console.log(text); };
document.body.callback("hello world");

Using tree.close_cb or any other property to store a function or anything else is totally fine, as the DOM is just a (persistent) tree of JavaScript objects. As such they behave like any other JS object, and properties can be added without any restrictions.

Related

Replacing page elements dynamically through a greasemonkey script (even post page load)

I have the following javascript function:
function actionFunction() {
let lookup_table = {
'imageurl1': "imageurl2",
};
for (let image of document.getElementsByTagName("img")) {
for (let query in lookup_table) {
if (image.src == query) {
image.src = lookup_table[query];
}
}
}
}
I want it to work even after a page is fully loaded (in other words, work with dynamically generated html elements that appeared post-load by the page's js).
It could either be by running the function every x seconds or when a certain element xpath is detected within the page, or every time a certain image url is loaded within the browser (which is my main goal here).
What can I do to achieve this using javascript + greasemonkey?
Thank you.
Have you tried running your code in the browser's terminal to see if it works without greasemonkey involved?
As to your question - you could either use setInterval to run given code every x amount of time or you could use the MutationObserver to monitor changes to the webpage's dom. In my opinion setInterval is good enough for the job, you can try learning how the MutationObserver works in the future.
So rewriting your code:
// arrow function - doesn't lose this value and execution context
// setInterval executes in a different context than the enclosing scope which makes functions lose this reference
// which results in being unable to access the document object
// and also window as they all descend from global scope which is lost
// you also wouldn't be able to use console object
// fortunately we have arrow functions
// because arrow functions establish this based on the scope the arrow function is defined within
// which in most cases is global scope
// so we have access to all objects and their methods
const doImageLookup = () => {
const lookup_table = {
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png': 'https://www.google.com/logos/doodles/2022/gama-pehlwans-144th-birthday-6753651837109412-2x.png',
};
const imgElementsCollection = document.getElementsByTagName('img');
[...imgElementsCollection].forEach((imgElement) => {
Object.entries(lookup_table).forEach(([key, value]) => {
const query = key; // key and value describe object's properties
const replacement = value; // here object is used in an unusual way, i would advise to use array of {query, replacement} objects instead
if (imgElement.src === query) {
imgElement.src = replacement;
}
});
});
};
const FIVE_MINUTES = 300000; // ms
setInterval(doImageLookup, FIVE_MINUTES);
You could make more complex version by tracking the img count and only doing the imageLookop if their number increases. This would be a big optimization and would allow you to run the query more frequently (though 5 minutes is pretty long interval, adjust as required).

Adding Click Event Listeners During Iteration?

I'm trying to add a click event listener to a bunch of newly created divs in a for loop. The issue I'm having is that only the last div keeps its event listener. I've read up about closures and read several other posts and questions and their answers, and as far as I can tell I have it set up correctly. But it still isn't working for me. Only the final div to be iterated is receiving the event listener.
function edit_entry(k) {
wrd_input.value = k;
def_input.value = lexicon[k][1];
wrd_input.onkeyup();
delete lexicon[k];
rewrite_entries();
}
function rewrite_entries(keys = null) {
if (keys === null) { keys = []; }
let sorted_keys = sort_lex_keys();
lex_body.style.color = 'rgb(200, 200, 200)';
lex_body.innerHTML = '';
sorted_keys.forEach((key) => {
if (!keys.length || keys.includes(key)) {
lex_body.innerHTML +=
`<div class='lex-entry' id=${key}><i>${key}</i>\n<p class='pronunciation'>${lexicon[key][0]}</p>${lexicon[key][1]}</div>\n`;
let entry = document.getElementById(key)
entry.addEventListener('click', edit_entry.bind(this, key) );
}
});
}
Current state of the relevant code above. If somebody knows the issue, it'd be very helpful.
If it's relevant, this code is running via Electron (17.0.0).
An attempted solution, using an anonymous arrow function instead of a bind:
entry.addEventListener('click', () => edit_entry(key) );
yields the same result.
Update:
Changing the .innerHTML attribute of something apparently strips all event listeners. So the solution was simply to create the element completely in js, add it to the container, and then add the listener. This change to the forEach loop solves the problem:
sorted_keys.forEach((key) => {
if (!keys.length || keys.includes(key)) {
let entry = document.createElement('div');
entry.className = 'lex-entry';
let word = document.createElement('p');
word.appendChild( document.createTextNode(key) );
word.style.fontStyle = 'italic';
let pron = document.createElement('p');
pron.className = 'pronunciation';
pron.appendChild( document.createTextNode(lexicon[key][0]) );
let defn = document.createTextNode(lexicon[key][1]);
entry.append(word, pron, defn);
entry.addEventListener('click', () => edit_entry(key) );
lex_body.appendChild(entry);
So a few things crosses my mind:
Firstly, edit_entry.bind(this, key);: Are you certain this is pointing to what you want? It should be pointing to the window object. Also, is there any reason to bind context for that particular function?
Secondly innerHTML: Generally it's discouraged to use innerHTML directly. I don't know how/when the browser layouts changes to innerHTML, the div might not be on the page when you call getElementById. As an alternative, you could try document.createElement("div"), set its properties accordingly and finally append it to lex_body.
Edit
From mdn:
Please note that using innerHTML to append html elements (e.g. el.innerHTML += "link") will result in the removal of any previously set event listeners. That is, after you append any HTML element that way you won't be able to listen to the previously set event listeners.
I think that binding the function every time with this is overriding the binding every time, that should be the reason that you are only getting the last one's event.
Inside of the forEach lambda, the this keyword refers to the internal class that is calling the forEach not your rewrite_entries function. Try not binding the call, like so:
entry.addEventListener('click', () => edit_entry(key) );

Dynamically loaded onclick function using in function data

Ok, my coding is a little weak, but I'm learning. I have a code simular to
$.post("test.php", {"data":"data"}, function(grabbed){
$.each(grab, function(i, item){
var place = document.getElementById("div");
var para = createElement("p");
para.innerHTML = item.name;
para.onclick = function(){
document.getElementById(?para?).innerHTML = ?item.price?;
}
place.appendChild(para);
});
}, "json");
My question is how would I use say, item.price in the onclick function. I haven't actually tried yet, but I'm pretty sure that referencing the data retrieved from the request won't work in the dynamically loaded onclick function.
My function works a little differently but the basic principle is on here. Any advise would be appreciated, or perhaps someone can point me in the right direction.
I was thinking of assigning ids to the div or para and using those in the function somehow. I also thought of using cookies to store the data I want, but I can't figure it out. Is there something else I need to know or try.
Your question concerns the concept of closures in JavaScript.
Closures are functions that refer to independent (free) variables. In
other words, the function defined in the closure 'remembers' the
environment in which it was created.
Consider the following code snippet:
var foo = (function(){
var remembered = "yea!";
return function(){
console.log("Do you remember? - " + remembered);
};
})();
foo(); // prints: Do you remember? - yea!
That said, looking at your code:
...
// the function assigned to `onclick` should remember
// the variable `item` in the current context.
para.onclick = function(){
document.getElementById(?para?).innerHTML = ?item.price?;
}
...
That works since you're using JQuery's $.each() for iteration. And here is a demonstration: http://jsfiddle.net/55gjy3fj/
However if you were doing the iteration with a plain for-loop, you would have to wrap the handler inside another closure:
var getHandler = function(para, item){
return function(){
document.getElementById(the_id_here).innerHTML = item.price;
};
};
para.onclick = getHandler(para, item);
...
This fiddle demonstrates the point: http://jsfiddle.net/63gh1gof/
Here's a fiddle that summarises both cases:
Since you're already using jQuery, this could be the perfect opportunity to get introduced to .data(). Use it to save an arbitrary object to the dom element, and then reference it later in the onclick-listener.
var para = $('<p/>').html(item.name).data('item', item);
para.click(function() {
$(this).data('item') // item!!!
});
$('#div').append(para);
Actually, here you could write a more efficient method using jQuery's delegated event handlers.
Just attach the event handler to the parent element:
$('#div').on('click', 'p', function() {
$(this).data('item') // item!!!
});

Determine if JavaScript syntax is valid in change handler of ACE

I'm using the ACE editor for interactive JavaScript editing. When I set the editor to JavaScript mode, ACE automatically determines if the code is valid or not, with an error message and line number highlighted when it's not.
During the change event handler, I want to detect if ACE thinks the code is valid or not before I attempt to eval() it. The only way I thought that I might do it is:
var jsMode = require("ace/mode/javascript").Mode;
var editor = ace.edit('mycode'), edEl = document.querySelector('#mycode');
editor.getSession().setMode(new jsMode);
editor.getSession().on('change',function(){
// bail out if ACE thinks there's an error
if (edEl.querySelector('div.ace_gutter-cell.ace_error')) return;
try{
eval(editor.getSession().getValue());
}catch(e){}
});
However:
Leaning on the presence of an element in the UI with a particular class seems awfully fragile, but more importantly,
The visual update for parsing occurs after the change callback occurs.
Thus, I actually have to wait more than 500ms (the delay before the JavaScript worker kicks in):
editor.getSession().on('change',function(){
setTimeout(function(){
// bail out if ACE thinks there's an error
if (edEl.querySelector('div.ace_gutter-cell.ace_error')) return;
try{
eval(editor.getSession().getValue());
}catch(e){}
},550); // Must be longer than timeout delay in javascript_worker.js
});
Is there a better way, something in an undocumented API for the JS mode, to ask whether there are any errors or not?
The current session fires onChangeAnnotation event when annotations change.
after that the new set of annotations can be retrieved as follows
var annotations = editor.getSession().getAnnotations();
seems to do the trick. It returns a JSON object which has the row as key and an array as value. The value array may have more than one object, depending on whether there are more than one annotation for each row.
the structure is as follows (copied from firebug –for a test script that I wrote)
// annotations would look like
({
82:[
{/*annotation*/
row:82,
column:22,
text:"Use the array literal notation [].",
type:"warning",
lint:{/*raw output from jslint*/}
}
],
rownumber : [ {anotation1}, {annotation2} ],
...
});
so..
editor.getSession().on("changeAnnotation", function(){
var annot = editor.getSession().getAnnotations();
for (var key in annot){
if (annot.hasOwnProperty(key))
console.log("[" + annot[key][0].row + " , " + annot[key][0].column + "] - \t" + annot[key][0].text);
}
});
// thanks http://stackoverflow.com/a/684692/1405348 for annot.hasOwnProperty(key) :)
should give you a list of all annotations in the current Ace edit session, when the annotations change!
Hope this helps!
I found a solution that is probably faster than traversing the DOM. The editor's session has a getAnnotations method you can use. Each annotation has a type that shows whether they are an error or not.
Here is how I set my callback for the on 'change'
function callback() {
var annotation_lists = window.aceEditor.getSession().getAnnotations();
var has_error = false;
// Unfortunately, you get back a list of lists. However, the first list is
// always length one (but not always index 0)
go_through:
for (var l in annotation_lists) {
for (var a in annotation_lists[l]) {
var annotation = annotation_lists[l][a];
console.log(annotation.type);
if (annotation.type === "error") {
has_error = true;
break go_through;
}
}
}
if (!has_error) {
try {
eval(yourCodeFromTextBox);
prevCode = yourCodeFromTextBox;
}
catch (error) {
eval(prevCode);
}
}
}
As far as I know, there are two other types for annotations: "warning" and "info", just in case you'd like to check for those as well.
I kept track of the pervious code that worked in a global (well, outside the scope of the callback function) because often there would be errors in the code but not in the list of annotations. In that case, when eval'ing the errored code, it would be code and eval the older code instead.
Although it seems like two evals would be slower, it seems to me like the performance is no that bad, thus far.
Ace uses JsHint internally (in a worker) and as you can see in the file there is an event emitted:
this.sender.emit("jslint", lint.errors);
You can subscribe to this event, or call the JSHint code yourself (it's pretty short) when needed.
I found you can subscribe worker events in Ace 1.1.7:
For javascript code, subscribe 'jslint' event:
session.setMode('ace/mode/javascript}');
session.on('changeMode', function() {
if (session.$worker) {
session.$worker.on('jslint', function(lint) {
var messages = lint.data, types;
if (!messages.length) return ok();
types = messages.map(function(item) {
return item.type;
});
types.indexOf('error') !== -1 ? ko() : ok();
});
}
});
For JSON code, subscribe 'error' and 'ok' event:
session.setMode('ace/mode/json');
session.on('changeMode', function() {
// session.$worker is available when 'changeMode' event triggered
// You could subscribe worker events here, whatever changes to the
// content will trigger 'error' or 'ok' events.
session.$worker.on('error', ko);
session.$worker.on('ok', ok);
});

event when adding element into document

Adding new html string into the page:
document.getElementById('container').innerHTML = '<div id="child"></div>';
Is there an event that let me know when child element is in the document?
I have a function, which return some html codeas a string. And when this html will be added in the document, I need to execute javascript function. I've tried to use inline onload event
document.getElementById('container').innerHTML = '<div id="child" onload="console.log(\'ready!\');"></div>';
but it does not seem to work.
UPDATE:
Probably, I should provide more details about the situation. I have a library function
myLibrary.getHtml()
In old version, users just call this function and append the result into the document:
document.getElementById('container').innerHTML = myLibrary.getHtml();
In new version, the result is not a plain html. Now users can interact with it after they append it in the document. So after they append html into the document, I need to go through the result DOM and attach event handlers, hide some elements and other things. That is why, I need to know when they add it in the document and execute a javascript function, which turn plain html into fancy interactive widget.
You could try using DOM Mutation Events, but they are still inconsistently implemented across browsers.
If i have not misunderstood the question, you can probably get this to work
document.getElementById('id').childNodes;
If you can use jQuery, you could write your own custom event which would call the function you need to call whenever the new html has been added:
$('#container').bind('changeHtml', function(e) {
// Execute the function you need
});
And of course instead of just adding the html to the element, you would need to wrap it up in a function which triggers your custom event. Then all you'd need to do is call this function instead of setting the innerHtml yourself.
function addHtml(html) {
$('#container').innerHTML = html;
$('#container').trigger('changeHtml');
}
Solved by myself this way:
myLibrary.getHtml = function() {
var html;
...
var funcName = 'initWidget' + ((Math.random() * 100000) | 0);
var self = this;
window[funcName] = function() { self._initWidget(); };
return html + '<img src="fake-proto://img" onerror="' + funcName + '()"';
}
The idea behind the code is that I specify incorrect url for the src attribute of the img tag and the image triggers error event and call my function. Quite simple :)

Categories