JS -> Accessing a variable from another function - javascript

I'm new to this site and to programming in general and I'm a bit stuck on a school assignment.
For reference here is my code I'm currently working on: Pastebin
function initChests(){
let gameChests = document.getElementById('chests');
for(let i = 0; i < 3; i++) {
let singleChest = document.createElement('img');
singleChest.src = 'images/chest-closed.png';
singleChest.alt = 'A chest'
singleChest.style.marginRight = '20px';
gameChests.appendChild(singleChest);
}
}
function initChestEventListeners() {
singleChest.addEventListener('click', chestClicked);
}
function chestClicked(e){
console.log("hello");
}
Now to my problem. What I want to do is access the variable singleChest in my initChestEventListeners function without making the variable global.
I am aware I can just put the singleChest addEventListener inside the initChests function, but that's not quite what I want to do either. I want to make it work with the function structure I have if possible.
Is this something that is possible and if so, can someone please explain what I would need to do or redirect me to a guide that could help explain the solution?
Thank you in advance!
Kind regards.

One option is to add an event listener to the parent element instead, the gameChests, and on click, if the clicked element is an img, do something with it:
function initChests() {
const gameChests = document.getElementById('chests');
for (let i = 0; i < 3; i++) {
let singleChest = document.createElement('img');
singleChest.src = 'images/chest-closed.png';
singleChest.alt = 'A chest'
singleChest.style.marginRight = '20px';
gameChests.appendChild(singleChest);
}
}
function initChestEventListeners() {
const gameChests = document.getElementById('chests');
gameChests.addEventListener('click', chestContainerClicked);
}
function chestContainerClicked(e) {
if (!e.target.matches('img')) {
return;
}
console.log("hello", e.target);
}
initChests();
initChestEventListeners();
<div id="chests"></div>
This method of watching for events that bubble up to the parent element is called event delegation.

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.

Warning not to make function within a loop

I've written a code to create modal windows for div container. Once the button is clicked, I get the button's number and display a related modal window. Tested, works on all browsers.
myModalContent = new tingle.modal();
var myBtn = document.querySelectorAll("button.project__btn");
for (var i = 0; i < myBtn.length; i++) {
myBtn[i].addEventListener("click", function () {
myModalContent.open();
if (this.hasAttribute("data-btn")) {
myModalContent.setContent(document.querySelector(".project" + this.getAttribute("data-btn") + "-modal").innerHTML);
} else {
myModalContent.setContent(document.querySelector(".project1-modal").innerHTML);
}
});
}
A js validator gives one warning "Don't make functions within a loop."
Read some posts related to this topic, especially that the function must be created outside of the loop, I created a function:
function handler(modalDiv, trigBtn, index){
modalDiv.open();
if (trigBtn[index].hasAttribute("data-btn")) {
modalDiv.setContent(document.querySelector(".project" + trigBtn[index].getAttribute("data-btn") + "-modal").innerHTML);
} else {
modalDiv.setContent(document.querySelector(".project1-modal").innerHTML);
}
}
Then called it from within a loop:
for (var i = 0; i < myBtn.length; i++) {
myBtn[i].onclick = handler(myModalContent, myBtn, i);
}
It doesn't seem to work properly, it displays a last modal window right after the web page loads. My understanding that the function must be connected with the click event listener, ie when a button is clicked, the modal window should pop up. Now, the modal window pops up without any click event. Could you give me an idea how to properly write a function? Or if I should just simply ignore this js validation warning or not.
Keep it simple! You do not have to change anything about your code but to move the function expression to a named function declaration outside of the loop body:
var myModalContent = new tingle.modal();
var myBtn = document.querySelectorAll("button.project__btn");
function myHandler() {
myModalContent.open();
if (this.hasAttribute("data-btn")) {
myModalContent.setContent(document.querySelector(".project" + this.getAttribute("data-btn") + "-modal").innerHTML);
} else {
myModalContent.setContent(document.querySelector(".project1-modal").innerHTML);
}
}
for (var i = 0; i < myBtn.length; i++) {
myBtn[i].addEventListener("click", myHandler);
}
The warning is trying to prevent a problem with "modified closures". If your function did anything with the variable i, then you'd find that the value of the variable i at the time when users click the button is always myBtn.length because that's the value it ends up with at the end of the loop.
This:
for (var i = 0; i < myBtn.length; i++) {
...
Is treated like this:
var i;
for (i = 0; i < myBtn.length; i++) {
...
Since you don't use i anywhere in your function, you're technically safe, but there's a possibility that other developers in the future could change the code and end up running into this problem.
In order to fix this code in the way it looks like you're trying to fix it, you'd need to have the handler function return a function itself.
myBtn[i].addEventListener("click", createHandler());
function createHandler() {
return function() {
myModalContent.open();
if (this.hasAttribute("data-btn")) {
myModalContent.setContent(document.querySelector(".project" + this.getAttribute("data-btn") + "-modal").innerHTML);
} else {
myModalContent.setContent(document.querySelector(".project1-modal").innerHTML);
}
};
}
This has the same effect as your working code, but prevents someone from trying to use i inside of the closure. If someone needs i there, they can add it to the createHandler's argument list, where it's not reusing the same variable for each pass through the loop.
Alternatively, if you can use modern versions of javascript, you can use the let keyword instead of var.
This:
for (let i = 0; i < myBtn.length; i++) {
...
Is treated more like how this code would work in a language like C#:
for (var _ = 0; _ < myBtn.length; _++) {
var i = _;
...
In other words, the scope of the i variable is internal to the for loop, rather than global to the function you're in.

Duplicating elements with Javascript and naming w/ counter

I think I've searched long enough to warrant asking this, and I hope I'm not missing something obvious, but I'm at my wits' end with this. I'm a complete JavaScript noob, and I'm having difficulty getting a script I found online to work correctly.
The project I was assigned was to make it so this form could be extended by clicking a button, and I thought I'd be able to accomplish it with HTML alone, but that doesn't seem possible. I found this script, and was able to get the duplication part of it to work:
http://www.quirksmode.org/dom/domform.html
However, the part of the script that's supposed to append a counter to the names of the fields isn't working, and therefore when the form is submitted, everything is recorded under the first form's name value. My guess is that that part of the script is trying to get the name of the wrong node, but I really don't know. Here's a shortened version of what I have. Ugly, but hopefully it gets the point across...
http://pastebin.com/nQhnXXKx
Let me know if I can clarify, and any help would be greatly, greatly appreciated!
Reorganizing the code, you could use something like this:
(function () {
"use strict";
var counter, init, addWorkshop, renameInputs, removeWorkshop;
counter = 0;
init = function () {
document.getElementById("moreWorkshops").onclick = addWorkshop;
addWorkshop();
};
addWorkshop = function () {
var clonedWorkshop, targetInsert;
counter++;
clonedWorkshop = document.getElementById("readroot").cloneNode(true);
clonedWorkshop.id = "";
clonedWorkshop.className = "";
clonedWorkshop.querySelector(".remover").onclick = removeWorkshop;
renameInputs(clonedWorkshop);
targetInsert = document.getElementById("writeroot");
targetInsert.parentNode.insertBefore(clonedWorkshop, targetInsert);
};
renameInputs = function (container) {
var children, i, j, cur, theName;
children = container.children;
for (i = 0, j = children.length; i < j; i++) {
cur = children[i];
if (cur.nodeName.toLowerCase() === "input") {
theName = cur.name;
if (theName) {
cur.name = theName + counter;
}
} else {
renameInputs(cur);
}
}
};
removeWorkshop = function () {
this.parentNode.parentNode.removeChild(this.parentNode);
};
window.onload = init;
}());
DEMO: http://jsfiddle.net/gAaxS/
Note that this is very structure-specific - for example, the this.parentNode.parentNode means that it has exactly two ancestors that you want to target. If you changed the HTML, you'd have to change the JS (which is usual).

Understanding Scope of 'this' in jQuery on change event

I wrote a quick custom extension for jQuery for a project I am working on. I am having a hard time understanding the scope of 'this' in a custom onChange method I would like implement. If left out the middle of my code where I am calling the webservice but if you checkout the last two methods, you will see where my problem is. I want to call the updateDetails method with the selected value changes. However, when that method is called within the onchange event, I obviously lose the scope of "this" as this.materialListResponse comes back as undefined in this context. Any help on helping me understand this would be greatly appreciated.
$.fn.appendMaterials = function (options) {
this.options = options;
//Set Default Options
this.defaults = {
typeID: '66E1320D-51F9-4900-BE84-6D5B571F9B80'
};
this.options = $.extend({}, this.defaults, options);
//
Code here to call web service and generate response XML
//
this.materialListResponse = $.xml2json(
$.parseXML($(this.materialListWebservice()).find("GetMaterialTreeResponse").text())).Materials.Material;
this.appendOptionString = function () {
var i = 0;
this.optionString = '<option>'
for (i = 0; i < this.materialListResponse.length; i++) {
this.optionString += '<option>' + this.materialListResponse[i].MaterialCode + '</option>';
};
this.append(this.optionString);
return this;
};
this.appendOptionString();
this.updateDetails = function () {
for (i = 0; i < this.materialListResponse.length; i++) {
if (this.materialListResponse[i].MaterialCode === this.val()) {
$('#table1 #MaterialDescription').val(this.materialListResponse[i].Description);
}
}
}
this.change(this.updateDetails)
};
pass the object this as data to the event:
this.change({that: this}, this.updateDetails)
and then you can access that in the scope of the event callback
this.updateDetails = function(event) {
var that = event.data.that;
...
}
RESOURCES
http://api.jquery.com/event.data/
The event handler will be called later, when you have exited the extension. It's called in the scope of the element, so this will be the element that has changed.
Copy the reference to the list to a local variable, and use that in the event handler:
var list = this.materialListResponse;
this.updateDetails = function() {
for (i = 0; i < list.length; i++) {
if (list[i].MaterialCode === this.val()) {
$('#table1 #MaterialDescription').val(list[i].Description);
}
}
}
By using the local variable in the function, the variable will be part of the closure for the function, so it will survive the scope of the extension method where it is declared.
When a method is called in JavaScript as a callback it behaves as a function. In this case "this" refers to the owner of this function, usually the Window object in a Web browser.

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