Vue reads v-bind:class before computed - vue.js

I use v-for to display a list of images in my container. After this, I calculate the start and end positions of these images using $refs in a computed property.
The problem is that I want to use v-bind:class="" to animate them, but Vue reads this before the computed property is created (my guess).
<img ref="imageAnimate" class="image-parts"
v-for="(images, i) in finalImages"
:src="images.full_image_url"
:class="{animated: scroll >=animateHooks[i].animated}">
animateHooks() {
let hooks = [];
for (i=0; i < this.$refs.imageAnimate.length; i++) {
let img = {
start: this.$refs.imageAnimate[i].offsetTop,
end: this.$refs.imageAnimate[i].offsetTop + this.$refs.imageAnimate[i].offsetHeight,
animated: false
};
hooks.push(img);
}
return hooks;
}

Your problem is not that computed properties are calculated after the render cycle, but rather that you are using a computed property that tries to find an element that has not yet been rendered.
On the first render cycle, the component has not been rendered yet. It will loop through finalImages and try to set the class based on your computed property. Since nothing has been rendered yet, this.$refs is empty. Trying to get the length of that will result in "Cannot get property length of undefined". One solution would be to not loop through the refs, but instead loop through finalImages to create your computed property. I am not 100% sure if it will recalculate the computed property based on this.$refs though, but there is only one way to figure that out.
animateHooks() {
const refs = this.$refs.imageAnimate;
return this.finalImages.map((image, index) => {
const img = {
start: refs ? refs[i].offsetTop : 0,
end: refs ? (refs.offsetTop + refs[i].offsetHeight) : 0,
animated: false
};
return img;
});
}
You would be able to access the $refs one tick after the initial render (e.g. inside a this.$nextTick(() => { ... });), but this is of no use to you in a computed property.

Related

View not updating on Set changes in Vue.js

In Vue.js, I have this pieces of code (with Typescript Vue.js 2 and classed components):
toggle(id: string): void {
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
} else {
this.selectedIds.add(id);
}
}
and
get handledUsers() {
return this.users.map((user) => ({
...user,
selected: this.selectedIds.has(user._id),
}));
}
where selectedIds is a Set<string>.
The problem is that in Vue.js, Set is not modified as Array, so it seems that when I update the Set, Vue.js does not detects it as if I did a .splice() of an array. How can I make the view update?
You can add a key to the element or component that you want to be updated, and then change the key value. Changing the key value of an element or a component would make theme rerender.
Here is an example:
<div :key="refreshKey" >
some content...
</div>
Changing the value of refreshKey would cause this div to rerender and update it's content. So you should define a refreshKey in your components data, and then change it's value in your toggle method, for example from true to false.

Computed value based on component's created method

In my component I have acreated method where I make a request and then I want to use the data I get for a computed property.
computed: {
...mapState(["pages"]),
pageContent() {
let slug = this.$route.params.slug ? this.$route.params.slug : 'home' ;
let page = this.pages.find(page => page.slug == slug);
return page.content;
}
},
methods: {
...mapActions(['setPagesAction'])
},
created() {
this.setPagesAction();
}
The problem is that created is executed after the computed property pageContent is read, so it is undefined.
How can I make pageContent get the data from created ?
Solution
As you know, computed property will update itself when the dependent data changes. ie when this.pages is assigned in vuex. So
You have mainly three options.
Option 1
Set a variable so that till the time page content is being loaded, a loading spinner etc is being shown.
Option 2
If you are using vue-router, then use the beforeRouteEnter guard to load the data.
like here
Option 3
Load data initially (when app starts) and add it to vuex. (you can use vuex modules for seperating data store if needed)

You may have an infinite update loop in a component render function error - solution?

I am getting error "You may have an infinite update loop in a component render function." What should I do?
I have tried making the arrays a data value. Also, I have tried using a for loop. It seems like it's isolated in the first method.
data() {
return {
activeTab: 0,
uniqueLobs: []
}
},
methods: {
addDollarSymbol(val){
var newVal = "$" + val;
return newVal.replace(/<(?:.|\n)*?>/gm, ''); // Trims white space
},
removeDuplicateLOB(lineOfBusiness) {
// Removes duplicate LOBs for tabs
let incomingLobs = [];
lineOfBusiness.forEach((business) => {
incomingLobs.push(business.line_of_business.name);
});
this.uniqueLobs = [...new Set(incomingLobs)];
return this.uniqueLobs;
},
showSpecificLobData(activeTab){
//compares tab LOB to all incoming card data and shows only the LOB data for that specific tab
let activeTabData = [];
this.product_rate_card.forEach((product) => {
if (product.line_of_business.name == this.uniqueLobs[activeTab] ) {
activeTabData.push(product);
}
});
return activeTabData;
}
}
The 'loop' in this case refers to an infinite recursion rather than a for loop.
That warning is logged here:
https://github.com/vuejs/vue/blob/ff911c9ffef16c591b25df05cb2322ee737d13e0/src/core/observer/scheduler.js#L104
It may not be immediately obvious what most of that is doing but the key part of the code is the line if (circular[id] > MAX_UPDATE_COUNT) {, which is checking whether a particular watcher has been triggered more than 100 times.
When reactive data changes it will cause any components that depend on that data to be re-rendered. If the rendering process changes that same data then rendering will be triggered again. If the data never stabilizes then this will continue forever.
Here's a simple example of a component that triggers that warning:
<template>
<div>
{{ getNextCount() }}
</div>
</template>
<script>
export default {
data () {
return {
count: 1
}
},
methods: {
getNextCount () {
this.count++
return this.count
}
}
}
</script>
The template has a dependency on count but by calling getNextCount it will also change the value of count. When that value changes the component will be re-added to the rendering queue because a dependency has changed. It can never break out of this cycle because the value keeps changing.
I can't say for sure what is causing this problem in your component as you haven't posted enough code. However, it could be something like the line this.uniqueLobs = ..., assuming that is being called during rendering. In general I would suggest avoiding changing anything on this during the rendering phase. Rendering should be read-only. Generally you'd use computed properties for any derived data that you want to keep around.
Most times it’s as a result of how you're passing props to another component.
If it’s Vue.js 2, try using v-on:[variable-name].

How can I duplicate slots within a Vuejs render function?

I have a component which is passed content via a slot. I'm using a render function to output the content. The reason I'm using a render function is because I want to duplicate the content multiple times. When I use this code, everything works fine:
render(createElement){
return createElement('div', {}, this.$slots.default);
}
When I data that is being passed changes, the output changes as well.
However, since I want to duplicate the slot content, I'm now trying this:
return createElement(
'div', {},
[
createElement('div', { }, this.$slots.default),
createElement('div', { }, this.$slots.default)
]
)
Now the problem is, when the slot content changes from outside the component, only the content in the second div gets updated, the content in the first div stays the same..
Am I missing something here?
I can't explain why it happens. But the doc does mention that "VNodes Must Be Unique" in a render function. See https://v2.vuejs.org/v2/guide/render-function.html#Constraints.
Anyway, this is a VNode cloning function, which works, which I discovered from https://jingsam.github.io/2017/03/08/vnode-deep-clone.html.
function deepClone(vnodes, createElement) {
function cloneVNode(vnode) {
const clonedChildren = vnode.children && vnode
.children
.map(vnode => cloneVNode(vnode));
const cloned = createElement(vnode.tag, vnode.data, clonedChildren);
cloned.text = vnode.text;
cloned.isComment = vnode.isComment;
cloned.componentOptions = vnode.componentOptions;
cloned.elm = vnode.elm;
cloned.context = vnode.context;
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
return cloned;
}
const clonedVNodes = vnodes.map(vnode => cloneVNode(vnode))
return clonedVNodes;
}
How to use it:
render(createElement) {
return createElement('div', {}, [
createElement('div', {}, this.$slots.default),
createElement('div', {}, [...deepClone(this.$slots.default, createElement)])
])
}
Demo: https://jsfiddle.net/jacobgoh101/bz3e0o5m/
I found this SO question searching for a way to render the content of a slot multiple times like e.g. for a generic list that can have a template for the content of a list row, which is used for each item.
As of 2020 (in fact earlier) multiple rendering of a slot can be achieved using scoped slots. This is documented here:
https://v2.vuejs.org/v2/guide/components-slots.html#Other-Examples
The documentation says:
Slot props allow us to turn slots into reusable templates that can render different content based on input props
(obviously, if we can use the template to render different content based on props, we can also use it to render the same content)
The example given right there uses a template instead of a render function, but how to use scoped slots in a render function is fortunately also documented:
https://v2.vuejs.org/v2/guide/render-function.html#Slots

Can't copy props to model data and render it in Vue 2

I'm having this problem that looks a lot like a bug to me and I can't figure out how to solve it.
I created a generic list component and I tell it what child component it should insert in each item and what are the data it should pass to the child component. I'm passing everything as props along with the list (array) itself.
The problem is that I can't mutate the list props. So I try to copy it to model attribute. Otherwise I get this error:
Avoid mutating a prop directly since the value will be overwritten
whenever the parent component re-renders.....
And I can't just make it work in any of the lifecycle events. When I save the file and the hot-reloading reloads the page, the list is there, rendered, full of items. When I press F5 to manually reload the page, it is no more. Everything seems to be alright with code though
So in the parent component I'm doing this:
<List ref="link_list"
:list="this.foo.links" //this is array
:child="'LinkFormItem'" //this is the name of the child component
:section_name="'Links'"
:defaults="{content: '', type: 'facebook'}" />
In the List component I get this:
Template
<li class="" v-for="item in datalist">
<component :is="child" :item="item" ></component>
<button v-on:click='remove(index++)' type="button" name="button" class='red button postfix small'>Remove</button>
</li>
Script
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
name: 'search',
props: ['child', 'list', 'defaults','section_name'], //it is received as 'list'
components: {
Child1, Child2
},
data () {
return {
index: 0,
datalist: [] //i'm trying to copy 'list' to 'datalist'
}
},
beforeMount: function () {
// i'm copying it
for(var k in this.list){
this.datalist.push(this.list[k])
}
},
methods: {
//and here I should change it the way I want
add: function () {
this.datalist.push(this.defaults)
},
getList () {
return this.datalist;
},
remove(index){
var datalist = [];
for(var k in this.datalist){
if(k != index) datalist.push(this.datalist[k]);
}
this.datalist = datalist;
}
}
}
</script>
I don't see any problems with my Script. What is going on??
#edit
Ok, some console.log later I found out what the problem seems to be. The HTTP Request is really taking much longer than the mounting of the component to happen. But when it happens, it is not triggering the update in the list component. Nothing is re-rendered and the list is empty.
Workaround
well I realised the problem was related to propagation. I made a few changes in the code to asure the parent component was updating and changing the model value. but the child component (the list component) was not receiving it.
then I gave up trying to understand why and did the following:
1- used the ref in the child component to force an update in the child component with $forceUpdate and then I was assigning the props to the model in the beforeUpdate event. It was causing an error: an re-rendering loop. The update caused a new update and so on. We could just use a flag to stop it.
2- Instead I just called a child method directly:
this.$refs.link_list.updateList(data.links);
I hate this approach because I think it's way too explicit. But it did the job. Then in the child component a new method:
updateList(list){
this.datalist = list;
}
3- The other possibility that passed through my mind was emitting an event. But I didn't try, too complicated
You can simply do like as follows
data () {
return {
index: 0,
datalist: this.list // to copy props to internal component data
}
},
Once you done about you need to apply data manipulation opertions on new this.datalist , not on this.list
If you don't want to mutate the original list array you can do this:
data () {
return {
index: 0,
datalist: Object.assign({}, this.list)
}
}
I think this will help you