Javascript Beginner: can't get function to execute on multiple objects - javascript

I'm working on Cole Steele's Web Developer Bootcamp on Udemy #264. Event Bubbling. I'm trying to build a function which will allow one or more objects to be passed in and to execute the same action (toggle the classList 'hide' so that the 'Click Here to Hide' button goes away and the 'Click Here to Show' button appears) on each of them.
I am able to get this working by calling the function separately, such as
const container = document.querySelector('#container')
const show = document.querySelector('#show')
function hideOneElement(ele){
ele.classList.toggle('hide');
}
show.addEventListener('click', function () {
hideOneElement(container);
hideOneElement(show);
})
However, when I try to call the function with both container (the div that says 'Click here to hide') and show at the same time, I can't get it to work. I tried writing the hide function as a for...of, such as
function hideElements(elements){
for (const ele of elements) {
ele.classList.toggle('hide')
}
}
let stuffToHide = [container,show]
hideElements(stuffToHide)
However this does not seem to work. I also tried passing in as two separate arguments but that also doesn't seem to work:
function hideElements(ele1, ele2) {
ele1.classList.toggle('hide');
ele2.classList.toggle('hide')
}
hideElements(container, show);
At this point, I'm not sure where to go, and my Google-jitsu is not finding anything useful. This isn't really part of the course exercise, but seems like I'm fundamentally missing something about calling functions.
Full code and HTML below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.hide {
display: none;
}
</style>
</head>
<body>
<section onclick="alert('sectopm clicked')">
<p onclick="alert('paragraph clicked')">I am a paragraph
<button onclick="alert('button clicked')">Click</button>
</p>
</section>
<div id="container">
Click to Hide
<button id="colorbtn">Change Color</button>
</div>
<div id="show">
Click here to Show
</div>
<script src="app.js"></script>
</body>
</html>
const makeRandColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return (`rgb(${r},${g},${b})`);
}
const button = document.querySelector('#colorbtn')
const container = document.querySelector('#container')
const show = document.querySelector('#show')
function hideElements(ele1, ele2) {
// for (const ele of elements) {
// ele.classList.toggle('hide')
// }
// elements.classList.toggle('hide')
ele1.classList.toggle('hide');
ele2.classList.toggle('hide')
}
function hideOneElement(ele){
ele.classList.toggle('hide');
}
//hideElements(show); //run function once to toggle on the 'hide' class so that this is not shown by default.
hideElements(show)
button.addEventListener('click', function (e) {
container.style.backgroundColor = makeRandColor();
e.stopPropagation();
})
container.addEventListener('click', function () {
hideElements(container, show);
//hideElements(container, show); //hide the 'click here to hide' stuff
})
show.addEventListener('click', function () {
hideElements(container);
hideElements(show);
})
show.addEventListener('click', function () {
hideOneElement(container);
hideOneElement(show);
})

"I'm trying to build a function which will allow one or more objects to be passed in" so you need to be able to call hideElements as either hideElements(ele1) or hideElements(ele1, ele2)?
If that's the case you'll need to pass an array vs individual variables. You were on the right track with the commented out code here:
// for (const ele of elements) {
// ele.classList.toggle('hide')
// }
// elements.classList.toggle('hide')
But you weren't trying to pass them as an array and you can't loop through an individual element (there's only one). If you wrap your variables in an array using [] that should fix your issues.
Example:
function hideElements(elements) {
for (const ele of elements) {
ele.classList.toggle('hide')
}
}
hideElements([show]); //run function once to toggle on the 'hide' class so that this is not shown by default.
container.addEventListener('click', function() {
hideElements([container, show]);
})
show.addEventListener('click', function() {
hideElements([container, show]);
})
Working Example:
const makeRandColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return (`rgb(${r},${g},${b})`);
}
const button = document.querySelector('#colorbtn')
const container = document.querySelector('#container')
const show = document.querySelector('#show')
function hideElements(elements) {
for (const ele of elements) {
ele.classList.toggle('hide')
}
}
function hideOneElement(ele) {
ele.classList.toggle('hide');
}
hideElements([show]); //run function once to toggle on the 'hide' class so that this is not shown by default.
button.addEventListener('click', function(e) {
container.style.backgroundColor = makeRandColor();
e.stopPropagation();
})
container.addEventListener('click', function() {
hideElements([container, show]);
})
show.addEventListener('click', function() {
hideElements([container, show]);
})
.hide {
display: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<section onclick="alert('sectopm clicked')">
<p onclick="alert('paragraph clicked')">I am a paragraph
<button onclick="alert('button clicked')">Click</button>
</p>
</section>
<div id="container">
Click to Hide
<button id="colorbtn">Change Color</button>
</div>
<div id="show">
Click here to Show
</div>
<script src="app.js"></script>
</body>
</html>

Some blocks will not work for sure - as you call hideElements with a single argument - the second arg will be undefined and there's no classList on undefined of course (causes error).
And also it's very confusing because you add event listener on the show element twice..
Copied from your post and added comments:
//hideElements(show); //run function once to toggle on the 'hide' class so that this is not shown by default.
hideElements(show) // - this will error like I said above as elem2 will be undefined..
button.addEventListener('click', function (e) {
container.style.backgroundColor = makeRandColor();
e.stopPropagation();
})
container.addEventListener('click', function () {
hideElements(container, show);
//hideElements(container, show); //hide the 'click here to hide' stuff
})
//below you make same event listener twice which is very confusing :
show.addEventListener('click', function () {
hideElements(container);
hideElements(show);
})
show.addEventListener('click', function () {
hideOneElement(container);
hideOneElement(show);
})

Related

Uncaught TypeError: Cannot set property 'innerHTML' of nulll

Im getting a error menioned above to not use innerHTML please help with this.Getting error in js file.
Its just a basic counter im trying to make......
index.html
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script type="text/javascript" src="script.js"></script>
</head>
<body>
<div>
<h1>Counter</h1>
<h1 class="counter-display">(..)</h1>
<button class="counter-minus">-</button>
<button class="counter-plus">+</button>
</div>
</body>
</html>
Script.js
let counterDisplayElem = document.querySelector('.counter-display');
let counterMinusElem = document.querySelector('.counter-minus');
let counterPlusElem = document.querySelector('.counter-plus');
let count = 0;
updateDisplay();
counterPlusElem.addEventListener("click", () => {
count++;
updateDisplay();
});
counterMinusElem.addEventListener("click", () => {
count--;
updateDisplay();
});
function updateDisplay() {
counterDisplayElem.innerHTML = count;
};
Javascript can't find the elements since the script runs before the DOM is loaded.
You should put your code inside of a function and run it through the body onload.
function loaded() {
let counterDisplayElem = document.querySelector('.counter-display');
let counterMinusElem = document.querySelector('.counter-minus');
let counterPlusElem = document.querySelector('.counter-plus');
let count = 0;
updateDisplay();
counterPlusElem.addEventListener("click", () => {
count++;
updateDisplay();
});
counterMinusElem.addEventListener("click", () => {
count--;
updateDisplay();
});
function updateDisplay() {
counterDisplayElem.innerHTML = count;
};
}
<body onload="loaded()">
<div>
<h1>Counter</h1>
<h1 class="counter-display">(..)</h1>
<button class="counter-minus">-</button>
<button class="counter-plus">+</button>
</div>
</body>
Another option would be to put the script tag at the end of your <body> rather than inside of <head>
Your querySelector functions are probably not finding the HTML elements because they are running before the elements are created.
To prevent this without the use of jQuery you should surround your JS code with this:
document.addEventListener("DOMContentLoaded", function(event) {
//do work
});
It's supported by 99 % of browsers.
Put your code in a window's load event handler, then your code will run when your DOM has been loaded and document.querySelector('.counter-display'); can access to the h1 element.
Try this:
<script>
window.addEventListener('load', () => {
let counterDisplayElem = document.querySelector('.counter-display');
let counterMinusElem = document.querySelector('.counter-minus');
let counterPlusElem = document.querySelector('.counter-plus');
let count = 0;
updateDisplay();
counterPlusElem.addEventListener("click", () => {
count++;
updateDisplay();
});
counterMinusElem.addEventListener("click", () => {
count--;
updateDisplay();
});
function updateDisplay() {
counterDisplayElem.innerHTML = count;
};
});
</script>

Switch function when click button in javascript

I want to switch function when button 'historyBtn' clicked in javascript.
Here is the code:
const expr = '';
var history = "document.getElementById('historyBtn'). clicked == true";
switch (expr) {
case history:
allMessage();
break;
default:
limit_message();
}
But when i click button 'historyBtn' i cant switch function limit_message() to allMessage()
Edit:
in this case i want to run function 'limit_message' for default function, and then switch that function to allMessage when i click 'historyBtn'
Try this, first get the button element, then assign an eventListener and logs the click event:
let history = document.getElementById('historyBtn');
history.addEventListener('click', function(e){
console.log(e)
})
function hello(ishistory) {
if (ishistory) {
console.log("History")
}else{
console.log("Limit")
}
}
<button onclick="hello(true)">history</button>
<button onclick="hello(false)">Limit</button>
Just pass a flag to function on button click like this.
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id = "historyBtn" onclick="allMessage()">History</button>
</body>
<script>
window.onload = limit_message();
function allMessage (){
alert ('In all messages - historyBtn clicked!');
}
function limit_message (){
alert ('limit_message - onload');
}
</script>
</html>

Get event target inside a web component

Anchor elements (<a>) are created when the user interacts with a web component. The problem is, that I cannot get the anchor element returned from the "outside" of the web component when an anchor is clicked.
I add an event listener to document listening for click events. When an element somewhere in the DOM is clicked I expect the e.target to be the clicked element.
In the case of a click somewhere inside the web component the custom element (<fancy-list></fancy-list>) will be returned - not the clicked element.
When the mode of the shadow DOM is set to open the DOM should be accessible.
class Fancylist extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
wrapper.innerHTML = `<ul></ul><button>Add item</button>`;
shadow.appendChild(wrapper);
this.on_root_click = this.on_root_click.bind(this);
}
connectedCallback() {
this.ul_elm = this.shadowRoot.querySelector('ul');
this.shadowRoot.addEventListener('click', this.on_root_click, false);
}
on_root_click(e){
switch(e.target.nodeName){
case 'BUTTON':
this.ul_elm.innerHTML += '<li>List item</li>';
break;
case 'A':
e.preventDefault();
console.log('You clicked a link!');
break;
}
}
}
customElements.define('fancy-list', Fancylist);
<!DOCTYPE html>
<html>
<head>
<title>List</title>
<meta charset="utf-8" />
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', e => {
document.body.addEventListener('click', e => {
//console.log(e.composedPath());
console.log(e.target); // why is this not returning an anchor element when an anchor is clickend inside the <fancy-list>?
}, false);
}, false);
</script>
</head>
<body>
<h1>List</h1>
<fancy-list></fancy-list>
</body>
</html>
The purpose of the Shadow DOM is precisely to mask the HTML content the Shadow DOM from the containter point of view.
That's also why inner events are retargeted in order to expose the Shadow DOM host.
However, you can still get the real target by getting the first item of the Event.path Array property.
event.path[0]
NB: of course it will work only with open Shadow DOM.
class Fancylist extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const wrapper = document.createElement('div');
wrapper.innerHTML = `<ul></ul><button>Add item</button>`;
shadow.appendChild(wrapper);
this.on_root_click = this.on_root_click.bind(this);
}
connectedCallback() {
this.ul_elm = this.shadowRoot.querySelector('ul');
this.shadowRoot.addEventListener('click', this.on_root_click, false);
}
on_root_click(e){
switch(e.target.nodeName){
case 'BUTTON':
this.ul_elm.innerHTML += '<li>List item</li>';
break;
case 'A':
e.preventDefault();
break;
}
}
}
customElements.define('fancy-list', Fancylist);
<!DOCTYPE html>
<html>
<head>
<title>List</title>
<meta charset="utf-8" />
<script type="text/javascript">
document.addEventListener('DOMContentLoaded', e => {
document.body.addEventListener('click', e => {
console.log(e.path[0]);
}, false);
}, false);
</script>
</head>
<body>
<h1>List</h1>
<fancy-list></fancy-list>
</body>
</html>
Update 2021
As commented now you should use event.composedPath().

JavaScript: function fires in different order

I don't understand why function alert() fires before setting style to indicator in this code:
JavaScript:
var MyClass = function()
{
th = this;
th.func = function(){alert('yes');};
th.Click = function(){
document.getElementById('indicator').style.color = "#0f0";
document.getElementById('indicator').innerHTML = "YES";
th.func(); // here it fires before above style changing
};
th.Start = function()
{
var a = document.getElementById('button1');
a.addEventListener('click', th.Click, false);
};
th.Init = function()
{
th.Start();
};
}
var a = new MyClass().Init();
Html:
<button id='button1'>Click Me</button>
<div id='indicator' style='color:#f00'>NO</div>
I want it to fire after.
That is because of the Single Threaded Nature of the Javascript. The alert / modal window actually stops everything else from running until it is dismissed.
That includes the changing of color. Javascript actually says hey browser start changing colors and moves on but as it hits an alert the changing of colors or whatever processes that happen will be paused and it will not start continuing again until the modal window aka alert is dismissed. A workaround might be to do something like this: codepen
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<button id='button1'>Click Me</button>
<div id='indicator' style='color:#f00'>NO</div>
<script type="text/javascript">
var MyClass = function() {
th = this;
th.func = function() { window.alert('yes'); };
th.Click = function() {
document.getElementById('indicator').style.color = "#0f0";
document.getElementById('indicator').innerHTML = "YES";
// The setTimeout fix
// ====
setTimeout(th.func, 100);
};
th.Start = function()
{
var a = document.getElementById('button1');
a.addEventListener('click', th.Click, false);
};
th.Init = function()
{
th.Start();
};
}
var a = new MyClass().Init();
</script>
</body>
</html>

Click handler losing reference to "this" object that created it

I made JS script:
var zzz;
zzz = {
fff: function (Id) {
alert("You did it! Id="+Id);
},
main: function (Id) {
var button, elements;
button = document.createElement("input");
button.type = "submit";
button.onclick = function () {
zzz.fff(Id);
};
elements = document.getElementById(Id);
elements.appendChild(button);
}
};
and HTML, where I tested it:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=UTF-8">
<title>My Web Page!</title>
<script type="text/javascript" src="test.js"></script>
</head>
<body>
<div id="div001"></div>
<div id="div002"></div>
<script type="text/javascript">
object1 = zzz;
object1.main("div001");
object2 = zzz;
object2.main("div002");
</script>
</body>
</html>
Why it works only if I write button.onclick = function () { zzz.fff(Id); }; and with this.fff(Id) it doesn't work?
When you bind an event handler (such as onclick), inside the handler this becomes the element that triggered the event (except if you used an inline onclick="" attribute, which should be avoided).
Instead of using zzz, you could also copy this to another variable that would be available inside the handler via closure:
var that = this;
button.onclick = function () {
that.fff(Id);
};
Or you could use Function.prototype.bind:
var clickHandler = button.onclick = function () {
this.fff(Id);
};
button.onclick = clickHandler.bind(this);

Categories