How to manually call query (e.g. on click) using Vue Apollo Composible? - composition

The default behavior is that useQuery is loaded immediately with component setup.
I want to trigger the query on some event like click.
How to do it?

i searched for it a lot too, but it is writtin in their documentation vue apollo v4e
import { useQuery, useResult } from '#vue/apollo-composable'
import gql from 'graphql-tag'
export default {
setup () {
const { result, loading, error, refetch } = useQuery(<query>)
const users = useResult(result)
return {
users,
loading,
error,
refetch,
}
},
}
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<ul v-else-if="users">
<li v-for="user of users" :key="user.id">
{{ user.firstname }} {{ user.lastname }}
</li>
<button #click="refetch()">Refresh</button>
</ul>
</template>
just add the "refetch" to the useQuery line and call it from the button click event.
EDIT: You could disable the query, so it wont trigger on component setup and on button click you could enable the query and refetch it.
Best Regards

Related

Vue Testing Library with NaiveUI

I'm using Vue 3, NaiveUI, Vitest + Vue Testing Library and got to the issue with testing component toggle on button click and conditional rendering.
Component TSample:
<template>
<n-button role="test" #click="show = !show" text size="large" type="primary">
<div data-testid="visible" v-if="show">visible</div>
<div data-testid="hidden" v-else>hidden</div>
</n-button>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { NButton } from 'naive-ui'
export default defineComponent({
name: 'TSample',
components: {
NButton
},
setup() {
const show = ref(true)
return {
show
}
}
})
</script>
The test case I have:
import { render, waitFor } from '#testing-library/vue'
import TSample from './TSample.vue'
import userEvent from '#testing-library/user-event'
describe('Tests TSample component', () => {
it('toggles between visible and hidden text inside the button', async () => {
const user = userEvent.setup()
const { getByText, queryByText, getByRole } = render(TSample)
expect(getByRole('test')).toBeInTheDocument()
expect(getByText(/visible/i)).toBeInTheDocument()
expect(queryByText('hidden')).not.toBeInTheDocument()
await user.click(getByRole('test'))
await waitFor(() => expect(queryByText(/hidden/i)).toBeInTheDocument()) <-- fails
})
})
The error:
<transition-stub />
<span
class="n-button__content"
>
<div
data-testid="visible"
>
visible
</div>
</span>
</button>
</div>
</body>
</html>...Error: expect(received).toBeInTheDocument()
received value must be an HTMLElement or an SVGElement.
Moreover in Testing Preview I get:
<button
class="n-button n-button--primary-type n-button--large-type mx-4"
tabindex="0"
type="button"
disabled="false"
role="test"
>
<transition-stub>
</transition-stub><span class="n-button__content">
<div data-testid="visible">visible</div>
</span>
</button>
a button, which makes it more confusing to me... Same situation happened when I replaced waitFor with nextTick from Vue, the component didn't do a toggle at all on click.
What works but isn't acceptable
When I changed the n-button to just button, the test passed and divs are toggled, but that's not the goal of this component. The component isn't supposed to be changed.
What I have tried:
I tried different approaches with reaching the div that contains hidden text. Either it was like above - queryByText/getByText or getByTestId, but test fails at the same point.
Also followed with similar approach shown at Testing Library - Disappearance Sample
but doesn't work in my case above.
What actually is going on and how can I test the change on click with such third-party components?
If more info is needed/something is missing, also let me know, I'll update the question.
Any suggestions, explanations - much appreciated.

How to have list + details pages based on API fetched content

I am facing a issue in my nuxt projct.
when i route the page by using nuxt-link, it doesn't render component in my page, i guess this is not making fetch call.
but when i use normal a href link, my page is working fine. everything is in place.
here is the link in a blog listing page component
// blog listing page snippet
<template>
<div>
<div v-for="blog in blogs.response.posts" :key="blog.id" class="col-md-3">
<nuxt-link :to="`/blogs/${blog.id}`" class="theme-blog-item-link"> Click to View Blog </nuxt-link>
</div>
</div>
</template>
<script>
export default {
data() {
return {
blogs: [],
}
},
async fetch() {
this.blogs = await fetch('https://www.happyvoyaging.com/api/blog/list?limit=4').then((res) => res.json())
},
}
</script>
but this works fine with if i replace nuxt-link with a href tag
<a :href="`/blogs/${blog.id}`" class="theme-blog-item-link">
Click to View Details
</a>
By click to that link, i want to view the detail of the blog by given id. that is _id.vue, code for that page is below.
//This is Specific Blog Details page code
<template>
<div class="theme-blog-post">
<div v-html="blogs.response.description" class="blogdesc"></div>
</div>
</template>
<script>
export default {
data(){
return {
blogs: []
}
},
async fetch() {
const blogid = this.$route.params.id
this.blogs = await fetch('https://www.happyvoyaging.com/api/blog/detail?id='+blogid+'').then((res) => res.json())
},
}
</script>
problem is on blogdetails page, where routing through nuxt-link not rendering the components but by normal a href link, it works fine
I am getting this error in console
vue.runtime.esm.js?2b0e:619 [Vue warn]: Unknown custom element: <PageNotFound> - did you register the component correctly? For recursive components, make sure to provide the "name" option.
found in
---> <Error> at layouts/error.vue
<Nuxt>
<Layouts/default.vue> at layouts/default.vue
<Root>
Since your API requires some CORS configuration, here is a simple solution with the JSONplaceholder API of a index + details list collection.
test.vue, pretty much the blog listing in your case
<template>
<div>
<div v-if="$fetchState.pending">Fetching data...</div>
<div v-else>
<div v-for="item in items" :key="item.id">
<nuxt-link :to="`/details/${item.id}`"> View item #{{ item.id }}</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: [],
}
},
async fetch() {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
this.items = await response.json()
},
}
</script>
details.vue, this one needs to be into a pages/details/_id.vue file to work
<template>
<div>
<button #click="$router.push('/test')">Go back to list</button>
<br />
<br />
<div v-if="$fetchState.pending">Fetching details...</div>
<div v-else>{{ details.email }}</div>
</div>
</template>
<script>
export default {
data() {
return {
details: {},
}
},
async fetch() {
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${this.$route.params.id}`)
this.details = await response.json()
},
}
</script>
As you can see, I do only use async/await here and no then for consistency and clarity.
Try this example, then see for fixing the CORS issue. The latter is backend only and not anyhow related to Nuxt. A quick google search may give you plenty of results depending of your backend.

How do I call the API with Axios on the (child) component and present the result on the Page (parent) component in Nuxt?

If I run this code on the Page component (mountains.vue) it works and I get the data from the API with help with Axios:
<template>
<div>
<ul>
<li v-for="(mountain, index) in mountains" :key="index">
{{ mountain.title }}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
mountains: [],
};
},
async asyncData({ $axios }) {
const mountains = await $axios.$get("https://api.nuxtjs.dev/mountains");
return { mountains };
},
};
</script>
But I want to put this code in a component (MountainsList) and do the Axios call in the component (MountainsList), but display the data on the Page component (mountains.vue) by injecting the component in Nuxt like this:
<template>
<MountainsList />
</template>
Now when I run the code, the data using Axios doesn't appear anymore... So how do I inject the data to the Page component above doing the Axios call in the child component?
asyncData only works on a page
From the docs
asyncData is called every time before loading the page component
One way you can accomplish what you want is passing the mountains in as a prop to the MountainList component. Something like below...
<template>
<MountainList :mountains="mountains" />
</template>
<script>
export default {
async asyncData({ $axios }) {
const mountains = await $axios.$get("https://api.nuxtjs.dev/mountains");
return { mountains };
},
};
</script>
And the component with the code and prop mountains...
<template>
<div>
<ul>
<li v-for="(mountain, index) of mountains" :key="index">
{{ mountain.title }}
</li>
</ul>
</div>
</template>
<script>
export default {
props: ['mountains'],
};
</script>
If you really want to make the API call in the child component you can use the fetch method.
Also you should not define a data() property on the page. I believe it will overwrite the server rendered data.
According to official docs :
Components in this directory will not have access to asyncData.
It means that any components inside the components folder cannot access that method.

Limit #click event on a dynamically created element using v-for to the element it's called on

I have a component that generates a div for every element in an array using v-for. To get more info on the component you click an icon that uses fetches API data and displays it under the icon. It currently displays the fetched info under every element in the array instead of the element it's called on. How can I fix this? (new to vue.js)
Strain.vue
<template>
<div>
<div id="strain-container"
v-for="(strain, index) in strains"
:key="index"
>
<h3>{{ strain.name }}</h3>
<p>{{ strain.race }}</p>
<i class="fas fa-info-circle" #click="getDetails(strain.id)"></i>
<strain-description :strainData="strainData"></strain-description>
</div>
</div>
</template>
<script>
import axios from 'axios';
import strainDescription from './strainDescription'
export default {
props: ['currentRace'],
components: {
'strain-description': strainDescription,
},
data(){
return{
strains: [],
apiKey: 'removed-for-stack-overflow',
strainData: {},
}
},
methods: {
getDetails: function(id){
const descApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/desc/${id}`);
const effectApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/effects/${id}`);
const flavourApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/flavors/${id}`);
axios.all([descApi, effectApi, flavourApi])
.then((values)=> axios.all(values.map(value => value.json())))
.then((data) => {
this.strainData = data;
});
}
},
Then output the data in strain-description component:
strainDescription.vue
<template>
<div id="strain-description">
<p>{{ strainData[0].desc }}</p>
<p>{{ strainData[1] }}</p>
<p>{{ strainData[2] }}</p>
</div>
</template>
<script>
export default {
props: ['strainData'],
}
</script>
Understandably (though not to me) this outputs it into every instance of the "strain-container", instead of the instance it's called on.
Any help is appreciated!
Add the strainData to the strain in the strain array. So first you can pass the index through to your click function
<i class="fas fa-info-circle" #click="getDetails(strain.id, index)"></i>
then you can update the strains array by index with your data
getDetails: function(id, index){
const descApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/desc/${id}`);
const effectApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/effects/${id}`);
const flavourApi = fetch(`https://strainapi.evanbusse.com/${this.apiKey}/strains/data/flavors/${id}`);
axios.all([descApi, effectApi, flavourApi])
.then((values)=> axios.all(values.map(value => value.json())))
.then((data) => {
this.strains[index].strainData = data;
});
}
then back in the template you can display like so
<strain-description :strainData="strain.strainData"></strain-description>
Bonus to this is you can check whether the strainData already exists on the clicked strain by checking if strain[index].strainData is defined or not before you make an api call
EDIT
If it doesn't update the template you may need to use vue set to force the render
this.$set(this.strains[index], 'strainData', data);

Getting ref of component in async v-for

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