Getting ref of component in async v-for - vue.js

I have a list of items that don't get created until after an async call happens. I need to be able to get the getBoundingClientRect() of the first (or any) of the created items.
Take this code for instance:
<template>
<div v-if="loaded">
<div ref="myItems">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
<div v-else>
Loading...
</div>
</template>
<script>
import axios from 'axios';
export default {
data() {
return {
items: []
}
},
created() {
axios.get('/url/with/some/data.json').then((response) => {
this.items = response.data;
this.loaded = true;
}, (error) => {
console.log('unable to load items');
});
},
mounted() {
// $refs is empty here
console.log(this.$refs);
// this.$refs.myItems is undefined
}
};
</script>
So, I'm trying to access the myItems ref in the mounted() method, but the this.$refs is empty {} at this point. So, therefore, I tried using a component watch, and various other methods to determine when I can read the ref value, but have been unsuccessful.
Anyone able to lead me in the right direction?
As always, thanks again!!
UPDATES
Added a this.$watch in the mounted() method and the $refs still come back as {}. I then added the updated() method to the code, then was able to access $refs there and it seemed to work. But, I don't know if this is the correct solution?
How does vuejs normally handle something like dynamically moving a div to an on-screen position based on async data? This is similar to what I'm trying to do, grab an element on screen once it has been rendered first (if it even should be rendered at all based on the async data), then access it to do something with it (move it to a position)?

Instead of doing on this.$refs.myItems during mounted, you can do it after the axios promise returns the the response.
you also update items and loaded, sou if you want to use watch, you can use those

A little late, maybe it helps someone.
The problem is, you're using v-if, which means the element with ref="myItems" doesn't exist yet. In your code this only happens when Axios resolves i.e. this.loaded.
A better approach would be to use a v-show.
<template>
<div>
<div v-show="loaded">
<div ref="myItems">
<div v-if="loaded">
<div v-for="item in items">
<div>{{ item.name }}</div>
</div>
</div>
</div>
</div>
<div v-show="!loaded">
Loading...
</div>
</div>
</template>
The difference is that an element with v-show will always be rendered and remain in the DOM; v-show only toggles the display CSS property of the element.
https://v2.vuejs.org/v2/guide/conditional.html#v-show

Related

Adding Props to found components throw the mounted wrapper

I have a form that contains a selector reusable component like this
<template>
<div class="channelDetail" data-test="channelDetail">
<div class="row">
<BaseTypography class="label">{{ t('channel.detail.service') }}</BaseTypography>
<BaseSelector
v-model="serviceId"
data-test="serviceInput"
class="content"
:option="servicePicker.data?.data"
:class="serviceIdErrorMessage && 'input-error'"
/>
</div>
<div class="row">
<BaseTypography class="label">{{ t('channel.detail.title') }}</BaseTypography>
<BaseInput v-model="title" data-test="titleInput" class="content" :class="titleErrorMessage && 'input-error'" />
</div>
</div>
</template>
I'm going to test this form by using vue-test-utils and vitest.
I need to set option props from the script to the selector.
In my thought, this should be worked but not
it('test', async () => {
const wrapper=mount(MyForm,{})
wrapper.findComponent(BaseSelector).setProps({option:[...some options]})
---or
wrapper.find('[data-test="serviceInput"]').setProps({option:[...some options]})
---or ???
});
Could anyone help me to set the props into components in the mounted wrapper component?
The answer is that you should not do that. Because BaseSelector should have it's own tests in which you should test behavior changes through the setProps.
But if you can't do this for some reason, here what you can do:
Check the props passed to BaseSelector. They always depend on some reactive data (props, data, or computed)
Change those data in MyForm instead.
For example
// MyForm.vue
data() {
return {
servicePicker: {data: null}
}
}
// test.js
wrapper = mount(MyForm)
wrapper.setData({servicePicker: {data: [...some data]})
expect(wrapper.findComponent(BaseSelector)).toDoSomething()
But I suggest you to cover the behavior of BaseSelector in separate test by changing it's props or data. And then in the MyForm's test you should just check the passed props to BaseSelector
expect(wrapper.findComponent(BaseSelector).props('options')).toEqual(expected)

Use axios together with `nuxt/content` in a component's `fetch` method

I´m creating a static site using #nuxt/content and I´m trying to create some components that will be used from the markdown. One of those components needs to call external API to retrieve some data that is shown in the HTML.
These are the relevant snippets, trying to show a card with information from a GitHub repository:
blog_article.md
In <content-github-repository repo="jgsogo/qt-opengl-cube">this repository</content-github-repository> there is...
content/github/Repository.vue
<template>
<span class="">
<a :href="`https://github.com/${repo}`">
<slot>{{ repo }}</slot>
</a>
<div>{{ info.description }}</div>
</span>
</template>
<script>
export default {
props: {
repo: {
type: String,
require: true,
},
},
data: () => ({
info: null,
}),
async fetch() {
console.log(`Getting data for repo: ${this.repo}`);
this.info = (await this.$axios.get(`https://api.github.com/repos/${this.repo}`)).data;
},
};
</script>
The error I get is Cannot read properties of null (reading 'description'), it is like the this.info is not being populated before rendering the HTML... but it runs, because I get the output from the console.log. Maybe I misunderstood the docs about fetch, is there any other way to achieve this behavior?
Thanks!
Check out the docs here. From the looks of it, Nuxt can mount the component before fetch finishes, so you need to guard against that data not being set when accessing it in your component.
Something as simple as the following should work:
<template>
<span class="">
<a :href="`https://github.com/${repo}`">
<slot>{{ repo }}</slot>
</a>
<p v-if="$fetchState.pending">Fetching info...</p>
<p v-else-if="$fetchState.error">An error occurred :(</p>
<div v-else>{{ info.description }}</div>
</span>
</template>

Vue/Nuxt Component updates and rerenders without any data changing

I have two components, Carousel.vue and Showcase.vue. I'm testing them both in a page like this:
<template>
<main>
<app-showcase
before-focus-class="app-showcase__element--before-focus"
after-focus-class="app-showcase__element--after-focus"
>
<div class="test-showcase" v-for="n in 10" :key="n">
<img
class="u-image-cover-center"
:src="`https://picsum.photos/1000/1000?random=${n}`"
alt=""
/>
<div>Showcase numero {{ n }}</div>
</div>
</app-showcase>
<div class="u-layout--main u-margin-vertical--4">
<div>
<app-button #click="changeRequestTest(4)">Test Request 4</app-button>
<app-button #click="changeRequestTest(10)">Test Request 10</app-button>
<app-carousel
content-class="u-spread-horizontal--main"
center
:request-element="requestTest"
scroll-by="index"
#index-change="onIndexChange"
>
<template #header>
<h4>Placeholder images</h4>
<div>
Carousel heading h4, showing item number {{ index + 1 }}.
</div>
</template>
<template #default>
<img
:src="`https://picsum.photos/100/80?random=${n}`"
:data-carousel-item-name="n === 10 ? 'giovanni-rana' : ''"
alt=""
v-for="n in 20"
:key="n"
/>
</template>
</app-carousel>
</div>
</div>
</main>
</template>
<script>
export default {
data() {
return {
n1: 20,
n2: 20,
isAnimationOver: false,
index: 0,
requestTest: null,
};
},
methods: {
changeRequestTest(n) {
this.requestTest = n;
},
onIndexChange(e) {
this.requestTest = e;
},
logTest(msg = "hello bro") {
console.log(msg);
},
logElement(e) {
console.log(e);
},
},
created() {
this.requestTest = this.$route.query.hero;
},
};
</script>
Both components use a parameter called index, which basically registers the position (in the children array) of the element that is being focused/shown/highlighted.
...
data() {
return {
index: 0,
showBackButton: true,
showForwardButton: true,
lockScroll: false,
};
},
...
Carousel actually has a prop, requestElement, that allows the carousel to be scrolled to a specific element (via number or element name), and is used in combination with the event "index-change" to update the value in the parent component.
Now that's the problem: every time requestElement updates (there is a watcher in Carousel.vue to follow that), the Showcase component rerenders.
That's a huge problem because the Showcase component applies specific classes on the mounted hook, and on rerender, all that classes are lost. I solved the problem by calling my class-applying-methods in the update hook, but I don't particularly like this performance-wise.
Basically, if I remove any reference to requestElement, everything works as intended, but as soon as that value changes, the showcase rerenders completely. I tried to change props/params names, to use dedicated functions instead of inline scripts, nothing works. No common value is shared, neither via props or vuex, so this makes it even weirder.
Any idea why this happens?
EDIT: I tried the solution from this question, as expected no change was detected in Showcase component, but it still gets updated when requestElement is changed in Carousel.

Vue/Vuex Accessing Objects elements

Hi I am getting confused as to how I can access some data within my Object. I am using Vuex and I have a standard page. Here, I use a getter to obtain the Payment
Object and pass it to a component.
<template>
<div>
<div class="container">
<div class="row">
<div class="col-12 flex">
<payment-details :payment="payment"></payment-details>
</div>
</div>
</div>
</div>
</template>
<script>
import {
PaymentDetails
} from "#/components/Payment";
export default {
components: {
'payment-details': PaymentDetails
},
created () {
if (!this.payment.paymentID) {
this.$store.dispatch('paymentById', this.$route.params['id'])
}
},
computed: {
payment () {
return this.$store.getters.paymentById(this.$route.params['id'])
}
}
}
</script>
Now within the component, within the template, I can do something like this
<template>
<div v-if="payment">
<div class="row">
<div class="col-12">
<h3 class="h-100">{{ payment.details }}</h3>
</div>
</div>
</div>
</template>
This all works fine. However, within this component, I need to access some elements of the payment object. This is where I get confused, if I create a mounted or created hook and do this
created() {
console.log(this.payment)
}
I always get an Observer object. If I try accessing an element from this Object e.g.
created() {
console.log(this.payment.details)
}
I get undefined. I basically have a slideshow I am creating in a mounted hook. I need to push some items contained within this Object onto the slideshow array.
So how can I actually get access to the elements of this Object?
Thanks
You should use watcher on your vuex object.
Here is link to documentation https://v2.vuejs.org/v2/guide/computed.html#Computed-vs-Watched-Property
Most probably your this.payment.details is instantiated after your created method was called.
Move your code from created method to:
export default {
watch: {
payment: function (val) {
console.log('-------- this is this.payment.details:');
console.log(val.details);
},
...
Yes it will gave you of undefined because in your props you declare only a payment object alone not like this one below.
payment : {
details: '',
etc: ''
}
But it will still works when you use this payment data in your component, it's like it only gives you an error something like 'calling details on null' like that. I prefer to put condition first if payment has already data before you call in your component. Like below
<div v-if="payment.details">{{payment.details}}</div>

Undefined props in Vue js child component. But only in its script

I have just started using Vue and experienced some unexpected behavior. On passing props from a parent to child component, I was able to access the prop in the child's template, but not the child's script. However, when I used the v-if directive in the parents template (master div), I was able to access the prop in both the child script and child template. I would be grateful for some explanation here, is there a better was of structuring this code? See below code. Thanks.
Parent Component:
<template>
<div v-if="message">
<p>
{{ message.body }}
</p>
<answers :message="message" ></answers>
</div>
</template>
<script>
import Answers from './Answers';
export default {
components: {
answers: Answers
},
data(){
return {
message:""
}
},
created() {
axios.get('/message/'+this.$route.params.id)
.then(response => this.message = response.data.message);
}
}
</script>
Child Component
<template>
<div class="">
<h1>{{ message.id }}</h1> // works in both cases
<ul>
<li v-for="answer in answers" :key="answer.id">
<span>{{ answer.body }}</span>
</li>
</ul>
</div>
</template>
<script>
export default{
props:['message'],
data(){
return {
answers:[]
}
},
created(){
axios.get('/answers/'+this.message.id) //only worls with v-if in parent template wrapper
.then(response => this.answers = response.data.answers);
}
}
</script>
this.message.id only works with v-if because sometimes message is not an object.
The call that you are making in your parent component that retrieves the message object is asynchronous. That means the call is not finished before your child component loads. So when your child component loads, message="". That is not an object with an id property. When message="" and you try to execute this.message.id you get an error because there is no id property of string.
You could continue to use v-if, which is probably best, or prevent the ajax call in your child component from executing when message is not an object while moving it to updated.