How to properly splice array item by id - javascript

I have a simple popup component that get's called on a click event in some places in my app. The popups get created on click and are pushed in an array, that is later forEach-ed and displayed so that when the user clicks on a button multiple times multiple popups are displayed. The success ones disappear and the red ones disappear once someone clicks on the X, I achieve that by splicing them from the array, but if a user generated 5 popups and clicks the X the next popup gets closed, not the one that the user has clicked on, so it seems that I am not targeting the specific popup order in the array but the next, how can I properly target the exact clicked modal so that it gets closed and not the one that is next in line, what am I doing wrong here? How should I splice the array properly so that I target the exact pushed modal and not the rest?
Here is my code:
<template>
<div class="main">
<div
v-for="item in array"
:key="item.id"
:class="['popUp', `popUp--type--${item.popUpTypeType}`]"
>
<div class="popUp-side">
<p class="exclamation-mark">!</p>
</div>
<h5 class="popUp-message">{{item.message}}</h5>
<div class="popUp-side">
<p class="closing-x" #click="closeMessage" v-if="item.popUpTypeType === 'danger'">X</p>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'popUp',
data: () => ({
array:[],
}),
methods: {
removepopUpType() {
var self = this;
var id = Math.round( Math.random()*1e13 );
var index = self.array.map(x => {
return x.id;
}).indexOf(id);
self.array.splice(index, 1);
},
callpopUpType(obj){
var newpopUpType = {
popUpTypeType: obj.type,
message: obj.message,
id: obj.id
};
if(obj.type === 'success'){
setTimeout(this.removepopUpType, 2000)
}
this.array.push(newpopUpType);
},
closeMessage() {
this.removepopUpType()
},
},
created() {
this.$root.$on('call-popUp', this.callpopUpType);
},
destroyed(){
this.$root.$off('call-popUp', this.callpopUpType);
}
}
</script>

There is quite a bit of unusual code in the example, so I may be missing something, but couldn't you just pass the item to the method.
<p
v-if="item.popUpTypeType === 'danger'"
class="closing-x"
#click="closeMessage(item)"
>X</p>
and then
closeMessage(item) {
const index = this.array.indexOf(item);
if (index >= 0) this.array.splice(index, 1)
},

Related

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!

Push value of state variable in state array in react

I am facing wierd issue here.
The senario is : I have 2 variables(lets say a,b) in state of component: 1 is 2d array and other is Object. I have multiple small divs on click of which i am storing 1/0 at a's index(like if i click 1st div in 1st row: i will update value of a[0][0] to 1).
And then there are buttons below that. One button will insert the current selection in the this.state.b array and clear the selections after insertion. One is there just to clear the selection And other buttons are for each saved selection. So whenever i click on any saved selection button , the divs selected in that selection should get selected. Everything upto this works fine.
Now the issue here is, when i save a selection, then click on that saved selection button and update it, Even if i dont click on save button the selection gets updated at that index and when i save it, it creates another entry in that object.
For ex: I have saved single selection. Assume b will have value like {0:[[0,0][1, 0]]} and then I click on selection 0 button,it will show just 1st div in second row selected. Then i select div at postion [0,0] it automatically updates the varibale b as {0:[[1,0][1,0]]}. and when i click save button, it will also push another entry in b like {0:[[1,0][1,0]], 1:[[1,0][1,0]]}.
I dont want to update previous entry, I just want to add new entry on click of save button.
this is my save function:
handleSave(index) {
let routes = this.state.routes;
const selectedArray = this.state.selectedArray;
routes[index] = selectedArray;
this.setState({routes: routes});
this.setState({selectedArray: new Array(2).fill(0).map(() => new Array(2).fill(0))});
}
this is my div pattern
<div className="content">
<div>
{
length.map(key => {
return <div key={key}>{
length.map(innerKey => {
return <div key={key +''+ innerKey} className={"radio " + (this.state.selectedArray[key][innerKey] === 1 ? "selected" : "unselected")} onClick={(e) => {
let op = this.state.selectedArray;
op[key][innerKey] = op[key][innerKey] === 0 ? 1 : 0;
this.setState({selectedArray: op})
}
}></div>
})}</div>
})
}
<button className="save" onClick={() => this.handleSave(Object.keys(this.state.routes).length)}>Save</button>
<button className="save" onClick={() => this.setState({selectedArray: new Array(2).fill(0).map(() => new Array(2).fill(0))})}>Clear</button>
</div>
<div>
{Object.keys(this.state.routes).map((key, index) => {
return <div key={index}><button className="save" onClick={() => {console.log(key);this.setState({selectedArray: this.state.routes[key]})}}>Selection {index}</button>
</div>
})}
</div>
any solution to this?
################################# Updates ##########################
Tried this.Still it is not working:
handleSave(index) {
const selectedArray = [...this.state.selectedArray];
this.setState({routes: {...this.state.routes, [index]: selectedArray}});
this.setState({selectedArray: new Array(14).fill(0).map(() => new Array(14).fill(0))});
}
let op = [...this.state.selectedArray];
op[key][innerKey] = (op[key][innerKey] === 0 ? 1 : 0);
this.setState({selectedArray: op})
Thanks

setTimeout on a high volume data

I have tried googling and searching entirety of stack overflow for this question but I think it boils down to the keywords I'm using to search.
Basically my problem boils down to the following: when the cursor leaves an element, wait 500 milliseconds before closing the element. Before close the element, check if the cursor is back in the element, and if its not, do not hide it.
I'm using vuejs to do this but I boiled down the problem to being in setTimeout function. The part where I have the code is fairly complex to post it here, therefore I created a simple POC to demonstrate the problem:
<template>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
components: {},
methods: {
handleMouseEnter(index) {
setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
return {
showBox: []
};
},
created() {
for (let i = 0; i <= 2000; i++) {
this.showBox[i] = false;
}
}
};
</script>
You can checkout the sandbox here: https://codesandbox.io/s/cold-river-ruz7b
If you hover over from top to bottom in a moderate speed you will realize that even after leaving the li element the red box stays.
I guess the problem lays in the fact that handleMouseEnter is being called with a setTimeout and the handleMouseLeave is not. Therefore, making handleMouseEnter be executed after handleMouseLeave therefore showing the box.
Any light would be highly appreciated here and if a short explanation could be given on why the problem is happening it would be great
Your example seems to operate the opposite way round to the original problem description (the timer is on showing not hiding) but I think I get what you mean.
As you suggest, the problem is that the timer callback is being called after the mouseleave event fires. So the red box does get hidden but shortly thereafter the timer fires and brings it back.
In the example below I have simply cancelled the timer using clearTimeout. In general it might be necessary to store an array of such timers, one for each element, but in this specific example I believe it only makes sense to have one timer active at once so I can get away without an array.
I also moved the initial population of showBox into data. There seemed no reason to use a created hook here.
There's no need to copy the whole array each time, you can just use $set to set the value in the existing array. I haven't changed that in my example.
I would also note that for this particular example you don't need an array to hold all the showBox values. Only one red box can be visible at once so you only need a single property to hold the index of the currently visible box. I haven't changed this in my example as I suspect your real use case is not as straight forward as this.
new Vue({
el: '#app',
methods: {
handleMouseEnter(index) {
this.currentTimer = setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
clearTimeout(this.currentTimer)
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
const showBox = [];
for (let i = 0; i <= 2000; i++) {
showBox[i] = false;
}
return {
showBox
};
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>

Use Vuetify v-btn with loader inside a v-for loop

I am trying to figure out how to have my dynamically generated buttons inside a for loop each have a separate loader. Vuetify has buttons with Loaders.
The problem I am having is, When these buttons are in a for loop and one is clicked they all show the loading indicator. I would like only the one that is clicked to show the loading indicator.
HTML:
<div v-for="(item, i) in items" :key='i'>
<v-btn
dark
color="pink"
:loading="loading"
#click="loader = 'loading'"
>
<v-icon>location_on</v-icon> Lookup
<span slot="loader">locating...</span>
<span slot="loader" class="custom-loader">
<v-icon dark>cached</v-icon>
</span>
</v-btn>
</div>
SCRIPT
data () {
return {
loader: null,
loading: false
}
},
Let's say I have 3 items. The code above will generate three buttons but they all will share the same loading param. How can I have each button use its only loading param? As always any and all help is much appreciated.
You're using the same data property for all buttons, so these buttons share the same loading state which affects the at one time, to make difference try to add a data property called index which represents the current clicked button index :
data () {
return {
index:-1,
loader: null,
loading: false
}
},
and bind loading prop to the condition loading && i==index and update the current index on click event #click="loader = 'loading';index=i" :
<div v-for="(item, i) in items" :key='i'>
<v-btn
dark
color="pink"
:loading="loading && i==index"
#click="loader = 'loading';index=i"
>
<v-icon>location_on</v-icon> Lookup
<span slot="loader">locating...</span>
<span slot="loader" class="custom-loader">
<v-icon dark>cached</v-icon>
</span>
</v-btn>
</div>
Its actually a lot easier than you think:
<div v-for="(item, i) in items" :key='i'>
<v-btn
dark
color="pink"
:loading="loading[index]"
#click="loading[index] = true"
>
<v-icon>location_on</v-icon> Lookup
<span slot="loader">locating...</span>
<span slot="loader" class="custom-loader">
<v-icon dark>cached</v-icon>
</span>
</v-btn>
</div>
data () {
return {
loading: {},
}
},
In the beginning loading[index] will be undefined, so it will be evaluated as false, once you establish its value in the click event it will be evaluated as true, it worked for me, hope it helps.
you can use reactive array to prevent changing the index like this.
<div v-for="(item, i) in items" :key='i'>
<v-btn #click.prevent="testload(i)" :loading="loading[i]"></v-btn>
</div>
data () {
return {
loading: [],
}
},
methods: {
testload: function (index) {
// reactive array
this.$set(this.loading, index, true);
console.log(index)
console.log(this.loading[index])
// stop loading after x miliseconds
setTimeout(() => (this.$set(this.loading, index, false)), 3000)
},
dont forget to input false according the length of items, this is the example:
getDataAll: function () {
var i = 0
for (i = 0; i < items.length; i++) {
this.loading.push(false);
}
}

how to add css classes to a component in react?

So basically what I am doing is iterating through an array of data and making some kind of list. What I want to achieve here is on clicking on a particular list item a css class should get attached.
Iteration to make a list
var sports = allSports.sportList.map((sport) => {
return (
<SportItem icon= {sport.colorIcon} text = {sport.name} onClick={this.handleClick()} key= {sport.id}/>
)
})
A single list item
<div className="display-type icon-pad ">
<div className="icons link">
<img className="sport-icon" src={icon}/>
</div>
<p className="text-center">{text}</p>
</div>
I am not able to figure out what to do with handleClick so that If I click on a particular list it gets highlighted.
If you want to highlight the particular list item it's way better to call the handleClick function on the list item itself, and you can add CSS classes more accurately with this approach,
here is my sample code to implement the single list component
var SingleListItem = React.createClass({
getInitialState: function() {
return {
isClicked: false
};
},
handleClick: function() {
this.setState({
isClicked: true
})
},
render: function() {
var isClicked = this.state.isClicked;
var style = {
'background-color': ''
};
if (isClicked) {
style = {
'background-color': '#D3D3D3'
};
}
return (
<li onClick={this.handleClick} style={style}>{this.props.text}</li>
);
}
});
Keep a separate state variable for every item that can be selected and use classnames library to conditionally manipulate classes as facebook recommends.
Edit: ok, you've mentioned that only 1 element can be selected at a time,it means that we only need to store which one of them was selected (I'm going to use the selected item's id). And also I've noticed a typo in your code, you need to link the function when you declare a component, not call it
<SportItem onClick={this.handleClick} ...
(notice how handleClick no longer contains ()).
And now we're going to pass the element's id along with the event to the handleClick handler using partial application - bind method:
<SportItem onClick={this.handleClick.bind(this,sport.id} ...
And as I said we want to store the selected item's id in the state, so the handleClick could look like:
handleClick(id,event){
this.setState({selectedItemId: id})
...
}
Now we need to pass the selectedItemId to SportItem instances so they're aware of the current selection: <SportItem selectedItemId={selectedItemId} ....Also, don't forget to attach the onClick={this.handleClick} callback to where it needs to be, invoking which is going to trigger the change of the state in the parent:
<div onClick={this.props.onClick} className={classNames('foo', { myClass: this.props.selectedItemId == this.props.key}); // => the div will always have 'foo' class but 'myClass' will be added only if this is the element that's currently selected}>
</div>

Categories

Resources