What's the scope of parameters of a function set by onclick? - javascript

I've a function which accept only html string, it will show a info window (a popup) with the html as its content:
function createInfoWindow(info_html){
// show a popup with info_html as its content
}
Now I want to create a info window which will have a button:
function createMyInfoWindow(o){
var info_html = "<input type='button' value='click me' onclick='foo(o)'>"
createInfoWindow(info_html);
}
function foo(o){
console.log(o);
}
createMyInfoWindow({ name: "test", age: 21);
However, when I click the button, it says that o can't be found.

Try following code
var info_html = "<input type='button' value='click me' onclick='foo(\""+o+"\")'>"
UPDATE
If o is object it becomes more complicated.
You can store passed objects in store-object. Then you can pass corresponding index in foo:
var storage = [];
function createMyInfoWindow(o){
var index = storage.length;
storage[index] = o;
var info_html = "<input type='button' value='click me' onclick='foo(\""+index+"\")'>"
createInfoWindow(info_html);
}
function foo(i){
console.log(storage[i]);
}
createMyInfoWindow({ name: "test", age: 21);

In the HTML assigned to the innerHTML of the input, handlers are wrapped in functions so the scope is the handler, then global (there may be other objects on the scope chain).
In your code, name is local to createMyInfoWindow, the handler (nor any other function) has access to that variable. See Molecule's answer for how to use it.

The way you are doing it now is a form of eval, and is generally frowned upon. Read up on Unobtrusive Javascript if you'd like to know all the reasons why.
However, there is a really excellent way to accomplish the same task without the scope problems your facing (let alone trying to get that object passed to the function in string form -- yikes!) Doing this properly will require some restructuring of your functions, but I'm sure you'll find it to be worth it.
function createMyInfoWindow(o){
// creating window first so we can access it from the DOM
createInfoWindow(info_html);
// we can select the window from the DOM now, but it would be even better if
// createInfoWindow returned that object so we could just pick up where we left off
var myInfoWindow = document.getElementById("myInfoWindow");
// The button you are putting into the window
var myButton = document.createElement("input");
myButton.type = "button";
myButton.value = "click me";
// because of javascript closures, we can call foo(o) from within an anonymous function
myButton.onclick = function () { foo(o) };
}
I prefer creating HTML elements this way for many reasons: 1) Avoid the implicit use of eval, 2) much easier to debug the HTML when it is generated by the javascript for you and 3) no more scope issues for event functions.
You just have to create the window in the reverse order now, because the window element must exist first in order to add a button to it, and the button element must exist before you can add the onclick handler to it.

Related

javascript/HTML: Bind event handler function with object as argument in HTML string literal

With an electron app I'm working on, I failed to pass an object into the onclick event handler for an image. The event handler is specified in HTML, but the HTML is a string literal in JS.
Goal
The idea is to pass an object from class My into the event handler of the image control the object code tries to generate, see code below
class My {
constructor() {
this.id = '123';
}
GenerateCtrl() {
let html = `
<div class="myclass" id="my-${this.id}">
<img class="img-btn" id="do-${this.id}" src="images/btn-img.png" role="button" onclick="Do(this, '${this}')">
</div>
`;
return html;
}
}
The above HTML will be a section of my page.
Here is the event handler
function Do(img, myobj) {
alert(JSON.stringify(myobj));
}
The caller code when constructing the page:
const sect = document.querySelector(.myclass);
var myObj = new My();
sect.innerHTML = myObj.GenerateCtrl();
I can see the image control on the page without issues.
Expectation
On clicking the image, I expect to see the alert showing the object content.
Observation
but all I can see in the alert is "[object Object]".
Remarks
This tells me that the object doesn't come through that handler function as an argument but as a converted string.
How should I fix this?
The proper way to do this would be to insert the HTML as an element, not an HTML string, so that a listener can be attached to the inner <img> beforehand. Attaching an event listener to an element whose parent you already have a reference to is extremely cheap, there's basically no performance penalty. This is how the code could look, using createElement to create the outer element, then insert it with appendChild:
function Do(my) {
console.log(my);
}
class My {
constructor() {
this.id = '123';
}
GenerateCtrl() {
const div = document.createElement('div');
Object.assign(div, { id: `my-${this.id}`, className: 'img-btn' });
div.innerHTML = `<img class="img-btn" id="do-${this.id}" src="images/btn-img.png" role="button">`;
div.children[0].addEventListener('click', () => Do(this));
return div;
}
}
var myObj = new My();
document.querySelector('.myclass').appendChild(myObj.GenerateCtrl());
<div class="myclass"></div>
Inline handlers have too many problems:
They have escaping issues (they need to be represented in a Javascript string, with string delimiters inside properly escaped for Javascript, and properly escaped for HTML markup)
They require global pollution (your current implementation requires a global window.Do function)
They run inside two unintuitive with statements - one for the document, one for the current element
They cannot reference a variable or object inside the closure that creates the inline hander
Best to avoid them.

Handling DOM events in JavaScript classes

(Feel free to reword the title; I find this hard to put into words.)
I've created a JavaScript "class" (for lack of a better word; I know JS isn't class-based) that represents a textarea and a div.
Every time the textarea's value is changed, I want the div's content to update accordingly, so I thought to assign an onkeyup event handler to the textarea — however, that turned out to be more problematic than I thought.
Here's the relevant part of my HTML:
<div id="container"></div>
<script src="MyTextarea.js"></script>
<script>
var ta = new MyTextarea('container');
</script>
And here's the JS I've written so far:
function MyTextarea(id) {
this.textarea = document.createElement('textarea');
this.box = document.createElement('div');
var container = document.getElementById(id);
container.appendChild(this.textarea);
container.appendChild(this.box);
this.textarea.onkeyup = this._synchronize;
}
MyTextarea.prototype._synchronize = function () {
this.box.innerHTML = this.textarea.value;
};
Instead of working, this insists on throwing a "this.textarea" is undefined" error. It turns out that — much to my surprise — in the _synchronize function, this doesn't refer to the MyTextarea object, but instead to the textarea element itself. I'm puzzled as to why that would be.
What am I doing wring and/or not getting here? Is it because I'm doing this within a "class"? How can I achieve this instead?
You are losing context when you assign event handler as direct function reference. So in other words, instead of MyTextarea instance object this points to HTMLTextAreaElement object.
There are multiple solutions, for example you can bind context explicitly:
this.textarea.onkeyup = this._synchronize.bind(this);
Or you could do it old-school way:
var self = this;
this.textarea.onkeyup = function() {
self._synchronize();
};

Class variables are not visible inside element event?

I am hoping to create a web application, this means that the HTML elements will have to be pretty dynamic as they will be being created and moved around being handled by different other elements a lot.
I therefore decided to use classes - with a lot of success, until the point of handling events..This is the class in suspicion that Chrome tells me confuses it with a variable not being defined.
function SpanInputPair(element) {
span = document.createElement("span");
this.Span = getNextInputID();
span.style.display = "none";
span.id = this.Span;
element.appendChild(span);
input = document.createElement("input");
input.type = "text";
this.Input = getNextInputID(); // request a new ID for this element and save into this class
input.id = this.Input; // Attach it onto the element for future finding :/ (slow)
element.appendChild(input);
input.focus();
span.onclick = function() {
var input = getInput(this.Input); // this.Input cannot be seen?
var span = getInput(this.Span);
input.value = span.innerHTML;
toggleDisplay(input);
toggleDisplay(span);
input.focus();
}
input.onblur...
The line "var input = getInput(this.Input);" is the issue as Chrome is telling me it doesn't see "this.Input" anymore. I know this because I can run this line and replace "this.Input" with the real value and it returns the element fine.
As you can see I have it creating a text box and span dynamically and setting their IDs for future use, this is because I previously tried to save the element itself in the class and use that inside the event - same issue, so I tried to find it in the document each event instead by using document.getElementByID().
Weirdly also it does not seem to focus the text box it just created (if that may be part of the issue too I don't know).
This project was created entirely from nothing and uses no libraries.
So my question is, can you please explain why the element's event cannot see any variables in the class that created it? And how could I fix this?
I have not yet seen this posted as many people are using a library such as JQuery and/or not using classes, is question is specific to event handling with help from internal variables of a class.
Although it may not make too much sense, it is normal in JavaScript because the value of this has changed within the local function you have declared. Therefore, you must understand properly how to declare and use functions in JavaScript:
Functions in JavaScript has something called context, which just means that every function is bounded to a specific object. If we declare a function in your script, it will be bounded to the global object (which is window in browsers).
However, if we try to declare a method (a function that belongs to an object),
this will be a magic variable that represents the object itself. Everytime you invoke this.my_property, you would get the value of the property of your object, either an "attribute" or a "method".
Therefore, the function's context can be changed, as it happens with event handlers. In your scenario, event handlers are functions whose context has changed, so everytime you access to this, its value will be the HTMLElement object that receives the event when you click on it.
To solve this problem, you can declare an auxiliary variable and use it inside of the handler. The common practice followed by the community is:
var that = this; // Also, the people use 'self' instead of 'that'
span.onclick = function() {
var input = that.input;
// ...
};
Using this workaround, you will not have any problem.
The value of 'this' inside the click function is the element clicked. Here that would be the <span> you just created.
You are wanting the value of the this back when you defined the click function. (The question might also be: What is the value of 'this' when the SpanInputPair function is called? You may want to consider that.)
You can do that by something like this which adds it to the closure:
var thisInput = this.Input;
...
span.onclick = function() {
var input = getInput(thisInput);
You are going to have the same problem with this.Span on the next line.

How to assign event callbacks iterating an array in javascript (jQuery)

I'm generating an unordered list through javascript (using jQuery). Each listitem must receive its own event listener for the 'click'-event. However, I'm having trouble getting the right callback attached to the right item. A (stripped) code sample might clear things up a bit:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
Actually, more is going on in this iteration, but I didn't think it was very relevant to the question. In any case, what's happening is that the callback function seems to be referenced rather than stored (& copied). End result? When a user clicks any of the list items, it will always execute the action for the last class_id in the classes array, as it uses the function stored in callback at that specific point.
I found dirty workarounds (such as parsing the href attribute in an enclosed a element), but I was wondering whether there is a way to achieve my goals in a 'clean' way. If my approach is horrifying, please say so, as long as you tell me why :-) Thanks!
This is a classic "you need a closure" problem. Here's how it usually plays out.
Iterate over some values
Define/assign a function in that iteration that uses iterated variables
You learn that every function uses only values from the last iteration.
WTF?
Again, when you see this pattern, it should immediately make you think "closure"
Extending your example, here's how you'd put in a closure
for ( class_id in classes )
{
callback = function( cid )
{
return function()
{
$(this).selectClass( cid );
}
}( class_id );
li_item = jQuery('<li></li>').click(callback);
}
However, in this specific instance of jQuery, you shouldn't need a closure - but I have to ask about the nature of your variable classes - is that an object? Because you iterate over with a for-in loop, which suggest object. And for me it begs the question, why aren't you storing this in an array? Because if you were, your code could just be this.
jQuery('<li></li>').click(function()
{
$(this).addClass( classes.join( ' ' ) );
});
Your code:
for(class_id in classes) {
callback = function() { this.selectClass(class_id) };
li_item = jQuery('<li></li>')
.click(callback);
}
This is mostly ok, just one problem. The variable callback is global; so every time you loop, you are overwriting it. Put the var keyword in front of it to scope it locally and you should be fine.
EDIT for comments: It might not be global as you say, but it's outside the scope of the for-loop. So the variable is the same reference each time round the loop. Putting var in the loop scopes it to the loop, making a new reference each time.
This is a better cleaner way of doing what you want.
Add the class_id info onto the element using .data().
Then use .live() to add a click handler to all the new elements, this avoids having x * click functions.
for(class_id in classes) {
li_item = jQuery('<li></li>').data('class_id', class_id).addClass('someClass');
}
//setup click handler on new li's
$('li.someClass').live('click', myFunction )
function myFunction(){
//get class_id
var classId = $(this).data('class_id');
//do something
}
My javascript fu is pretty weak but as I understand it closures reference local variables on the stack (and that stack frame is passed around with the function, again, very sketchy). Your example indeed doesn't work because each function keeps a reference to the same variable. Try instead creating a different function that creates the closure i.e.:
function createClosure(class_id) {
callback = function() { this.selectClass(class_id) };
return callback;
}
and then:
for(class_id in classes) {
callback = createClosure(class_id);
li_item = jQuery('<li></li>').click(callback);
}
It's a bit of a kludge of course, there's probably better ways.
why can't you generate them all and then call something like
$(".li_class").click(function(){ this.whatever() };
EDIT:
If you need to add more classes, just create a string in your loop with all the class names and use that as your selector.
$(".li_class1, .li_class2, etc").click(function(){ this.whatever() };
Or you can attach the class_id to the .data() of those list items.
$("<li />").data("class_id", class_id).click(function(){
alert("This item has class_id "+$(this).data("class_id"));
});
Be careful, though: You're creating the callback function anew for every $("<li />") call. I'm not sure about JavaScript implementation details, but this might be memory expensive.
Instead, you could do
function listItemCallback(){
alert("This item has class_id "+$(this).data("class_id"));
}
$("<li />").data("class_id", class_id).click(listItemCallback);

JavaScript mechanism for holding onto a value from a user action

I've created a JavaScript object to hold onto a value set by a user checking a checbox in a ColorBox.
I am relatively new to jQuery and programming JavaScript "the right way" and wanted to be sure that the below mechanism for capturing the users check action was a best practice for JavaScript in general. Further, since I am employing jQuery is there a simpler method to hold onto their action that I should be utilizing?
function Check() {
this.Checked = false;
}
obj = new Check;
$(document).ready(function() {
$('.cboxelement').colorbox({ html: '<input id="inactivate" type="checkbox" name="inactivatemachine"> <label for="inactivate">Inactivate Machine</label>' });
$(document).bind('cbox_cleanup', function() {
obj.Checked = $.fn.colorbox.getContent().children('#inactivate').is(':checked');
});
$(document).bind('cbox_closed', function() {
if ($($.fn.colorbox.element()).attr('id').match('Remove') && obj.Checked) {
var row = $($.fn.colorbox.element()).parents('tr');
row.fadeOut(1000, function() {
row.remove();
});
}
});
});
Personally, I would attach the value(s) to an object directly using jQuery's built-in data() method. I'm not really entirely sure what you are trying to do but, you can, for instance, attach values to a "namespace" in the DOM for use later one.
$('body').data('colorbox.checked',true);
Then you would retrieve the value later by:
var isChecked = $('body').data('colorbox.checked');
You run the data() method on any jquery object. I would say this is best-practice as far as jQuery goes.
You could capture the reference in a closure, which avoids global data and makes it easier to have multiple Checks. However, in this case it appears to be binding to the single colorbox, so I don't know that you could usefully have multiple instances.
function Check() {
this.Checked = false;
var obj = this; // 'this' doesn't get preserved in closures
$(document).ready(function() {
... as before
)};
}
var check = new Check; // Still need to store a reference somewhere.
$($.fn.colorbox.element()) is redundant. $.fn.colorbox.element() is already a jquery element.
It's common use (in the examples i watched, at least) to prepend a $ to variables referencing jquery elements.
So, var $rows = $.fn.colorbox.element().parents('tr'); gives instantly the idea that it is referencing jquery element(s).
I am afraid fadeOut won't work on rows in IE6 (if i recall correctly). You should be able to hide all the content inside the <tr> before removing it.
Can't help on the "simplify" thing because i don't know the colorbox's best uses.

Categories