Reduce DOM updates vanilla JS - javascript

I want to dynamically get and display a list when the button is clicked. I have this code:
document.getElementById('button').addEventListener('click', (e) => {
const arr = ['item1', 'item2', 'item3'];
const outputContainer = document.getElementById('output');
while (outputContainer.firstChild) {
outputContainer.removeChild(outputContainer.lastChild);
}
arr.map((el) => {
const listItem = document.createElement('li');
const txt = document.createTextNode(el);
listItem.appendChild(txt);
outputContainer.appendChild(listItem);
})
})
<button id="button">Get list</button>
<ul id="output" />
But every time I click the button the whole ul element is re-rendered although its content doesn't change. Is there any way to re-write this code somehow using vanilla JS to avoid unnecessary DOM updates?
P.S: List can be different every time cause it will be coming from an API

If you need to overwrite your list items every time your button is clicked, you could use the Element.replaceChildren() method (MDN documentation).
It saves you the trouble of having to manually remove each child before adding new ones.
I rewrote a bit of your code but the logic is the same. Notice how you don't need the while loop anymore.
As far as I know, the UL will still re-render since your're updating (replacing) its children. You could implement a comparison between the previous and next list elements and stop the function early if both lists contain the same data, thus preventing unnecessary renders.
// get the output UL element
const outputContainer = document.getElementById("output");
const onButtonClick = () => {
const arr = ["item1", "item2", "item3"]; // use of element.replaceChildren() to clear the list and append new items
outputContainer.replaceChildren(
...arr.map((listItemText) => {
const listItem = document.createElement("li");
listItem.textContent = listItemText;
return listItem;
})
);
};
document.getElementById("button").addEventListener("click", onButtonClick);
<button id="button">Get list</button>
<ul id="output" />

Related

Is there a way to get all classes using document.querySelectorAll in reactjs and manipulate it?

I wanted to allow users to change the theme of the application by picking which theme they want the body's background color changes and all button colors. But the problem is that whenever I use document.querySelectorAll('.btn-theme-1').style.backgroundColor it tells me that it cannot read those properties.
I know of useRef() but in my case I am trying to select all buttons throughout the entire application. Not just one element in the current component. So I would like to know if there is a way to fix what I am attempting or if I am doing this the wrong way.
Here is the code for what I tried. This is my pick theme component:
import ColorThemes from '../data/ColorThemes';
import { useEffect } from 'react';
const PickTheme = () => {
const changeTheme = (c) => {
document.body.style.background = c.default || c.bgColor;
document.body.style.color = c.bodyColor;
document.querySelector('.bi-quote').style.color = c.buttonBg;
document.querySelectorAll('.text-color').forEach(el => el.style.color = c.fontColor)
document.querySelectorAll('.btn-theme-1').forEach(el => {
el.style.color = c.buttonColor;
el.style.backgroundColor = c.buttonBg;
});
};
useEffect(() => {
},[changeTheme]);
return(
ColorThemes.background.map(c => {
if(c.bgColor) {
return(
<button type="button" key={c.bgColor} className="btn btn-light me-2 p-3 rounded-5" onClick={() => changeTheme(c)} style={{backgroundColor: c.bgColor}}></button>
);
} else {
return(
<><br/><button type="button" key={c.default} className="btn btn-light me-2 mt-2 rounded-5" onClick={() => changeTheme(c)}>Default</button></>
);
}
})
);
};
export default PickTheme;
It successfully changes the bodys color and background color but not the other classes. I tried with and without useEffect and still receive the same issue.
If I comment out everything except the last selector, the buttons then change colors. So maybe it is conflicting or cannot change everything at once, for example:
const changeTheme = (c) => {
// document.body.style.background = c.default || c.bgColor;
// document.body.style.color = c.bodyColor;
// document.querySelector('.bi-quote').style.color = c.buttonBg;
// document.querySelectorAll('.text-color').forEach(el => el.style.color = c.fontColor)
document.querySelectorAll('.btn-theme-1').forEach(el => {
el.style.color = c.buttonColor;
el.style.backgroundColor = c.buttonBg;
});
};
This changes the buttons background and color after commenting out the other parts.
I know of useRef() but in my case I am trying to select all buttons throughout the entire application. Not just one element in the current component.
Using .querySelector or any other selector will select only those elements that are currently rendered, actually. So if you e.g. toggle the state and component re-renders with different elements, they will not be affected with your change, which will result in partially toggled theme for different elements.
You should either set a context, wrapping whole App or set a redux variable holding info which theme is currently selected. Then, you will be able to manipulate styles using e.g. theme in styled components: https://styled-components.com/docs/advanced#theming or just toggling classNames with css modules, basing on that variable.
You can use useRef() with a function that runs on each element and add them to it, let me dive deeper into it.
Let's first create a reference containing, for now, an empty array:
const myRef = useRef([])
Great, we now want to populate that.
It's going to be in two parts, first, let's make a function that will populates that array:
const addToMyRef = (element) => {
if (element && !myRef.current.includes(element)) {
myRef.current.push(element);
}
};`
Great, we now have a function that takes an element of the DOM as an argument, verifies that it exists and that it is not yet in our array, then adds it.
But now, when will it get triggered? Simply in the ref= attribute!
<button ref={addToMyRef}></button>
You'll now see that your reference is now a set of them, so you can create a reference per element, or maybe modify the code a little to makes it takes objects to have a all-in-one reference for each element of the dom. (We could imagine it being myRef.buttons/myRef.inputs...)

Can a javascript variable be made local to a specific html element?

As a novice Javascript programmer, I'd like to create an html document presenting a feature very similar to the "reveal spoiler" used extensively in the Stack Exchange sites.
My document therefore has a few <div> elements, each of which has an onClick event listner which, when clicked, should reveal a hiddent text.
I already know that this can be accomplished, e.g., by
<div onclick="this.innerHTML='Revealed text'"> Click to reveal </div>
However, I would like the text to be revealed to be initially stored in a variable, say txt, which will be used when the element is clicked, as in:
<div onclick="this.innerHTML=txt"> Click to reveal </div>
Since there will be many such <div> elements, I certainly cannot store the text to be revealed in a global variable. My question is then:
Can I declare a variable that is local to a specific html element?
Yes you can. HTML elements are essentially just Javascript Objects with properties/keys and values. So you could add a key and a value to an HTML element object.
But you have to add it to the dataset object that sits inside the element, like this:
element.dataset.txt = 'This is a value' // Just like a JS object
A working example of what you want could look like this:
function addVariable() {
const myElement = document.querySelector('div')
myElement.dataset.txt = 'This is the extended data'
}
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
addVariable() // Calling this one immediately to add variables on initial load
<div onclick="showExtendedText(event)">Click to see more </div>
Or you could do it by adding the variable as a data-txt attribute right onto the element itself, in which case you don't even need the addVariable() function:
function showExtendedText(event) {
const currentElement = event.currentTarget
currentElement.innerHTML += currentElement.dataset.txt
}
<div onclick="showExtendedText(event)" data-txt="This is the extended data">Click to see more </div>
To access the data/variable for the specific element that you clicked on, you have to pass the event object as a function paramater. This event object is given to you automatically by the click event (or any other event).
Elements have attributes, so you can put the information into an attribute. Custom attributes should usually be data attributes. On click, check if a parent element has one of the attributes you're interested in, and if so, toggle that parent.
document.addEventListener('click', (e) => {
const parent = e.target.closest('[data-spoiler]');
if (!parent) return;
const currentMarkup = parent.innerHTML;
parent.innerHTML = parent.dataset.spoiler;
parent.dataset.spoiler = currentMarkup;
});
<div data-spoiler="foo">text 1</div>
<div data-spoiler="bar">text 2</div>
That's the closest you'll get to "a variable that is local to a specific html element". To define the text completely in the JavaScript instead, one option is to use an array, then look up the clicked index of the spoiler element in the array.
const spoilerTexts = ['foo', 'bar'];
const spoilerTags = [...document.querySelectorAll('.spoiler')];
document.addEventListener('click', (e) => {
const parent = e.target.closest('.spoiler');
if (!parent) return;
const currentMarkup = parent.innerHTML;
const index = spoilerTags.indexOf(parent);
parent.innerHTML = spoilerTexts[index];
spoilerTexts[index] = currentMarkup;
});
<div class="spoiler">text 1</div>
<div class="spoiler">text 2</div>
There are also libraries that allow for that sort of thing, by associating each element with a component (a JavaScript function/object used by the library) and somehow sending a variable to that component.
// for example, with React
const SpoilerElement = ({ originalText, spoilerText }) => {
const [spoilerShown, setSpoilerShown] = React.useState(false);
return (
<div onClick={() => setSpoilerShown(!spoilerShown)}>
{ spoilerShown ? spoilerText : originalText }
</div>
);
};
const App = () => (
<div>
<SpoilerElement originalText="text 1" spoilerText="foo" />
<SpoilerElement originalText="text 2" spoilerText="bar" />
</div>
)
ReactDOM.createRoot(document.querySelector('.react')).render(<App />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div class='react'></div>
Thanks everybody for your answers, which helped immensely! However, as a minimalist, I took all that I learned from you and came up with what I believe is the simplest possible code achieving my goal:
<div spoiler = "foo" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>
<div spoiler = "bar" onclick="this.innerHTML=this.getAttribute('spoiler')">
Click for spoiler
</div>

Why is "onclick" only working once and fails to update?

I am trying to understand why this onclick button is only working once.
Basically I am testing to see if the "heart" or "wishlist" button is clicked on. When clicked, console.log the name of the product so I can confirm it. But it only picks up the first product. When I click the wishlist button on the second product.
It gives this error "Uncaught SyntaxError: Invalid or unexpected token (at products:1:10)"
When I go to that line it just show ''
I have also tried using a
const wishlistBtn = document.querySelector('.wishlistBtn');
wishlistBtn.addEventListener('click', (product_name) => { console.log(product_name) })
But it just returns that the property is null. I'm wondering if the reason is because of the innerHTML I am including all of this in.
Javascript:
const getProducts = () => {
return fetch('/get-products', {
method: 'POST',
headers: new Headers({'Content-Type':'application/json'}),
body: JSON.stringify({})
})
.then(res => res.json())
.then(data => {
createProductCards(data);
})
}
var wishlist = (product_name) => {
console.log(product_name);
}
const createProductCards = (data) => {
let parent = document.querySelector('.container');
let start = '<div class="product-container">';
let middle = '';
let end = '</div>';
for(let i = 0; i < data.length; i++){
if(data[i].id != decodeURI(location.pathname.split('/').pop()) && !data[i].draft){
middle += `
<div class="product-card">
<div class="product-image">
${data[i].discount === '0' ? ``:`
<span class="discount-tag">${data[i].discount}% off</span>
`}
<img src="${data[i].images[0]}" class="product-thumb" alt="">
<button class="card-btn wishlistBtn" onclick="wishlist('${data[i].name}')"><i class="bi-heart"></i></button>
</div>
<div class="product-info">
<h6 class="product-brand">${data[i].name}</h6>
${data[i].discount === '0' ? `<span class="price">$${data[i].totalPrice}</span>`:`
<span class="price">$${data[i].totalPrice}</span>
<span class="actual-price">$${data[i].actualPrice}</span>
`}
</div>
</div>
`;
}
}
parent.innerHTML = start + middle + end;
}
getProducts();
document.querySelector works only on the first matched element. You may need to use document.querySelectorAll & attach event after the for loop has completely finished it's execution
const wishlistBtn = document.querySelectorAll('.wishlistBtn').forEach((item) => {
item.addEventListener('click', getProductName)
})
function getProductName(product_name) {
console.log(product_name)
})
Here is an example
document.querySelectorAll('.test').forEach(item => {
item.addEventListener('click', getButtonValue)
})
function getButtonValue(elem) {
console.log(elem.target.innerHTML)
}
<button class="test">1</button>
<button class="test">2</button>
<button class="test">3</button>
<button class="test">4</button>
<button class="test">5</button>
<button class="test">6</button>
document.querySelector only returns the first instance of the selector. So the first wish list button on your page is the only one that gets a listener attached.
If you're coming from JQuery, this is a nuanced difference. To add the event listener to every .wishlistBtn you could do something like:
const wishlistBtns = document.querySelectorAll('.wishlistBtn');
[...wishlistBtns].forEach(wishListButton => wishListButton.addEventListener('click', (product_name) => { console.log(product_name) })
There are two differences:
The use of querySelectorAll returns a NodeList of all of the elements that match the .wishlistBtn selector.
Iterate over the NodeList and add an event listener to each individual node. Unfortunately NodeList isn't exactly an array so [...wishlistButtons] is a quick and dirty way to convert it to an array using the relatively new spread operator ...
I seem to have found my problem. The issue was with one of my products having quotations inside of it for some reason but once removed the onclick worked multiple times while sending the product name to a function to keep track.
The problem with the answers given was also that I didnt want to display the name at all inside the button itself <button class=“test”>Item</button> instead this is what I needed <button onclick=‘func(${passname})></button> so that would have not worked when attempted but it gave me a general idea for future references. Thanks!

How to get MDCMenu instance by element?

Let assume that I have a lot of html elements need to use MDCMenu. I don't want to init them one by one, so I init all of them with the code below:
html:
<button class="my-menu-toggle" data-toggle="mdc-menu" data-target="#my-menu">Menu Toggle</button>
<div class="mdc-menu" id="my-menu">
</div>
js:
document.querySelectorAll('[data-toggle="mdc-menu"]').forEach(toggleEl => {
let menuEl = document.querySelector(toggleEl.dataset.target);
let menu = new MDCMenu(menuEl);
toggleEl.addEventListener('click', (e) => {
menu.open = !menu.open;
});
// maybe I should do this, just wondering that if MDC already do same thing that I haven't figure out.
menuEl.MDCMenu = menu;
});
then I want to do somethings with one of menu, how can I get the MDCMenu instance of the element?

JS sync and async onclick functions

I've been struggling with this issue lately.
I'm not sure if it has any connection to "sync/async" functions in JS. If it does, I would be more then thankful to understand the connection.
I've been making a simple component function:
There's a button "next","back" and "reset". Once pressing the matching button, it allows moving between linkes, according to button's type.
The links are an array:
const links = ["/", "/home", "/game"];
Here is the component:
function doSomething() {
const [activeLink, setActiveLink] = React.useState(0);
const links = ["/", "/home", "/game"];
const handleNext = () => {
setActiveLink((prevActiveLink) => prevActiveLink+ 1);
};
const handleBack = () => {
setActiveLink((prevActiveLink) => prevActiveLink- 1);
};
const handleReset = () => {
setActiveLink(0);
};
return (
<div>
<button onClick={handleReset}>
<Link className = 'text-link' to = {links[activeLink]}> Reset</Link>
</button>
<button onClick={handleBack}>
<Link className = 'text-link' to = {links[activeLink]}>Back</Link>
</button>
<button onClick={handleNext}>
<Link className = 'text-link' to = {links[activeLink]}>Next</Link>
</button>
</div>
When I'm trying to put the activeLink in the "to" attribute of Link, it puts the old value of it. I mean, handleNext/ handleReset/handleBack happens after the link is already set; The first press on "next" needs to bring me to the first index of the links array, but it stayes on "0".
Is it has to do something with the fact that setActiveLink from useState is sync function? or something to do with the Link?
I would like to know what is the problem, and how to solve it.
Thank you.
Your Links seem to be navigating to a new page?
If so, the React.useState(0) gets called each time, leaving you with the default value of 0.
Also your functions handleNext and handleBack aren't called from what I can see.

Categories

Resources