Vue v-if not updating - javascript

Here's a part of my Vue template:
<ul>
<li v-for="friend in user.friends">
<span v-if="checkIfNew(friend.id)"> ... </span>
</li>
</ul>
Basically, friends is an array of objects, and I want to display the span element, if we have new messages from any of them. That's what checkIfNew() does. It checks whether the friend's id is in the unreadIds array (it contains ids of friends, who sent a message)
This array is being updated in a different method, but, here's the problem: v-if doesn't react to the changes.
Here's a part of the script section:
data(){
return {
unreadIds: []
}
},
methods:{
checkIfNew(id){
if(id in this.unreadIds) return true
else return false
}
},
computed:{
user(){
return this.$store.getters.user;
}
}
Does anyone have any ideas what am I doing wrong?

id in this.unreadIds doesn't do what you think it does. See the docs for the in operator. It will return true if the object has the value as a property. So if this.unreadIds had 3 items and you had an id of 1, then the in operator will return true because 1 is a property of the array (this.unreadIds[1] exists).
Instead, you should use includes.
Try rewriting your method like this:
checkIfNew(id) {
return this.unreadIds.includes(id);
}
Here's a working version of the component that updates the list without the Vuex store code:
<ul>
<li v-for="friend in user.friends" :key="friend.ids">
<span v-if="checkIfNew(friend.id)">{{ friend.name }}</span>
</li>
</ul>
export default {
data() {
return {
unreadIds: [5],
user: {
friends: [
{ id: 1, name: 'joe' },
{ id: 5, name: 'nancy' },
{ id: 9, name: 'sue' },
],
},
};
},
created() {
setTimeout(() => this.unreadIds.push(9), 2000);
},
methods: {
checkIfNew(id) {
return this.unreadIds.includes(id);
},
},
};
Just to prove here that David was correct all along, I put this code in a runable snippet, and cannot find any fault...
Vue.config.productionTip = false
new Vue({
el: '#app',
data() {
return {
unreadIds: [],
};
},
created() {
setTimeout(() => this.unreadIds.push(9), 2000);
},
methods: {
checkIfNew(id) {
// if(id in this.unreadIds) return true
// else return false
return this.unreadIds.includes(id);
},
},
computed: {
user(){
return {
friends: [
{ id: 1, name: 'joe' },
{ id: 5, name: 'nancy' },
{ id: 9, name: 'sue' }
]
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<div id="app">
<ul>
<li v-for="friend in user.friends" >
<span v-if="checkIfNew(friend.id)">{{ friend.name }}</span>
</li>
</ul>
</div>
The sample above is a bit closer to the original question
user is a computed not data
unreadIds is initially empty
Upvote from me!

You want to leverage Vue's reactivity system as the previous answers do not.. they will eventually open you up to inexplicable problems that aren't easily debuggable.
Rather than invoking a method in a v-for (which I guarantee will become problematic for you in the future), you should declare a computed list that contains (or does not contain) the items you want rendered, something like this:
data(){
return {
unreadIds: []
}
},
computed:{
user(){
return this.$store.getters.user;
},
NewFriends() {
return this.user.friends.filter(friend => this.unreadIds.includes(friend.id));
}
}
Your markup would then just be:
<ul>
<li v-for="friend in NewFriends">
<span > ... </span>
</li>
</ul>
And Vue's reactivity system would handle any changes to data dependencies for NewFriends.
You don't want to ever use method calls in a template because method calls are only guaranteed to be invoked once (the same applies to functions that come from computed methods...).
If you find yourself trying to trigger re-renders and dependency checks manually, you will want to re-think your design.
ETA: The only time you will ever want to invoke a function of any kind in a template is to respond to events.

I'll have a stab at it - someone posted this earlier and deleted it. You need checkIfNew() to be a computed not a method for reactivity in the template.
Since you need to pass in the id, the computed needs to return a function.
data(){
return {
unreadIds: []
}
},
computed:{
user(){
return this.$store.getters.user;
},
checkIfNew(){
return (id) => {
return this.unreadIds.includes(id);
}
}
}
As David Weldon says, you should ideally change the array immutably - and probably why ohgodwhy asked the original question.

Related

How to pass input value data from Vue component to root element

I want to post json data from Vue to php, but I'm struggling to find a way to pass input value data from Vue component to root element.
When I call method submitProduct, alert message simply gives me 'undefined'.
I had to strip my original code because of that stupid post balance policy.
What's wrong here?
var productForm = Vue.createApp ({
methods:{
submitProduct: function(){
alert(JSON.stringify(productForm.product))
this.postData(productForm.product)
},
postData: function(p){
fetch('mysql_postdata.php', {
method: 'POST',
mode: "same-origin",
credentials: "same-origin",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({p:p})
//body: 'd='+d
})
}
}
})
productForm.component('custom-form', {
props: ["value"],
template: `
<label>SKU<input type="text" :value=this.product.sku></label>
<label>Name<input type="text" :value=this.product.name></label>
<label>Price<input type="text" :value=this.product.price></label>
` ,
data: function() {
return {
product: {
sku: 0,
name: 'asdf',
price: 0
},
options: ['Size', 'Weight', 'Dimensions'],
selected: 'Size'
}
}
})
productForm.component('status-bar', {
props: ['info'],
template: '<p>{{ selected }}</p>'
})
const vm = productForm.mount('#product_form')
The product state belongs to the custom-form component so the root app cannot access the state.
If you trying to create a form and get the input from the form, you need to do 1 of this:
Lift the state to the root and pass down the custom-form and bind an event to listen to the state change docs here. (only do this if the custom-form component is not deep down in the component tree)
Using the state management store like Vuex to share the state within the app (in case the child component is deep down in the tree you have to pass the state down so many levels, using store management will solve that). docs here. If your app is really small consider the provide/inject API.
Another choice is using the provide/inject API (similar to the context provider in react).
First of all after 3.5 days of struggling to try to understand Vue I came with tested successfull result. I want to thank you and anybody else, who helped me to understand basics principles in Vue! :)
Please see link below...
https://jsfiddle.net/e2mnh4xa/
P.S. And yes! You are right about rearranging 'custom-form' tag. :)
html code:
<div id="product_form" v-cloak>
<custom-form>
</custom-form>
</div>
js code:
var productForm = Vue.createApp ({})
productForm.component('custom-form', {
props: {
modelValue: {
type: String,
default: ''
},
},
components: ['status-bar'],
template: `
<button v-on:click="submitProduct">Save</button>
<label>SKU<input type="text" v-model="this.product.sku"></label>
<label>Name<input type="text" v-model="this.product.name"></label>
<label>Price<input type="text" v-model="this.product.price"></label>
` ,
data: function() {
return {
product: {
sku: 0,
name: 'asdf',
price: 0,
},
options: ['Size', 'Weight', 'Dimensions'],
selected: 'Size',
outputData: ''
}
},
computed:{
model:{
get(){ return this.modelValue },
set(v){ this.$emit('update:modelValue',v)}
}
},
methods:{
submitProduct: function(){
alert (this.showData());
return this.postData(this.product)
},
showData: function(){
console.log(this.product.sku)
return JSON.stringify(this.product)
}
}
})
const vm = productForm.mount('#product_form')

Variable is empty in child component, VueJs

When an event is triggered, I call a function that fills a variable and opens a modal from a child component. But, there, my new variable is empty, and if I close/re-open modal I have the data, so the data is loaded after.
I tried to load that child component after I have data, but no good till now.
Parent
<p class="open-modal-button" #click="openUpdatesModal">
<i class="fas fa-sync"></i>
Check for updates
</p>
<check-for-updates-modal
v-if="check_for_updates_values.account_name != ''"
:modalUpdatesOpen="modalUpdatesOpen"
:check_for_updates_values="check_for_updates_values"
/>
data() {
return {
//code....
check_for_updates_values: [],
modalUpdatesOpen: false,
};
}
openUpdatesModal() {
this.temporaryChecker();
},
temporaryChecker() {
this.check_for_updates_values.account_name = this.account_name;
this.check_for_updates_values.company_registration_number = this.company_registration_number;
this.check_for_updates_values.registered_address = this.registered_address;
this.modalUpdatesOpen = !this.modalUpdatesOpen;
},
Child
<b-col>{{check_for_updates_values.account_name}}</b-col>
props: [
"modalUpdatesOpen",
"check_for_updates_values",
],
watch: {
modalUpdatesOpen() {
this.checkForChanges();
this.$bvModal.show("modal-check-for-updates");
},
},
If you really initialize check_for_updates_values as an array, this is the problem. According to your usage, it should be an object.
Also, be careful to explicitly initialize every existing key on the object, or Vue won't be able to register them for reactivity watchers! That means if you have a data object empty foo: {}, any time you add a property, it won't refresh the vue foo.label = 'test' // won't render properly.
data() {
return {
check_for_updates_values: {
account_name: null,
company_registration_number: null,
registered_address: null,
},
};
}

VueJS deep watcher - specific property on multiple objects

The Problem
I have a "products" array with multiple objects. Each product object contains the property "price". I want to watch this property in each product for possible changes. I am using this to calculate a commission price when the user changes the price in an input box.
My products array looks like this;
[
0: {
name: ...,
price: ...,
commission: ...,
},
1: {
name: ...,
price: ...,
commission: ...,
},
2: {
name: ...,
price: ...,
commission: ...,
},
...
...
...
]
My code
I've tried this, but it doesn't detect any changes except for when the products are first loaded;
watch : {
// Watch for changes in the product price, in order to calculate final price with commission
'products.price': {
handler: function (after, before) {
console.log('The price changed!');
},
deep : true
}
},
The products are loaded like this;
mounted: async function () {
this.products = await this.apiRequest('event/1/products').then(function (products) {
// Attach reactive properties 'delete' & 'chosen' to all products so these can be toggled in real time
for (let product of products) {
console.log(product.absorb);
Vue.set(product, 'delete', false);
Vue.set(product, 'chosen', product.absorb);
}
console.log(products);
return products;
})
}
Other questions I've looked at
Vue.js watching deep properties
This one is trying to watch a property that does not yet exist.
VueJs watching deep changes in object
This one is watching for changes in another component.
You can't really deep-watch products.price, because price is a property of individual product, not the products array.
Declarative watchers are problematic with arrays, if you attempt to use an index in the watch expression, e.g products[0].price, you get a warning from Vue
[Vue warn]: Failed watching path: “products[0].price”. Watcher only accepts simple dot-delimited paths. For full control, use a function instead.
What this means is you can use a programmatic watch with a function, but it's not explained all that well.
Here is one way to do it in your scenario
<script>
export default {
name: "Products",
data() {
return {
products: []
};
},
mounted: async function() {
this.products = await this.apiRequest('event/1/products')...
console.log("After assigning to this.products", this.products);
// Add watchers here, using a common handler
this.products.forEach(p => this.$watch(() => p.price, this.onPriceChanged) );
// Simulate a change
setTimeout(() => {
console.log("Changing price");
this.products[0].price= 100;
}, 1000);
},
methods: {
onPriceChanged(after, before) {
console.log(before, after);
}
}
};
</script>
Here is my test Codesandbox (I use color instead of price because there's no price in the test api)

Filter array by nested value that meets certain condition in Vue

I am trying to filter an Array that contains nested array of Objects. I would like for the v-for to only show those objects that meet certain condition. I have created a JSfiddle Click Here
The part that confuses me is that each enagagement could have 1 object or 3 objects, and I don't know how to check value conditions for each nested object.
I want to only show Engagements with questions that are not answered. I am using a boolean value to represent whether the question is answered or not.
This is the v-for
<div id="app">
<h2>Engagements:</h2>
<div>
<div v-for="(engagment, index) in filteredQuestions" :key="index">
<li v-for="question in engagment.questions" :key="question.id">
<span>{{ question.id }},</span>
<span> {{ question.question}} </span>
<span><input type="checkbox" v-model="question.answered"></span>
</li>
</div>
</div>
</div>
This is the Script and Data
new Vue({
el: "#app",
data: {
engagements: [
{
id: 1,
text: "1040",
questions: [
{
id: 1,
question: 'this is a question',
answered: 0
},
{
id: 2,
question: 'this is a question',
answered: 1
},
]
},
{
id: 2,
text: "1040",
questions: [
{
id: 3,
question: 'this is a question',
answered: 0
},
]
},
]
},
computed: {
filteredQuestions() {
const engagement = this.engagements.filter((engagement) => {
return engagement.questions.filter((question) => question.answered === 0)
})
return engagement
}
}
})
Currently no matter how I format the filteredQuestions method it will either render the entire list or show nothing. Please view the jsfiddle I included at the top of this post!
You're filtering the engagements based on them having 1 or more unanswered questions, but the v-for is still rendering all questions inside those engagements.
WRONG: Add v-if="question.answered==0" to the <li> element to only show unanswered questions. (This is wrong practice, I found out: see lint error here. You should not use v-if and v-for on the same element.)
CORRECT:
In this case extend your filteredQuestions computed value function to only return questions without answers. (Now you are just filtering the engagements based on that, but still returning all of the questions.)
Your computed value function could be:
filteredQuestions() {
return this.engagements
// Return a modified copy of engagements..
.map((engagement) => {
// ..with all answered questions filtered out..
engagement.questions = engagement.questions.filter((question) => question.answered === 0);
return engagement;
})
// ..and only return engagements that have (unanswered) questions left
.filter((engagement) => engagement.questions.length !== 0);
}
The above option not work if you are trying to find the first level's array and nested item in array
For example engagements's name and questions sub item name because the filter will do the last match
If you are trying to find matches on nested array for example on names should do the next code
return this.content.filter((sub) => {
//for save the status
let show = false
//find in nested: themes
sub.Themes = sub.Themes.filter((theme) => {
if (reg.test(theme.name)) {
show = true
return true
}
return false
})
//if was finded match in themes show the subject or if the subject name match too
if (show === true || reg.test(sub.name)) {
return true
}
return false
})

Vue.js + Vuex: How to mutate nested item state?

let's say I have following tree:
[
{
name: 'asd',
is_whatever: true,
children: [
{
name: 'asd',
is_whatever: false,
children: [],
},
],
},
],
The tree is stored in a module via Vuex under key 'tree' and looped through with following recursive component called 'recursive-item':
<li class="recursive-item" v-for="item in tree">
{{ item.name }}
<div v-if="item.is_whatever">on</div>
<div v-else>off</div>
<ul v-if="tree.children.length">
<recursive-item :tree="item.children"></recursive-item>
</ul>
</li>
Now i want to toggle item's property 'is_whatever', so i attach a listener
<div v-if="item.is_whatever"
#click="item.is_whatever = !item.is_whatever">on</div>
<div v-else>off</div>
When i click it, it works, but emits following
"Error: [vuex] Do not mutate vuex store state outside mutation handlers."
[vuex] Do not mutate vuex store state outside mutation handlers.
How am I supposed to implement it without this error? I can see no way how to dispatch an action or emit event to the top of the tree because it's nested and recursive, so I haven't got a path to the specific item, right?
After consulting with some other devs later that evening we came with few ways how to achieve it. Because the data are nested in a tree and I access the nodes in recursive manner, I need to either get the path to the specific node, so for example pass the index of a node as a property, then add the child index while repeating that in every node recursively, or pass just the id of a node and then run the recursive loop in the action in order to toggle its properties.
More optimal solution could be flattening the data structure, hence avoiding the need for a recursion. The node would be then accessible directly via an id.
Right now you're changing the state object directly by calling item.is_whatever = !item.is_whatever, what you need to do is create a mutation function that will execute that operation for you to guarantee proper reactivity:
const store = new Vuex.Store({
state: { /* Your state */ },
mutations: {
changeWhatever (state, item) {
const itemInState = findItemInState(state, item); // You'll need to implement this function
itemInState.is_whatever = !item.is_whatever
}
}
})
Then you need to expose this.$store.commit('changeWhatever', item) as an action in your view that'll be trigger by the click.
There is a debatable solution, but I'll just leave it here.
State:
state: {
nestedObject: {
foo: {
bar: 0
}
}
}
There is Vuex mutation:
mutateNestedObject(state, payload) {
const { callback } = payload;
callback(state.nestedObject);
},
And this is an example of use in a component:
this.$store.commit('mutateNestedObject', {
callback: (nestedObject) => {
nestedObject.foo.bar = 1;
},
});

Categories