JS - IntersectionObserver - Argument 1 does not implement interface Element - javascript

I'm trying to add a class 'is-visible' to all elements of a class 'module' using IntersectionObserver when they come into view on scrolling. I get the error:
IntersectionObserver.observe: Argument 1 does not implement interface
Element.
Javascript:
let modules = document.getElementsByClassName("module");
const observer = new IntersectionObserver(
([e]) => {
let modules = document.getElementsByClassName("module");
modules.forEach((entry) => {
entry.classList.toggle("is-visible", e.intersectionRatio < 1);
});
},
{ threshold: [0, 0.25, 0.5, 0.75, 1] }
);
window.addEventListener(
"load",
(event) => {
observer.observe(modules);
},
false
);
HTML
<div id="wrapper">
<div id="hero">
<h1>This is my hero</h1>
</div>
<div id='strip'>
<div id='s1'>Option 1</div>
<div id='s2'>Option 2</div>
<div id='s3'>Option 3</div>
<div id='s4'>Option 4</div>
</div>
<div id="box" class="module fade-in-section">
<h2>Contact us</h2>
<img src="https://picsum.photos/300/100" />
</div>
<div class="module fade-in-section">
<h2>What we do</h2>
<img src="https://picsum.photos/300/100" />
</div>
<div class="module fade-in-section">
<h2>Who we are</h2>
<img src="https://picsum.photos/300/100" />
</div>
<div class="module fade-in-section">
<h2>Mission</h2>
<img src="https://picsum.photos/300/100" />
</div>
<div class="module fade-in-section">
<h2>About us</h2>
<img src="https://picsum.photos/300/100" />
</div>
<div class="module fade-in-section">
<h2>Services</h2>
<img src="https://picsum.photos/300/100" />
</div>
<div class="module fade-in-section">
<h2>Processes</h2>
<img src="https://picsum.photos/300/100" />
</div>
</div>
Here's a codepen:
https://codepen.io/m-herda/pen/BazpzZM
UPDATE:
Apart from the explanation to my problem that was mentioned in the comments/answers, it looks like a lot of the code above was not necessary. The code below works fine and is much simpler.
const modules = document.querySelectorAll(".module");
function handleIntersection(entries) {
entries.map((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("is-visible");
} else {
entry.target.classList.remove("is-visible");
}
});
}
window.addEventListener("load", (event) => {
const observer = new IntersectionObserver(handleIntersection);
modules.forEach((m) => observer.observe(m));
});

The error you get is:
IntersectionObserver.observe: Argument 1 does not implement interface Element.
This clearly states that the first argument of the function IntersectionObserver.observe needs to be an element.
Read the documentation of it.
In your code, you are calling .observe with a collection of elements.
The solution is to call it on each element you want to observe separately.

Instead of using getElementsByClassName("module"); to select your class use querySelector(".module"); or querySelectorAll(".module"); when selecting the observed element.

Related

Filter html elements based on data attribute

I have the following html structure
<div id="container">
<div id="child_1" data-customId="100">
</div>
<div id="child_2" data-customId="100">
</div>
<div id="child_3" data-customId="100">
</div>
<div id="child_4" data-customId="20">
</div>
<div id="child_5" data-customId="323">
</div>
<div id="child_6" data-customId="14">
</div>
</div>
And what I want to do is to get the count of child divs that contains different data attribute. For example, I'm trying this:
$(`div[id*="child_"]`).length); // => 6
But that code is returning 6 and what I want to retrieve is 4, based on the different data-customId. So my question is, how can I add a filter/map to that selector that I already have but taking into consideration that is a data-attribute.
I was trying to do something like this:
var divs = $(`div[id*="child_"]`);
var count = divs.map(div => div.data-customId).length;
After you getting the child-divs map their customid and just get the length of unique values:
let divs = document.querySelectorAll(`div[id*="child_"]`);
let idCustoms = [...divs].map(div=>div.dataset.customid);
//idCustoms: ["100", "100", "100", "20", "323", "14"]
//get unique values with Set
console.log([... new Set(idCustoms)].length);//4
//or with filter
console.log(idCustoms.filter((item, i, ar) => ar.indexOf(item) === i).length);//4
<div id="container">
<div id="child_1" data-customId="100">
</div>
<div id="child_2" data-customId="100">
</div>
<div id="child_3" data-customId="100">
</div>
<div id="child_4" data-customId="20">
</div>
<div id="child_5" data-customId="323">
</div>
<div id="child_6" data-customId="14">
</div>
</div>
Note: $ is equivalent to document.querySelectorAll in js returns a NodeList that's why I destructure it by the three dots ...
You'll have to extract the attribute value from each, then count up the number of uniques.
const { size } = new Set(
$('[data-customId]').map((_, elm) => elm.dataset.customid)
);
console.log(size);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container">
<div id="child_1" data-customId="100">
</div>
<div id="child_2" data-customId="100">
</div>
<div id="child_3" data-customId="100">
</div>
<div id="child_4" data-customId="20">
</div>
<div id="child_5" data-customId="323">
</div>
<div id="child_6" data-customId="14">
</div>
</div>
No need for jQuery for something this trivial, though.
const { size } = new Set(
[...document.querySelectorAll('[data-customId]')].map(elm => elm.dataset.customid)
);
console.log(size);
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="container">
<div id="child_1" data-customId="100">
</div>
<div id="child_2" data-customId="100">
</div>
<div id="child_3" data-customId="100">
</div>
<div id="child_4" data-customId="20">
</div>
<div id="child_5" data-customId="323">
</div>
<div id="child_6" data-customId="14">
</div>
</div>
Note that the property customid is lower-cased in the JavaScript. This could be an easy point of confusion. You might consider changing your HTML from
data-customId="14"
to
data-custom-id="14"
so that you can use customId in the JS (to follow the common conventions).

Why my call map function is hide in the count nodeList but it is visible in return output?

This will be a complex sample cause my codes is like a lot...
So basically I just direct to my point. My problem is that I don't know why my list map function is not includes the output function of return when I'm trying to get the document.querySelectorAll() of all the items. Basically something like this.
<div className='gallery' style={ ispost ? { display:'none' } : {display:"block"}}>
{mainList.map((elem,idx) => {
return (
<div className="gallery-container" key={idx}>
<div className="l">
<div className="l-box">
<div className="text">
<div className="hide">
<h2> Onigsahima Tokyo </h2>
</div>
</div>
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
</div>
<div className="r">
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
</div>
</div>
)
})}
<div className="gallery-container" style={ ispost ? { display:'none' } : {display:"inline-grid"}}>
<div className="l">
<div className="l-box">
<div className="text">
<div className="hide">
<h2> Onigsahima Tokyo </h2>
</div>
</div>
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
</div>
<div className="r">
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
<div className="r-box">
<div className="hide">
<div className="color-sample">
<img src={g3} alt="" />
</div>
</div>
</div>
</div>
</div>
</div>
And then I have a useEffect() and I'll show all the codes about it..but don't get confused about it just focus on the querySelectorAll() cause it is the only thing that affect it all the animation
useEffect(() => {
gsap.registerPlugin(ScrollTrigger,CSSRulePlugin)
// gsap.registerPlugin(CSSRulePlugin)
const sections_ = document.querySelectorAll('.gallery-container')
console.log(sections_)
let sections = gsap.utils.toArray(".gallery-container")
console.log(sections)
let scrollTween = gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none", // <-- IMPORTANT!
scrollTrigger: {
trigger: ".gallery",
pin: true,
scrub: 3,
end: "+=3000",
// markers:true
}
});
gsap.to(".gallery-container:nth-child(1) .color-sample",{
y:0,
ease:"none",
scrollTrigger:{
trigger:".gallery-container:nth-child(1)",
containerAnimation:scrollTween,
start:"0% 0%",
end:"10% 6%",
// markers:{startColor: "orange", endColor: "green"},
scrub:2,
}
})
const rule = CSSRulePlugin.getRule(`.r-box:after`)
const rule2 = CSSRulePlugin.getRule(`.r-box:before`)
gsap.to([rule,rule2],{
width:'35%',
height:'35%',
background:"yellow",
delay:3,
ease:"none",
scrollTrigger:{
trigger:`.gallery-container:nth-child(1)`,
containerAnimation:scrollTween,
start:"0% 0%",
end:"10% 6%",
// markers:true,
scrub:2,
}
})
gsap.to(`.gallery-container:nth-child(1) .text h2`,{
y:0,
// delay:2,
ease:"none",
scrollTrigger:{
trigger:`.gallery-container:nth-child(1)`,
containerAnimation:scrollTween,
start:"0% 0%",
end:"10% 6%",
// markers:true,
scrub:2,
}
})
sections.forEach((elem,i) => {
gsap.to(`.gallery-container:nth-child(${i+2}) .color-sample`,{
y:0,
ease:"none",
scrollTrigger:{
trigger:`.gallery-container:nth-child(${i+2})`,
containerAnimation:scrollTween,
start:"center 50%",
end:"center 60%",
// markers:true,
scrub:2,
}
})
gsap.to(`.gallery-container:nth-child(${i+2}) .text h2`,{
y:0,
// delay:2,
ease:"none",
scrollTrigger:{
trigger:`.gallery-container:nth-child(${i+2})`,
containerAnimation:scrollTween,
start:"center 50%",
end:"center 60%",
// markers:true,
scrub:2,
}
})
const rule = CSSRulePlugin.getRule(`.r-box:after`)
const rule2 = CSSRulePlugin.getRule(`.r-box:before`)
gsap.to([rule,rule2],{
width:'35%',
height:'35%',
background:"yellow",
delay:3,
ease:"none",
scrollTrigger:{
trigger:`.gallery-container:nth-child(${i+2})`,
containerAnimation:scrollTween,
start:"center 50%",
end:"center 60%",
// markers:true,
scrub:2,
}
})
})
const arrowsColor = document.querySelectorAll('.arrow.first ion-icon')
const arrows2Color = document.querySelectorAll('.arrow.second ion-icon')
var scrollPos = 0
window.addEventListener('scroll',() => {
if ((document.body.getBoundingClientRect()).top > scrollPos) {
// console.log('Right')
arrowsColor.forEach(elem => {
elem.style.color = 'black'
})
arrows2Color.forEach(elem => {
elem.style.color = 'yellow'
})
} else {
// console.log('Left')
arrowsColor.forEach(elem => {
elem.style.color = 'yellow'
})
arrows2Color.forEach(elem => {
elem.style.color = 'black'
})
}
scrollPos = (document.body.getBoundingClientRect()).top
})
},[])
I don't know if this is easiest way to explain it but this is the best I can..
Note that mainList is a useState so I'll have something like this in my codes
when I am pushing the items in setMainList
useEffect(() => {
axios.get('http://localhost:7777/gallery')
.then(res => {
let del = []
res.data.map((elem,i) => {
console.log(i)
setMainList([...mainList,elem])
})
})
},[])
Yes it works but why the document.querySelectorAll don't detect if there is a new element that I pop in that mainList.map function? I don't understand why..It shouldn't be this kind of way. I always do this but never encounter this bug..
Note that mainList.map() is like visible but it is not include in that querySelectorlAll
Please help badly need it to get this thing done.
Because your useEffect callback runs only once since you haven't added any dependencies. If you want the callback to be invoked whenever mainList changes, add mainList to the useEffect dependency list like so
useEffect(() => {
//...
}, [mainList])
Be careful though! You should use the return callback to remove any listeners or subscriptions you've set up.
useEffect(() => {
//...
function onScroll() {
//...
}
window.addEventListener('scroll', onScroll)
return () => {
// remove listeners
window.removeEventListener('scroll', onScroll)
}
}, [mainList])
I don't know how gsap works, but you might also want to "deregister" the plugin in the tear-down callback, or just use a different useEffect callback with no dependencies to register it only once - you probably still want to "deregister" it either way, but you wont do it every time mainList changes.
Edit: I highly suggest you read the useEffect docs
Edit2: Here's a gist

I need to move a node list from one parent to another

I'm trying to move a node list (in my case img HTML tags with a class name of ".images") when I reach a specific breakpoint to another parent as the first child of that new parent.
/* BEFORE BREAKPOINT */
<div class="old-parent">
<img class="images">
<div class="new-parent">
</div>
</div>
<div class="old-parent">
<img class="images">
<div class="new-parent">
</div>
</div>
<div class="old-parent">
<img class="images">
<div class="new-parent">
</div>
</div>
/*AFTER REACHING THE BREAKPOINT*/
<div class="old-parent">
<div class="new-parent">
<img class="images">
</div>
</div>
<div class="old-parent">
<div class="new-parent">
<img class="images">
</div>
</div>
<div class="old-parent">
<div class="new-parent">
<img class="images">
</div>
</div>
So far is working when I use a single selector, however when I try to select them all and transfer them to a new parent is not working.
const newParent = document.querySelectorAll('.new-parent');
const images = document.querySelectorAll('.images');
const mediaQ = window.matchMedia('(max-width: 494px)');
const changeImg = (e) => {
if (e.matches) {
newParent.forEach(elem => {
elem.insertBefore(images, elem.childNodes[0]);
})
}
};
mediaQ.addEventListener('change', changeImg);
Then returns an error in the console:
Uncaught TypeError: Failed to execute 'insertBefore' on 'Node': parameter 1 is not of type 'Node'
You need to index images to move one image at a time.
const changeImg = (e, i) => {
if (e.matches) {
newParent.forEach(elem => {
elem.insertBefore(images[i], elem.childNodes[0]);
})
}
};

How to check if all the input values are equal to my data?

I have 32 items in my array, all of them have these properties: id, word, image. User has to guess what's in all the images and write their guess in inputs (so 32 inputs in total). I need to check if the input equals my arrays property "word" and then when clicked a button (type submit, all my pic's and inputs are in a form) display some text for example "Oops! Guess again" if wrong and "Yay! You got it correctly" if right. The text should appear below every input. I displayed all the pictures and inputs with a forEach, and i'm using bulma framework for this page:
const wordBox = info.forEach((words) => {
mainColumns.innerHTML += `
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src=${words.image} alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<input class="input" id="text" type="text" placeholder="Įvesk žodį">
</div>
</div>
<div class="content">
Content
</div>
</div>
</div>
</div>`;
});
Any ideas?
This is how it should look like (the result should appear in content place)
Something like this
I use change instead of a button click
const info = [
{word:"flower",image:"flower.gif"},
{word:"boat",image:"boat.gif"}
];
const mainColumns = document.getElementById("mainColumns");
mainColumns.innerHTML = info.map(({image,word}) =>
`<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src=${image} alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<input class="input" data-word="${word}" type="text" placeholder="Įvesk žodį">
<span class="correct hide">Yay</span>
<span class="wrong hide">NOO</span>
</div>
</div>
<div class="content">
Content
</div>
</div>
</div>
</div>`).join("");
mainColumns.addEventListener("change",function(e) {
const correct = [...mainColumns.querySelectorAll("[data-word]")].map(input => {
if (input.value) {
const correct = input.value === input.dataset.word;
parent = input.closest("div");
parent.querySelector(".correct").classList.toggle("hide",!correct)
parent.querySelector(".wrong").classList.toggle("hide",correct);
return correct ? 1 : 0;
}
else return 0;
}).reduce((a,b)=>a+b);
document.getElementById("correct").innerText = correct;
})
#mainColumns { display:flex; }
.hide { display: none; }
<div id="mainColumns"></div>
Correct: <span id="correct"></span>
What you can do is to filter the word array with word from the input value. Then check if the length is equal zero, No match, if the length is greater than one, then there is a match.
const status = wordBox.filter(item => item.word === inputWord)
I'd move towards keeping the objects and the HTML separate, binding the HTML to the object and vice versa. This means including a couple more properties to your array elements.
let info = [{
image: 'flower.png',
word: 'flower',
content: '',
guess: ''
}];
function bindWords() {
info.forEach((words) => {
mainColumns.innerHTML = `
<div class="column is-one-quarter">
<div class="card">
<div class="card-image">
<figure class="image is-4by3">
<img src=${words.image} alt="Placeholder image">
</figure>
</div>
<div class="card-content">
<div class="media">
<div class="media-content">
<input class="input" data-word="${words.word}" type="text" placeholder="Įvesk žodį" value="${words.guess}">
</div>
</div>
<div class="content">
${words.content}
</div>
</div>
</div>
</div>`;
});
}
bindWords();
check.addEventListener('click', () => {
info = Array.from(document.querySelectorAll('.card')).map(el => ({
image: el.querySelector('img').src,
word: el.querySelector('.input').dataset.word,
guess: el.querySelector('.input').value,
content: el.querySelector('.input').value === el.querySelector('.input').dataset.word ?
'Correct' : 'Incorrect'
}));
bindWords();
});
<div id="mainColumns"></div>
<button id="check">Check Answers</button>

Dynamicaly change class name of same class by adding number

One final problem..how to dynamicaly change class name of same instruments by adding number?
<div class="score">
<div class="system">
<div class="stff_Flute"></div>
<div class="stff_Flute"></div>
<div class="stff_Clarinet"></div>
</div>
<div class="system">
<div class="stff_Flute"></div>
<div class="stff_Flute"></div>
<div class="stff_Clarinet"></div>
</div>
</div>
To this?...
<div class="score">
<div class="system">
<div class="stff_Flute_1"></div>
<div class="stff_Flute_2"></div>
<div class="stff_Clarinet"></div>
</div>
<div class="system">
<div class="stff_Flute_1"></div>
<div class="stff_Flute_2"></div>
<div class="stff_Clarinet"></div>
</div>
</div>
I have this code https://jsfiddle.net/7cLoxn29/1/ but something is wrong...
I'm not terribly fond of jQuery, so I created a vanilla JS solution for you (hopefully that's OK!):
let parents = document.querySelectorAll(".system")
parents.forEach((parent) => {
let children = parent.querySelectorAll("div")
children = Array.from(children).reduce((accumulator, current) => {
if (current.className in accumulator) {
accumulator[current.className].push(current)
} else {
accumulator[current.className] = [current]
}
return accumulator
}, {})
for (var key in children) {
if (children[key].length > 1) {
children[key].forEach((child, i, target) => {
child.className = `${child.className}_${i+1}`
})
}
}
})
Note that this is ES2015 JS code.
Here's an updated fiddle: https://jsfiddle.net/7cLoxn29/5/

Categories