Loop variable in v-for is undefined - vue.js

How can the loop variable in a v-for loop be undefined? If it was undefined, why is the loop entered at all?
The error I get is TypeError: Cannot read property 'title' of undefined.
This is the code in the template:
<!-- loop through measurements -->
<div class="test-container" v-for="(measurement, m) in printData.measurements" :key="m">
<div class="gray-ribbon page-break" v-if="testId">
{{ measurement.title }}
<span class="ribbon-sub-title">{{ testNoOfCount(m) }}</span>
</div>
...
</div>
The debugger shows this:
So the loop index variable is defined (= 0), we are in the first iteration.
pintData.measurements is an array of 2 objects defined in the component's props, both have a title property. And if I let the debugger continue, the breakpoint is hit again with m = 1 and measurement still being undefined...
This is printData as shown in the Vue inspector:
Vue version 3.2.26, component is using the Options API.

Found it.
The parent component (A) needs to load the measurement info from a REST server; the component with the error (B) was initialised too early, before the data was available. A set B's properties with a computed property that is an empty array until the server response is received. I do not know why Vue enters the loop at all, but preventing B's loading with a v-if until the server response is there solved the problem.

Related

Getting element (button) data attribute with vue3

I have the following html div. The {{ }} represent liquid syntax and it renders server side.
<div class="two columns">
<button
data-value='{{ value }}'
class="button-selection"
:class="selectionButtonClass($event)"
#click="selectionButton($event)"
>
<span class="text">{{ value }}</span>
</button>
</div>
In a vue 3 instance I have the following method in the same page.
selectionButtonClass(el) {
console.log('checking val');
console.log(el);
}
My goal is to set a conditional class in the selectionButton method but I can't get the element to get the data attribute. The above appears in the console log as undefined. However the #click does show the event obviously it's recognize the onclick but not the class method check.
$event is only available to event handlers. #click="selectionButton($event)" defines an inline event handler, while :class="selectionButtonClass($event)" is not an event handler.
To get the element, you need to add a ref attribute to the <button>:
<button
ref="selectionButton"
data-value='{{ value }}'
class="button-selection"
:class="selectionButtonClass($event)"
#click="selectionButton($event)"
>
And access it by this.$refs.selectionButton, assuming you are using the options API. However, the ref is available only after the component is mounted. Thus you need to handle the case where the ref is null.
More on template refs: https://vuejs.org/guide/essentials/template-refs.html
Since you are using server side rendering, I think it would be better to render the value as a parameter of the selectionButton function on the server side.

Changing value of outer variable used in props after emitted listener from component disables v-model listener effects in Vue.js

Considering following HTML code:
<div id="app">
<comp :is_checked="is_checked" v-on:ch="function(x){is_checked_2=x}"></comp>
<p>{{ is_checked_2 }}</p>
</div>
<script src="app.js"></script>
and app.js:
var tm = `<div>
<input type="checkbox" v-model="is_checked"
v-on:change="$emit('ch',is_checked)"
>{{ is_checked }}
</div>`
Vue.component('comp', {
template: tm,
props: ["is_checked"]
})
new Vue({
el: "#app",
data: function() {
return {
is_checked: null,
is_checked_2: null
};
}
});
If we replace is_checked_2=x by console.log(x) in v-on:ch="function(x){...}, everything works correct and v-model is changing is_checked when input checkbox value changes.
Also if we don't send the value of variable by props and define it locally in component, everything works correct.
It seems that changing the parent Vue's object variable is
regenerating the whole HTML of component where the value of variable
is sent by props and the variable is reset there immediately after
firing the event inside template. It is causing that functions
triggered by events don't change component's variables.
Is it a bug in Vue.js?
To make it more clear, the behavior is following: changing whatever Vue parent value that is not bound and however related to the component (no props, no slots) results at resetting all values in the component that are bound by props. Following only occurs if we reactivelly write to parent/main HTML using modified parent variable, we can achieve that by placing {{ .... }} there. This seems to be the bug.
Example: Vue has variables a and b. We place {{ a }} to the main code. The value of variable b is sent by props to component and matched to variable c. With the time we are changing that value of variable c inside the component. In one moment we decide to change value of a and it results by "forgetting" current state of c and it is reset to initial state by invoked props.
To summarize: the bug itself gets stuck that props should be reactivelly invoked only if corresponding parent variable is changed and not if whatever parent variable changes and at the same time they modify HTML code.
This issue doesn't have nothing to do with event listeners neither events, because it is possible to replicate it without using them.
Conclusion:
{{ is_checked_2 }} or {{ '',is_checked_2 }} or {{ '',console.log(is_checked_2) }} after changing value of is_checked_2 is causing rerendering the whole top Vue component having is_checked as variable and it is resetting child component variable, because it has the same name. The solution is never using the same name for child and parent component variables bound by props. This is issue of Vue/props architecture how it was designed.
I don't think it's a bug.
You're most likely running into a race condition where your change event gets executed before or in place of the internally attached one (hence the seemingly nullified data binding).
Because v-model is essentially just a syntactic sugar for updating data on user input events. Quoting the docs:
v-model internally uses different properties and emits different events for different input elements:
text and textarea elements use value property and input event;
checkboxes and radiobuttons use checked property and change event;
select fields use value as a prop and change as an event.
You might also want to see "Customizing Component v-model".

Pass for loop item to prop

How do you pass the item instance from the foreach loop as a prop?
<div v-for="n in parentArray">
<blog-card prop="{{ n.content }}"></blog-card>
</div>
When I run this I get an error
(Emitted value instead of an instance of Error)
Can this be done without rebinding the item to the parent component?
With Vue 2 you don't use interpolation in attributes, you use the attribute binding syntax.
<blog-card v-bind:prop="n.content"></blog-card>
Or the shortcut
<blog-card :prop="n.content"></blog-card>

Toggle in loop?

I wish to toggle (show/hide) a list when clicking on a title, but cant get the following to work
I have this:
<!-- Title -->
<div v-for="(subitem, index) in item" v-if="index === 0" #click="toggle(subitem)">
{{subitem.system_name}} - ({{item.length}})
</div>
<!-- All title items that should expand on click "Title" -->
<div v-if="subitem.clicked">
<p>{{subitem.system_name}}</p>
</div>
When clicking on the im triggering a toggle function called toggle, that sets a property on the item "clicked" to true or false (I should mention that this property does not already exist on the object, and I haven't got the possiblity add it, as we get the JSON from an API)
The toggle function is this:
toggle: function (data) {
data.clicked = !data.clicked;
},
Now, when I do this above, I get an error saying: "Property or method "subitem" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option"
Im guessing I get this because the "clicked" property doesnt exist in the object... So how do I work around this? Can't see any real solution ?
You initialize subitem in the v-for as a single item in the loop, but you are using it outside the element which has v-for loop on it. That's the reason you get that warning:
Property or method "subitem" is not defined on the instance but referenced during render.
Make sure to declare reactive data properties in the data option"
So move the div you want to toggle inside the div which has the v-for loop on it
<!-- Title -->
<div v-for="(subitem, index) in item" v-if="index === 0" #click="toggle(subitem)">
{{subitem.system_name}} - ({{item.length}})
<!-- All title items that should expand on click "Title" -->
<div v-if="subitem.clicked">
<p>{{subitem.system_name}}</p>
</div>
</div>
And coming yo the 2nd issue, as you mention the subitem obj does not have clicked property when you fetch the json from api.
You cannot add root level reactive properties after the vue instance is created.
Since you want to toggle the appeance of the div based on the property clicked which is not available at the time vue instance is created you should use vm.$set() to add reactive properties or Object.assign() to add properties to existing object. See Reactivity in depth
So in your case
toggle: function (data) {
if(data.hasOwnProperty('clicked')){
data.clicked = !data.clicked;
}else{
//since its the first time , set the value pf clicked to true to show the subitem
data = Object.assign({}, data, {clicked: true});
}
},

Vue.js prevent re-render?

I have a template in Vue.js that looks like this:
<div class="filter">
<div class="checkbox" v-for="(value, index) in range" :key="index">
<span class="hotels">{{ count(index) }}</span>
</div><!-- /.checkbox -->
</div><<!-- /.filter -->
The count method is important here. It triggers twice and also seems to be the cause of the second trigger and re-render.
This is my method:
count(value) {
console.log('count');
// Get the filtered AJAX data.
let hotels = this.allHotels;
const filters = Object.entries(this.filters);
for (let i = 0; i < filters.length; i += 1) {
// Get the Object from the array(index 0 being the name).
const filter = filters[i][1];
// Break the v-model reference by creating a shallow copy.
const values = filter.values.slice(0);
// Merge any selected checkbox values with the one we are currently iterating.
if (!values.includes(value)) values.push(Number(value));
// Check if the current filter has selected checkboxes and filter if it does.
hotels = filter.function(hotels, values);
}
return hotels.length;
}
Whenever I everything except the console.log() it works fine. Whenever I leave in the rest of the code the entire template seems to re-trigger. This seems strange because I am leaving the data in the template and the computed properties that the template relies on alone.
Is there any way to debug what is causing the second re-render?
Entire component:
https://gist.github.com/stephan-v/a5fd0b6bae854276666536da445bbf86
Edit
My components count function has a computed property in it called allHotels. This property is being set as a prop.
On the very first console.log it seems empty and then straight away it will get set causing the re-render. I was hoping that because this prop is coming from the backend it would take it as the initial value but this is not the case.
Is there anything to overcome this and prevent a re-render? There is no need for a re-render if Vue would take the prop data from the backend as initial data straight away.
Hopefully it is clear what I mean by this.
Replication:
Somehow I can't seem to replicate this, I would expect this to also work like my code and show an empty title on the first pass with a console.log:
https://jsfiddle.net/cckLd9te/123/
Perhaps because my prop data that I am passing into my component is a pretty big json_encoded block it takes time for Vue.js to interpret the data?