How to get MDCMenu instance by element? - javascript

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?

Related

Drag and drop list with hierarchy/nested behavior

I only want to do this with vanilla javascript. But if there is some lightweight library I might reconsider. Anyway, I basically want to emulate a file system with a list of elements.
<div class="container" >
<div class="item draggable" draggable> First </div>
<div class="children-nodes">
<div class="container" >
<div class="item draggable" draggable> First </div>
<div class="children-nodes">
</div>
</div>
</div>
</div>
As you can see in my HTML structure. Each container can be dragged and placed inside another .children-nodes, however the items themselves can also be dragged and moved around in the hierarchy. There are multiple of these containers with items nested inside eachother. I have written several algorithms to try to make this work I just can't seem to make it happen. I get errors like "new child contains parent" and etc.
Here's the latest logic i tried:
const draggables = document.querySelectorAll('.draggable');
draggables.forEach(draggable => {
draggable.addEventListener('dragstart', () => {
draggable.classList.add('is-dragging');
})
draggable.addEventListener('dragend', () => {
draggable.classList.remove('is-dragging');
})
draggable.addEventListener('dragover', (event) => {
event.stopPropagation()
event.preventDefault();
if(!draggable.classList.contains('.is-dragging')){
const item = document.querySelector('.is-dragging');
const containerOfItem = document.querySelector('.container:has(.is-dragging)');
const childrenContainer = containerOfItem.querySelector('.children-nodes');
const children = childrenContainer.querySelectorAll('div');
if(children.length == 0) {
const childrenDropContainer = draggable.parentNode.querySelector('.children-nodes');
childrenDropContainer.appendChild(containerOfItem);
} else {
}
}
})
})
And like I said. I've written it multiple different ways. I changed the markup too and searched around for some information. The kind of help that I would want would be what to consider, in which direction should I move, how do I solve this problem. Any help is gladly received.

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!

AddEventListener event fires only once

I am using Django templating engine and JavaScript. My HTML looks like this
<p class="content-card__address">{{ z.formatted_address|truncatewords:6 }}</p>
<div class="content-card-inner">
<p class="content-card__review">Отзывы ({{ z.post_relate.all.count }})</p>
<p class="content-card__phone">{{ z.international_phone_number }}</p>
<div class="div-shaddow"></div>
<p class="content-card__text">Показать</p>
</div>
Cards with text to be generated on the backend using a template engine. My JavaScript code only works on the first card and I need it to work on all cards. With JavaScript I add a class to the div elements. Here is my JavaScript
let call = document.querySelector('.content-card__text');
let divShadow = document.querySelector('.div-shaddow');
call.addEventListener('click', clickCall)
function clickCall() {
call.classList.add('visually-hidden');
divShadow.classList.add('visually-hidden');
}
This code returns you the first element in the DOM and you add click handlers only for it
document.querySelector('.content-card__text')
It will work for you:
const buttons = document.querySelectorAll('.content-card__text');
buttons.forEach(button => {
button.addEventListener('click', clickCall);
});
But please also note that you need to take this into account when working with .divShadow if this element is not alone on the page
Update: example based on your comment
const buttons = document.querySelectorAll('.content-card__text');
const divShadow = document.querySelectorAll('.div-shaddow');
buttons.forEach((button, index) => {
button.addEventListener('click', () => clickCall(index));
});
function clickCall(index) {
buttons[index].classList.add('visually-hidden');
divShadow[index].classList.add('visually-hidden');
}

Is there any way to bind a div which is created after DOM loaded and add event on it on vanila JS?

currently I am making my own project and I got stuck in :/
I wanted to add an event and give some function on div which is made with a button("create"). However, the console returned "Uncaught TypeError: Cannot read property 'addEventListener' of null" this. I think it is because DIV(.c) is made after JS run.
But I need to add an eventlistener and function on DIV(.c) to accomplish what I want.
So, is there any way to bind a div which is created later and add an
const createButton = document.querySelector(".create");
const paperBook = document.querySelector(".b");
createButton.addEventListener("click", createWriting);
function createWriting(e) {
e.preventDefault();
const writing = document.createElement("div");
writing.classList.add("c");
writing.innerHTML = `All work No rest make jack a dull boy`;
paperBook.appendChild(writing);
}
const myProblem = document.querySelector(".c");
myProblem.addEventListener("click", randomFunction);
function randomFunction(e) {
e.preventDefault();
console.log(e)
}
<div class="a">
<button class="create">create</button>
<div class="b"></div>
</div>
event on it?
Below is my code. But I summarized it to simplify for you if you need a whole code just ask me, please :)
thx!
I think the problem is that this part of code
const myProblem = document.querySelector(".c");
It is executed before you create your .c element, so the new elements would not be included on it. Try to add the event everytime a new element is created in the createWriting function
const createButton = document.querySelector(".create");
const paperBook = document.querySelector(".b");
createButton.addEventListener("click", createWriting);
function createWriting(e) {
e.preventDefault();
const writing = document.createElement("div");
writing.classList.add("c");
writing.innerHTML = `All work No rest make jack a dull boy`;
paperBook.appendChild(writing);
writing.addEventListener("click", randomFunction);
}
function randomFunction(e) {
e.preventDefault();
console.log(e)
}
<div class="a">
<button class="create">create</button>
<div class="b"></div>
</div>

Categories

Resources