Random number changes every time an event happens - javascript

So I have this array of colors in my data and I'm getting a random element from it by using Math.random() the problem is every time I click on <v-autocomplete :items="types" label="User type" multiple v-model="filters"></v-autocomplete>
the random color changes.
Here is a simplified version of my code:
<template>
<div>
<div
:key="index"
style="width:200px;height:200px;"
:style="'background-color:' + colors[Math.floor(Math.random() * 10)]"
v-for="(item, index) in items"
>
</div>
<v-autocomplete :items="types" label="User type" multiple v-model="filters"></v-autocomplete>
</div>
</template>
my data:
data: () => ({
items: [
{
name: "a"
},
{
name: "b"
},
{
name: "c"
}
],
colors: [
"#C004D9",
"#AB05F2",
"#A69C0F",
"#2745F2",
"#1B78F2",
"#F2BE22",
"#F2E635",
"#F29849",
"#2405F2",
"#6503A6",
"#010440",
"#F2E74B"
],
types: ["user", "admin", "manager"]
})
My question is can I stop Vue from updating the random number when I click on something or change some data?

If the data properties or computed properties the colorful DIVs references will not change when click 'AutoComplete', you may consider below two solutions:
Solution 1:
Uses the directive v-once:
Rendering plain HTML elements is very fast in Vue, but sometimes you
might have a component that contains a lot of static content. In these
cases, you can ensure that it’s only evaluated once and then cached by
adding the v-once directive to the root element
Vue.use(VAutocomplete.default)
new Vue({
el: "#app",
data () {return {
items: [
{
name: "a"
},
{
name: "b"
},
{
name: "c"
}
],
colors: [
"#C004D9",
"#AB05F2",
"#A69C0F",
"#2745F2",
"#1B78F2",
"#F2BE22",
"#F2E635",
"#F29849",
"#2405F2",
"#6503A6",
"#010440",
"#F2E74B"
],
types: ["user", "admin", "manager"],
filters: []
}}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/v-autocomplete#1.8.2/dist/v-autocomplete.min.js"></script>
<div id="app">
<v-autocomplete :items="types" label="User type" multiple v-model="filters"></v-autocomplete>
<div v-once>
<div
:key="index"
style="width:200px;height:200px;"
:style="'background-color:' + colors[Math.floor(Math.random() * 10)]"
v-for="(item, index) in items"
>
</div>
</div>
</div>
Solution 2:
Wraps the colorful DIVs into one component, if the dependencies of the component don't trigger the reactivity, the component will not be updated.
Vue.use(VAutocomplete.default)
Vue.component('v-color', {
'template': `
<div>
<div
:key="index"
style="width:200px;height:200px;"
:style="'background-color:' + colors[Math.floor(Math.random() * 10)]"
v-for="(item, index) in items"
>
</div>
</div>
`,
data () {
return {
colors: [
"#C004D9",
"#AB05F2",
"#A69C0F",
"#2745F2",
"#1B78F2",
"#F2BE22",
"#F2E635",
"#F29849",
"#2405F2",
"#6503A6",
"#010440",
"#F2E74B"
],
items: [
{
name: "a"
},
{
name: "b"
},
{
name: "c"
}
],
}
}
})
new Vue({
el: "#app",
data () {return {
types: ["user", "admin", "manager"],
filters: []
}},
methods: {
clickSomething() {
this.types.push('a')
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/v-autocomplete#1.8.2/dist/v-autocomplete.min.js"></script>
<div id="app">
<!-- <v-autocomplete :items="types" label="User type" multiple v-model="filters"></v-autocomplete> -->
<v-autocomplete :items="types" label="User type" multiple v-model="filters"></v-autocomplete>
<v-color></v-color>
</div>
or as the answer from #Rick, you can pre-calculate the color value for each DIVs first in data properties or computed properties, then binds it to the :style="color:_"

you don't want to have a random function in your style. That style is going to fire an indefinite number of times.
instead create a variable when the page loads that gets populated by your random function. Then use that variable to define your style.

Related

Vue draggable does not update text input fields when switched, but does update it in the array

I am trying to implement a drag and drop document builder with Vue draggable, you can add elements like headings and rearrange their order. These involve using components with input fields in.
I have got it working ok, except when you type something into heading 1 for example, then swap it with heading 2, the input text you typed is still in the same position where heading 1 was, even though the element swapped. But bizarely, it does switch it round on the array list correctly.
Basically, the input you type doesn't seem to stay with the component when you swap it. It either stays in its ORIGINAL place or just clears, which is obviously very problematic.
It uses a dynamic component looping through the array to display the list, it also involves emitting an object which then updates in the array:
AddNew.vue
<InsertContent #create="createNewElement($event, 0)" />
<draggable :list="userData.packetSections" :options="{animation:750}" handle=".handle">
<div
class="element-wrapper"
v-for="(section, index) in userData.packetSections"
:key="index"
>
<div class="element">
<component :is="section.type + 'Element'" #return="addData($event, index)" />
<i class="remove" #click="removeElement(index)"></i>
</div>
<InsertContent #create="createNewElement($event, index + 1)" />
</div>
</draggable>
<script>
data() {
return {
userData: {
packetSections: [
{
type: "Heading",
text: "test input", //<-- this swaps in the array when switched but does NOT swap in the browser
id: ""
},
{
type: "Heading",
text: "test 2",
id: ""
},
],
}
};
},
methods: {
createNewElement(event, index) {
var element = {
type: event,
text: "",
id: ""
};
this.userData.packetSections.splice(index, 0, element);
},
removeElement(index) {
this.userData.packetSections.splice(index, 1);
},
addData(emittedData, index) {
this.userData.packetSections.splice(index, 1, emittedData);
console.log(emittedData, index);
},
}
};
</script>
HeadingElement.vue
<template>
<div class="grey-box">
<h3>Subheading</h3>
<input type="text" v-model="data.text" #change="emitData" />
</div>
</template>
<script>
export default {
data(){
return{
data: {
type: "Heading",
text: "",
id: ""
}
}
},
methods:{
emitData(){
this.$emit('return', this.data);
}
}
};
</script>
Does anyone know why the text input fields would update in the array but NOT update the position in the browser?
Thanks
For anyone interested, I ended up fixing this. you have to pass down the contents of the array as props when you are working with dynamic components in your list that also pass up data.
So I change it from:
<component :is="section.type + 'Element'" #return="addData($event, index)" />
to:
<component :is="section.type + 'Element'" :data="userData.packetSections[index]" #return="addData($event, index)" />
and it worked fine.

Using Vuex, how do I remove items from an array when they are part of an array of objects?

Referencing the live demo code here:
https://codesandbox.io/s/vue-template-r26tg
Let's say I have a Vuex store with the following data:
const store = new Vuex.Store({
state: {
categories: [
{
name: "Category A",
items: [{ name: "Item 1" }, { name: "Item 2" }, { name: "Item 3" }]
},
{
name: "Category B",
items: [{ name: "Item A" }, { name: "Item B" }, { name: "Item C" }]
},
{
name: "Category C",
items: [{ name: "Item !" }, { name: "Item #" }, { name: "Item #" }]
}
]
}
});
And I have an App.vue, Category.vue and Item.vue that are set up so that they are rendered like so:
//App.vue
<template>
<div id="app">
<Category v-for="(category, index) in categories" :category="category" :key="index"/>
</div>
</template>
<script>
export default {
components: { Category },
computed: {
...mapState(["categories"])
}
};
</script>
//Category.vue
<template>
<div class="category">
<div class="header">{{ category.name }}</div>
<Item v-for="(item, index) in category.items" :item="item" :key="index"/>
</div>
</template>
<script>
export default {
components: { Item },
props: {
category: { type: Object, required: true }
}
};
</script>
//Item.vue
<template>
<div class="item">
<div class="name">{{ item.name }}</div>
<div class="delete" #click="onDelete">✖</div>
</div>
</template>
<script>
export default {
props: {
item: { type: Object, required: true }
},
methods: {
onDelete() {
this.$store.commit("deleteItem", this.item);
}
}
};
</script>
In other words, App.vue gets the list of categories from Vuex, then passes it down to Category.vue as a prop for each category, then Category.vue passes down category.items to Item.vue as a prop for each item.
I need to delete an item when the delete button next to it is clicked:
However, at the Item.vue level, I only have access to the item, but not the category. If I send the item to Vuex, I have no way of telling which category it belongs to. How do I get a reference to the category so that I can delete the item from it using Vuex?
I can think of two ways:
Add a parent reference back to the category for each item. This is undesirable not only because I'd have to massage the item data, but also because it introduces a circular reference that I'd rather not have to deal with in other parts of the app.
Emit an event from Item.vue up to Category.vue and let Category.vue handle the Vuex call for deletion. This way the category and the to-be-deleted item are both known.
Is there a better way of handling this kind of deletion?
I'd strongly recommend (2). In general, if you can create a component which takes props and emits events without having other side effects (API calls, Vuex mutations, etc.) that's usually the correct path. In this case, you can probably even push the event all the way back to the parent's parent.
Where shared state (Vuex) really helps is when you have two or more components which are far away from each other in the DOM tree. E.g. imagine a header with a count of the total items. That degree of spatial separation may exist in your app, but it doesn't in this simple example.
An additional benefit to emitting an event here is that you care more easily use tools like storybook without having to deal with any Vuex workarounds.
Personally, I'd go with 2 (emit an event from Item.vue up to Category.vue), but, since you asked about possibilities, there is a third way: passing a callback function.
Example:
Category.vue:
<template>
<div class="category">
<div class="header">{{ category.name }}</div>
<Item v-for="(item, index) in category.items" :item="item" :key="index"
:on-delete="deleteItem"/>
</div>
</template>
<script>
// ...
export default {
// ...
methods: {
deleteItem(i) {
console.log('cat', this.category.name, 'item', i)
//this.$store.commit("deleteItem", this.item);
}
}
};
</script>
Item.vue:
<template>
<div class="item">
<div class="name">{{ item.name }}</div>
<div class="delete" #click="() => onDelete(this.item)">✖</div>
</div>
</template>
<script>
export default {
props: {
item: { type: Object, required: true },
onDelete: { type: Function }
},
};
</script>
Updated sandbox here. Notice, in this case, the callback is onDelete.
If this were React, the callback was for sure a more idiomatic way. In Vue, as said, I'd argue in favor of emitting the event in the child and handling it in the parent (with v-on).

How to prevent multiple renders in Vue v-for lists

I've captured the current code I have here:
https://jsfiddle.net/prsauer/Lnhu2avp/71
The basic issue is that for each click on a list item every item's computeStyle is called -- I'd prefer for each click to only produce a single recompute of the style
<div id="editor">
<div v-for="item in dungeons" :key="item.isOpened">
<div v-on:click="clickedChest(item)" v-bind:style="computeChestStyle(item)">
{{ item.name }} {{ item.isOpened }}
</div>
</div>
</div>
var dgnData = [
{ name: "Lobby", isOpened: false },
{ name: "Side", isOpened: false },
];
new Vue({
el: '#editor',
data: { dungeons: dgnData },
computed: { },
methods: {
clickedChest: chest => {
chest.isOpened = !chest.isOpened;
console.log("##### Clicked chest", chest);
},
computeChestStyle:
item => {
console.log("computeStyle", item);
return item.isOpened ? "color: red" : "color: blue";
}
}
});
Function calls get re-evaluated on every update of the view. If you want results to be cached to only re-render as needed, you need a computed. That means you need to create a component for your item, and create the computed in the component.

Mix Dynamic class with data binders in Vue.js

So I have the following v-for in a HTML:
<ul v-for="(item, index) in openweathermap.list">
<li>{{item.dt_txt}}</li>
<li>{{item.weather[0].description}}</li>
<li>{{item.weather[0].id}}</li>
<li>{{item.main.temp}}°C</li>
</ul>
What I want to do is to add an icon to these information, like font awesome.
So I found these: <i class="owf owf-200"></i> This will serve me just right but the number must change dynamically. So the number is the {{item.weather[0].id}} in the v-for.
My question is this; How can I mix these two together?
I tried something like this <i class="owf owf-{{item.weather[0].id}}"></i>
but it obviously has wrong syntax.
Any help will be greatly appreciated!
You can use the v-bind:class - which allows you to append two strings, just like in Javascript. So the value should be 'owf owf-' + item.weather[0].id.
In the snippet, I've done that with dummy data and color changes for two different classes, but you should get the idea.
var app = new Vue({
el: "#app",
data:{
items: [
{
weather: [{ id: 200 }],
txt: "Some text"
},
{
weather: [{ id: 300 }],
txt: "Some other text"
}
]
}
});
.owf.owf-200 {
color: red;
}
.owf.owf-300 {
color: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.2/vue.min.js"></script>
<div id="app">
<template v-for="item in items">
<span v-bind:class="'owf owf-' + item.weather[0].id">
{{ item.txt }}
</span>
<br />
</template>
</div>

Leveraging the v-for loop for places outside the v-for

While looping on array of sales , i need to capture the object of which salesPerson === "bar" and print its sellValue outside the v-for block.
Of course i can't access the array in hard-coded way. i have to assume that the position of the object i'm looking for is random.
also, i can't add another loop on top of the one loop that already exist here. (v-for is a loop obviously).
i need way to do achieve it.
here is an example component:
<template>
<div id="app">
<!-- i need to print here the sellValue of 'bar' -->
<p v-for="(sell,index) in sales"
:key="index">{{sell.sellValue}}</p>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
sales: [
{
salesPerson: 'foo',
sellValue: 1
},
{
salesPerson: 'bar',
sellValue: 2
}
]
}
}
}
</script>
You can try using a custom HTML tag (not registered as a component for Vue), it's quite "ugly" but that is the only solution I could think of (beware of Vue's warnings if not disabled) :
<template>
<div id="app">
<uglyTag v-for="(sell,index) in sales" :key="index">
{{sell[ sell.findIndex( e=>e.salesPerson==="bar" ) ].sellValue}}
<p>{{ sell.sellValue }}</p>
</uglyTag>
</div>
</template>
Another solution would be to rethink the construction of your data so you could have (but still needs the uglyTag method) :
data(){
return {
salesTitle: 2,
sales: [
{
salesPerson: 'foo',
sellValue: 1
},
{
salesPerson: 'bar',
sellValue: 2
}
]
}
}
and
<template>
<div id="app">
<uglyTag v-for="(sell,index) in sales" :key="index">
{{ salesTitle }}
<p>{{ sell.sellValue }}</p>
</uglyTag>
</div>
</template>
Perhaps I didn't understand the question correctly, but you are still in the same scope of your component. Why don't you add a getter for the value you are interested in and display it where you want.
Vue.component('my-template', {
template: ' <div id="app">\
<!-- i need to print here the sellValue of \'bar\' -->\
<p>{{ saleValue }}</p>\
<p v-for="(sell,index) in sales" :key="index">{{sell.sellValue}}</p>\
</div>',
data: function() {
return {
sales: [{
salesPerson: 'foo',
sellValue: 1
}, {
salesPerson: 'bar',
sellValue: 2
}]
}
},
computed: {
saleValue: function() {
return this.sales.filter(function(val) {
return val.salesPerson === 'bar';
})[0].sellValue;
}
}
});
var vm = new Vue({
el: '#vm',
data: {}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.4.2/vue.min.js"></script>
<div id="vm">
<my-template></my-template>
</div>

Categories

Resources