pure javascript onclick becomes undefined after assigning functions scope ends - javascript

It's been a while since I wrote Javascript without jQuery, so please bear with me. I'm assuming I'm just doing something silly. I have this function that converts link urls to an internal representation that I use with a router I wrote.
Templater.prototype.replace_links = function() {
this.links = document.getElementsByTagName("a");
for (i = 0; i < this.links.length; i++) {
if (!(this.links[i].getAttribute("href") === this.VOID && this.links[i].getAttribute(this.HREF))) {
this.links[i].setAttribute(this.HREF, this.links[i].getAttribute("href"));
this.links[i].setAttribute("href", this.VOID);
this.links[i].onClick = function(self, link) {
return function() { self.router.go(link.getAttribute(self.HREF)); };
}(this, this.links[i]);
}
}
}
This function is called the first time when Templater is initialized. The first time it works correctly. However, I run it a second time after I append some html into the body of the document. I run it again just in case that appended html has links in it too:
<body>
<!-- arbitrary new html is loaded in here -->
Login <!-- becomes Login correctly -->
Home <!-- becomes Home correctly -->
</body>
When I console.log(this.links[0], this.links[0].onClick) after the function has been run but still within a Templater function, I get the correct html and then undefined for the onClick event:
Discover undefined
When I log the same to values within the replace_links scope, I get what I'm expecting. I.e. the function is shown:
Discover function () { self.router.go(link.getAttribute(self.HREF)); }
I was playing around with it some more and tried this way and got the same kind of thing.
Templater.prototype.replace_links = function() {
this.links = document.getElementsByTagName("a");
for (i = 0; i < this.links.length; i++) {
if (!(this.links[i].getAttribute("href") === this.VOID && this.links[i].getAttribute(this.HREF))) {
(function(self, link) {
link.setAttribute(self.HREF, link.getAttribute("href"));
link.setAttribute("href", self.VOID);
link.onClick = function() { self.router.go(link.getAttribute(self.HREF)); };
})(this, this.links[i]);
}
}
}
I console.log after the replace_link scope ends like before and this time I still get:
Discover undefined
I'd really appreciate any help and/or suggestions! Please let me know if I'm missing anything helpful.

The key points here have been treated as minor details.
I append some html into the body of the document
and
this.links[i].onClick = function(self, link) {
My point is, if you alter innerHTML, which I assume is the way you "append some html into the body of the document," the browser will serialize the DOM objects into HTML, do the string concatenation, and then parse it again. This results in new objects which no longer have the expandos, such as onClick. onClick is a custom property; you probably meant onclick anyway.
However, some of your changes will be serialized and parsed successfully, namely the setAttribute operations. Thus, when you run replace_links after the HTML appending, the
if (!(this.links[i].getAttribute("href") === this.VOID && this.links[i].getAttribute(this.HREF)))
check will treat the link as already replaced and not assign the onClick again.
Here's a fiddle that shows this in action. http://jsfiddle.net/k9d7b2ds/

UPDATE: Made some additional changes. The onclick event's default this object is always referencing the window object. You need to pass over the closure.
Check sample code here:
http://jsfiddle.net/y0443fz6/
Templater.prototype.replace_links = function() {
var that = this;
this.links = document.getElementsByTagName("a");
for (i = 0; i < this.links.length; i++) {
if (!(this.links[i].getAttribute("href") === this.VOID && this.links[i].getAttribute(this.HREF))) {
this.links[i].setAttribute(this.HREF, this.links[i].getAttribute("href"));
this.links[i].setAttribute("href", this.VOID);
this.links[i].onclick = function(self, link) {
return function() {
self.router.go(link.getAttribute(self.HREF));
};
}(that, this.links[i]);
}
console.log(this.links[i], this.links[i].onclick);
}
}
hope that helps. gl

Related

DOM does not update when element changes (VUE)

for a vue/mvc project i am making a page divided into html sections.
If the user clicks on a button a javascript function is called that changes the display properties of the sections so that only the clicked section is shown.
When the dom is created, it calls the function and correctly shows one section.
However when the button is clicked, the function is called again, but the dom does not change.
Here is the code for the created function:
created: function () {
var self = this;
var sectionElements = document.getElementsByTagName("section");
for (var i = 0; i < sectionElements.length; i++) {
self.sections.push({ isSelected: false, object: sectionElements[i] });
}
for (var i = 0; i < self.sections.length; i++) {
self.sections[i].isSelected = false;
}
this.showSelectedSection(0);
},
Here is the code of the javascript function.
showSelectedSection(index) {
for (var i = 0; i < this.sections.length; i++) {
if (i == index) {
this.sections[i].isSelected = true;
this.sections[i].object.style.display = "block";
}
else {
this.sections[i].isSelected = false;
this.sections[i].object.style.display = "none";
}
}
Does anyone know why this is happening and how i can fix it?
Any tips or help is greatly appreciated.
First of all, I don't totally get why you're using self = this in this example, seems like it's not necessary. Nevertheless that is not your problem. You're modifying an object inside an array, and you're doing this by accessing the index. Normally that would be ok, but vue is not aware of this change. Try either passing the direct reference to the object inside the array or add a deep watch to your array so vue can hear this changes and make the proper modifications to your DOM.

Using "this" in a constructor

I am attempting to make a project which does two way data binding on two specified variables. However, when I tried it out, the project did not seem to be working.
I'm pretty sure that what I did wrong is that I specified a constructor, and then inside, I created a variable using "this," a function using "this," and then I tried to use the first variable inside the function using "this." Is this allowed?
The code for my project is in the snippet below.
function glue(varOne, varTwo, interval = 60) {
this.varOne = varOne;
this.varTwo = varTwo;
this.varOneClone = this.varOne;
this.varTwoClone = this.varTwo;
this.interval = interval;
this.onChange = function(changedVar) {
if (changedVar == this.varOne) {
this.varTwo = this.varOne;
} else if (changedVar == this.varTwo) {
this.varOne = this.varTwo;
}
this.varOneClone = this.varOne;
this.varTwoClone = this.varTwo;
};
this.intervalID = setInterval(function() {
if (this.varOne != this.varTwo) {
if (this.varOne != this.varOneClone) {
this.onChange(this.varOne);
} else if (this.varTwo != this.varTwoClone) {
this.onChange(this.varTwo);
}
}
}, this.interval);
this.clearUpdate = function() {
clearInterval(intervalID);
};
this.changeUpdate = function(newInterval) {
this.interval = newInterval;
clearInterval(intervalID);
this.intervalID = setInterval(function() {
if (this.varOne != this.varTwo) {
if (this.varOne != this.varOneClone) {
this.onChange(this.varOne);
} else if (this.varTwo != this.varTwoClone) {
this.onChange(this.varTwo);
}
}
}, this.interval);
};
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Glue</title>
</head>
<body>
<input id="input" type="text"></input>
<p id="output"></p>
<script>
var input = document.getElementById("input");
var output = document.getElementById("ouput");
var glue = new glue(input, output, 60);
</script>
</body>
</html>
Thank you!
Edit:
I've tried using the var self = this; method which two people recommended, but it still refuses to work. The error from the console is TypeError: glue is not a constructor, but I'm not sure why this happens. I want glue to be a constructor. Please help! The new code is below.
function glue(varOne, varTwo, interval = 60) {
var self = this;
self.varOne = varOne;
self.varTwo = varTwo;
self.varOneClone = self.varOne;
self.varTwoClone = self.varTwo;
self.interval = interval;
self.onChange = function(changedVar) {
if (changedVar == self.varOne) {
self.varTwo = self.varOne;
} else if (changedVar == self.varTwo) {
self.varOne = self.varTwo;
}
self.varOneClone = self.varOne;
self.varTwoClone = self.varTwo;
};
self.intervalID = setInterval(function() {
if (self.varOne != self.varTwo) {
if (self.varOne != self.varOneClone) {
self.onChange(self.varOne);
} else if (self.varTwo != self.varTwoClone) {
self.onChange(self.varTwo);
}
}
}, self.interval);
self.clearUpdate = function() {
clearInterval(intervalID);
};
self.changeUpdate = function(newInterval) {
self.interval = newInterval;
clearInterval(intervalID);
self.intervalID = setInterval(function() {
if (self.varOne != self.varTwo) {
if (self.varOne != self.varOneClone) {
self.onChange(self.varOne);
} else if (self.varTwo != self.varTwoClone) {
self.onChange(self.varTwo);
}
}
}, self.interval);
};
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Glue</title>
</head>
<body>
<input id="input" type="text"></input>
<p id="output"></p>
<script>
var input = document.getElementById("input");
var output = document.getElementById("ouput");
var glue = new glue(input, output, 60);
</script>
</body>
</html>
Thanks for your help!
There are few problems that you could fix, as you said, you're using this everywhere. this might not always point to the same object in some context so if you want to use the same object, you could use a variable accessible from the scope.
Usually, people use a variable named self:
var self = this
Then the self variable should be used everywhere where you want to access specifically that object. So in your onChange and setInterval, it would be a better idea to access this by using the self object.
One other way, would be to instead bind a bounded function. In this case, the this object will be the one you bound the function to.
this.onChange = (function () {
// here this will be equal to context
}).bind(context)
In this case, you can set context to this and within the function you can still use this without worrying which object will be this.
You can also make sure to call method using this.func.call(this, params...) or this.func.apply(this, paramArray).
There are a lot of ways to fix this, but if you search about bind, apply and call. It will be enough to understand how to make more complex construction for your code.
To give you an idea of how bind works, take this example:
function bind(method, obj) {
// Create a callable
return function () {
// Pass the arguments to the method but use obj as this instead
return method.apply(obj, arguments);
}
}
function func(a, b, c) {
console.log(this, a, b, c)
}
var bound1 = bind(func, {a: 1})
var bound2 = bind(func, {a: 2})
func(1,2,3)
bound1(3,4,5)
bound2(6,7,8)
You'll see that when func isn't called, this defaults to window. But in case of bound1 and bound2, it will be using the second parameter of bind.
All that to say that you can control which object will be used as this but if you don't specify which object is going to be used, then you might end up using window as this like in a setInterval. In other words, the keyword this isn't scoped like other variables. It depends of the context, not of its place in the code.
The problem is that the setInterval callback is made in a different context, so this will no longer be your glue object. Since you're running within a browser, it will be the window object. I'm pretty sure that all the other times you use this, it is referencing the correct object.
There are a few options to handle this. The first is to use the .bind() method. It is definitely the cleanest, and requires the least amount of "tweaking". However, it's not supported by IE8. Hopefully you don't have to support that browser, considering MS has dropped support for it, except for in embedded systems. Here's how it would work:
this.intervalID = setInterval(function(self) {
if (this.varOne != this.varTwo) {
if (this.varOne != this.varOneClone) {
this.onChange(this.varOne);
} else if (this.varTwo != this.varTwoClone) {
this.onChange(this.varTwo);
}
}
}.bind(this), this.interval);
Another option is to create a variable that holds the value of this before you call setInterval and then rely on the closure to give you access to that variable:
function glue(varOne, varTwo, interval = 60) {
var self = this;
//...
this.intervalID = setInterval(function() {
if (self.varOne != self.varTwo) {
if (self.varOne != self.varOneClone) {
self.onChange(self.varOne);
} else if (self.varTwo != self.varTwoClone) {
self.onChange(self.varTwo);
}
}
}, this.interval);
//...
}
Finally, you could also use an immediately-invoked function to pass in the value of this without creating a closure:
this.intervalID = setInterval((function(self) {
return function() {
if (self.varOne != self.varTwo) {
if (self.varOne != self.varOneClone) {
self.onChange(self.varOne);
} else if (self.varTwo != self.varTwoClone) {
self.onChange(self.varTwo);
}
}
}
})(this), this.interval);
EDIT:
Everything said above is a problem and the options given for solving it should fix that problem. However, it's one of only a few problems in your code sample.
There's also a problem with the line var glue = new glue(input, output, 60);. You can't have a variable with the same name as a function. If the function is declared first, as it should be in this case, then the variable overwrites it, so the function essentially no longer exists by the time you call new glue(). This is why you are getting the glue is not a constructor error.
I see that in your jsbin you've changed the variable name to tape. That fixes the problem. However, jsbin puts all the code from the JavaScript pane at the bottom of your body. You need the function to be declared before the var tape = new glue(input, output, 60); line, since that line is calling the function. You can tell jsbin where to put the code from the JS pane, by putting %code% where you want it in in the HTML pane. So, if you put a line like <script>%code%</script> before your existing script block, it should fix that. This is totally just a quirk of using jsbin, and won't apply to code you have running in a standalone website (although, even on your standalone site, you need to make sure that the code declaring the glue function comes before the code calling it).
Now, that gets rid of all the errors that are being thrown, but still, the code isn't actually DOING anything. This is because near the beginning of the constructor you have:
this.varOneClone = this.varOne;
this.varTwoClone = this.varTwo;
So varOne starts out equal to varOneClone, and varTwo starts out equal to varTwoClone. The only other place you set them is inside the onChange method, but you only call onChange if varOne != varOneClone or varTwo != varTwoClone. It's like you're saying "Make these 2 values the same, then if they're different, call onChange." Obviously, in that case, onChange is never going to be called.
I realize that it's possible you have more code than you've included here, that IS changing those properties, but I think your goal is to check if the text WITHIN varOne or varTwo has changed, and if so, update the other one, rather than checking if the elements themselves have changed. Since the text can be changed by the user (at least for varOne, since that's an input), it can be changed outside of code. If assumption is correct, you need something like this:
function glue(varOne, varTwo, interval = 60) {
this.varOne = varOne;
this.varTwo = varTwo;
this.varOneCurrentText = this.varOne.value;
this.varTwoCurrentText = this.varTwo.textContent;
this.interval = interval;
this.onChange = function(changedVar) {
if (changedVar == this.varOne) {
this.varTwo.textContent = this.varOneCurrentText = this.varTwoCurrentText = this.varOne.value;
} else if (changedVar == this.varTwo) {
this.varOne.value = this.varOneCurrentText = this.varTwoCurrentText = this.varTwo.textContent;
}
};
this.intervalID = setInterval(function() {
if (this.varOne.value != this.varTwo.textContent) {
if (this.varOne.value != this.varOneCurrentText) {
this.onChange(this.varOne);
} else if (this.varTwo.textContent != this.varTwoCurrentText) {
this.onChange(this.varTwo);
}
}
}.bind(this), this.interval);
//...
}
A couple things to note about this. First, notice that I'm using .value for varOne but .textContent for varTwo. This is because you're passing in a form element (an input) for varOne and a non-form element (a paragraph) for varTwo. These types of elements have different ways of getting their current text. If you can design it so that only form elements will ever be passed in, it will make things easier. But if not, since you probably won't know in advance what type of elements are passed in, you'd need to add a check at the beginning, so you can use the correct property.
Also, while this should work, it would really be better to use events rather than having a continuous loop in the setInterval looking to see if the text has changed. The input has a change event that will be fired anytime its value is changed. You could just update varTwo whenever that event is fired. I don't think there's a similar event for the paragraph element, but you could create a custom event for it. I'm assuming that you're planning on having some other code that will update the text inside the paragraph, since that's not something the user can do directly. If so, then you could fire your custom event at the same time that you update its text. Then when that event is fired, you could update varOne.
I just noticed a typo in a line you have, as well:
var output = document.getElementById("ouput");
The element ID should of course be "output".

JavaScript error with eventListener

I'm getting an error which I'm not quite sure what to make of. Anyway, before I go on to that, I found out about Unobtrusive JavaScript, at first I was just going to add an "OnClick" to my HTML but then found out that isn't a very good thing to do.
Anyway, so I did that and turned up with this code which isn't quite finished yet, but I wanted to try it out anyway before I went in and made any other changes.
window.onload = function findSubmitButton(){
var button = document.getElementsByClass("send_info").addEventListener("click", retrieveInputText());
}
function retrieveInputText(){
var inputArray = document.querySelectorAll("#container_id input[type=text]");
var finalArray;
for (var i in inputArray){
if(inputArray[i].type == "text"){
finalArray.push(i);
}
alert("done");
}
}
The error chrome's console gives me is this: Uncaught TypeError: undefined is not a functionfindInputs.js:5 findSubmitButton
There was also something I wanted to know, I want to be able to use this script with any other sort of input form, so instead of directly identifying the button for this page, I used a class identifier, this way, it works with any page. The only way there would be any issues would be if I had two buttons of the sort, as it is right now, any page with that sort of information only has one button for such procedures. I would appreciate if someone helped me out with this, I'm new to JavaScript.
getElementsByClassName(), returns an array, not an element so it does not have the addEventListener method, also you need to pass a function reference
window.onload = function findSubmitButton() {
var button = document.getElementsByClassName("send_info")[0].addEventListener("click", retrieveInputText);
}
Also you need to initialize the array var finalArray = [];
window.onload = function findSubmitButton() {
var button = document.querySelector(".send_info").addEventListener("click", retrieveInputText);
}
function retrieveInputText() {
var inputArray = document.querySelectorAll("#container_id input[type=text]");
var finalArray = [];
for (var i in inputArray) {
if (inputArray[i].type == "text") {
finalArray.push(i);
}
}
alert("done:" + finalArray);
}
Demo: Fiddle

Storing variables used in greasemonkey script from website

On a certain homepage I visit I want to hide all links that I click. My idea was to use a Greasemonkey script like this:
var blocklist = JSON.parse(GM_getValue("blocklist"));
var as = document.getElementsByTagName('a');
var alength = as.length;
for(var i=0; i<alength; i++) {
var a = as[i];
if(blocklist.indexOf(a.href) >= 0) {
a.style.display='none';
} else {
a.setAttribute('onclick', 'alert("HELP"); return true;');
}
}
Inside the script I can call this, no problem:
blocklist = blocklist.concat('http://someurl');
GM_setValue("blocklist", JSON.stringify(blocklist));
But in the website itself (read where it says alert("HELP");) I cannot call this function because neither the function nor the blocklist do exist.
Is there a way to access the function from the website? (probably not?) Where else could I store the values to get them back on the next load of the website? The firefox browser is set to sanitize on shutdown, so can't use a:visited or similar.
Don't try to call GM_ functions from a webpage. (1) It's not directly possible, (2) it's a security risk, (3) it's almost never really necessary.
Never use onclick in a Greasemonkey script (or at all, really). A simple alert("HELP"); return true; might work, but anything more will crash and it's bad form anyway.
Also, if you use querySelectorAll versus getElementsByTagName, you can fine-tune what links you process, EG: document.querySelectorAll ("div.main a.user") -- which would get only those links with the CSS class user that were inside the <div> with the class main.
In this case, use addEventListener (or use jQuery) to handle the links so your script code would become like:
var blocklist = JSON.parse (GM_getValue ("blocklist") );
var targlinks = document.querySelectorAll ('a');
for (var J = targlinks.length - 1; J >= 0; --J) {
var targlink = targlinks[J];
if (blocklist.indexOf (targlink.href) >= 0) {
targlink.style.display = 'none';
} else {
targlink.addEventListener ('click', virginLinkHandler, false);
}
}
function virginLinkHandler (zEvent) {
var newURL = zEvent.target.href;
blocklist = blocklist.concat (newURL);
GM_setValue ("blocklist", JSON.stringify (blocklist) );
}
You should use localStorage so that you can retain your list on subsequent page loads. It's really not too different from GM_setValue.
localStorage.setItem("blocklist", JSON.stringify(blocklist));
var blocklist = JSON.parse(localStorage.getItem("blocklist"));

JavaScript - Loop over all a tags, add an onclick to each one

I've got a list of links that point to images, and a js function that takes a URL (of an image) and puts that image on the page when the function is called.
I was originally adding an inline onlick="showPic(this.getAttribute('href'))" to each a, but I want to separate out the inline js. Here's my func for adding an onclick to each a tag when the page loads:
function prepareLinks(){
var links = document.getElementsByTagName('a');
for(var i=0; i<links.length; i++){
var thisLink = links[i];
var source = thisLink.getAttribute('href');
if(thisLink.getAttribute('class') == 'imgLink'){
thisLink.onclick = function(){
showPic(source);
return false;
}
}
}
}
function showPic(source){
var placeholder = document.getElementById('placeholder');
placeholder.setAttribute('src',source);
}
window.onload = prepareLinks();
...but every time showPic is called, the source var is the href of the last image. How can I make each link have the correct onclick?
JavaScript doesn't have block scope, so the closed variable ends up being whatever was last assigned to it. You can fix this by wrapping it in another closure:
function prepareLinks() {
var links = document.getElementsByTagName('a');
for(var i = 0; i < links.length; i++) {
var thisLink = links[i];
var source = thisLink.getAttribute('href');
if(thisLink.getAttribute('class') == 'imgLink') {
thisLink.onclick = (function(source) {
return function() {
showPic(source);
return false;
};
})(source);
}
}
}
Of course, you can make this one simpler and use this:
function prepareLinks() {
var links = document.getElementsByTagName('a');
for(var i = 0; i < links.length; i++) {
var thisLink = links[i];
if(thisLink.getAttribute('class') == 'imgLink') {
thisLink.onclick = function() {
showPic(this.href);
return false;
};
}
}
}
I believe this either breaks compatibility with IE5 or IE6, but hopefully you don't care about either of those =)
Minitech's answer should fix your problem, which is that the source variable is shared by all your onclick handlers
The way you're doing it is very wasteful, there's no need to set a separate handler for each link. Also, it won't work if any links are added dynamically. Event delegation is the way to go.
function interceptLinks() {
// Bad way to set onclick (use a library)
document.onclick = function() {
if (this.tagName.toUpperCase() != 'A' ) {
return;
}
// Bad way to check if it contains a class (use a library)
if (this.getAttribute('class') == 'imgLink') {
showPic(this.getAttribute('href'));
return false;
}
}
}
This is the age-old problem of event handlers inside of a loop that access an outer variable.
Your source variable is pulled off the scope chain at the time of the click event, and by then, it's been set to the last href attribute due to the iteration being finished.
You need to break the closure by doing one of two things.
The easiest but not supported by many browsers is to use let which lets you use block scope.
let source = thisLink.getAttribute('href');
jsFiddle. It worked in Firefox, but not Chrome.
In 2038, when we're dealing with the year 2038 problem and all browsers have implemented ES6, this will be the standard way to fix this problem.
A more difficult to understand and implement method that is compatible with all browsers is to break the closure with a pattern such as...
thisLink.onclick = (function(src) {
return function(){
showPic(src);
return false;
}
})(source);
jsFiddle.
Thanks for all the replies. Turns out I had diagnosed the problem incorrectly, sorry. Actually using a new var and annon. function to add an onclick on each loop iteration works (the passed href is correct). It was not working because I was getting at the a-tags by the "imgLink" class which I had removed from the HTML when I removed the inline onclick handlers (I get them with an ID on a parent now). Also I needed to use "return !showPic(this.href);" to stop the link being followed normally when clicked.
Working code:
function showPic(source){
var placeholder = document.getElementById('placeholder');
placeholder.setAttribute('src',source);
return true;
}
function prepareLinks() {
if(!document.getElementById('imgLinks')){ return false; };
var galLinks = document.getElementById('imgLinks');
var links = galLinks.getElementsByTagName('a');
for(var i=0; i<links.length; i++) {
var thisLink = links[i];
thisLink.onclick = function() {
return !showPic(this.href);
};
}
}
window.onload = function(){
prepareLinks();
}

Categories

Resources