Vue 3 display fetch data v-for - vue.js

So, I'm creating a Pokemon application and I would like to display the pokemon names using the api : https://pokeapi.co/api/v2/pokemon/.
I'm doing a fetch request on the api and then display the pokemon names in my template. I have 0 problem when I try to display only 1 pokemon but I have this error when I try to display all my pokemons using v-for.
Do you have any idea why I meet this error ?
<template>
<p class="dark:text-white"> {{pokemons[0].name}} </p> //working
<div v-for="(pokemon, index) in pokemons" :key="'poke'+index"> //not working...
{{ pokemon.name }}
</div>
</template>
<script>
const apiURL = "https://pokeapi.co/api/v2/pokemon/"
export default {
data(){
return{
nextURL:"",
pokemons: [],
};
},
created(){
this.fetchPokemons();
},
methods:{
fetchPokemons(){
fetch(apiURL)
.then( (resp) => {
if(resp.status === 200){
return resp.json();
}
})
.then( (data) => {
console.log(data.results)
// data.results.forEach(pokemon => {
// this.pokemons.push(pokemon)
// });
// this.nextURL = data.next;
this.pokemons = data.results;
console.log(this.pokemons);
})
.catch( (error) => {
console.log(error);
})
}
}
}
</script>
<style>
</style>

I've just pasted your code into a Code Pen and removed the working/not working comments and the code runs and shows the names.
Maybe the problem is in the parent component where this component is mounted, or the assignment of the :key attribute
try :key="'poke'+index.toString()", but I'm pretty sure js handels string integer concats quiet well.
Which version of vuejs do you use?
Edit from comments:
The parent component with the name PokemonListVue imported the posted component as PokemonListVue which resulted in a naming conflict. Renaming either one of those solves the issue.
In the error message posted, in line 3 it says at formatComponentName this is a good hint.

Related

How to update a variable from an asynchronous function?

In my template, in a v-slot (which means users is not available in <script setup>), I have
<template v-slot:body-cell-assignedTo="props">
<q-td :props="props">
<div v-for="u in props.users" :key="u">{{u}}</div>
</q-td>
</template>
This displays
john
mary
I can enrich this information by calling an API:
fetch('https://example.com/api/user/john')
.then(r => r.json())
.then(r => console.log(r))
This displays in the console John de Brown, 1262-1423.
My question: how to combine these two mechanisms? In other words, how to asynchronously update the value in {{}}?
I would need to do something like
<div v-for="u in props.users" :key="u">{{enrichFetchFunction(u)}}</div>
but it would need to be asynchronous, and yet somehow return a value.
EDIT: I will ultimately enrich the source data that is displayed in the v-slot. I would still be interested, though, if waiting for such an asynchronous function there (à la await) is doable in Vuie.
I assume you are using Compositions API. See this playground
<script setup>
import { ref, onMounted } from 'vue'
const users = ref([])
onMounted(async() => {
fetch('https://mocki.io/v1/67abcfb6-4f25-4513-b0f9-1eb6c4906413')
.then(r => r.json())
.then(r => users.value = r)
})
</script>
<template>
<div v-for="u in users" :key="u">{{u}}</div>
</template>
This is doable with Lifecycle hooks such as mounted(), yet you will need some sort of listener to react to the information being changed. here is an example that updates the values as soon as it is mounted and includes a button that will also update the values (you can run the code here in Vue SFC Playground):
<template>
<div id="app">
<h1 v-for="u in enrichedUsers" :key="u">{{ u }}</h1>
<button #click="myAsyncFunction">
update
</button>
</div>
</template>
<script>
// pseudo api
const fetchEnrichedAPI = function(user) {
return new Promise( (resolve, reject) => {
var enrichedUsers = []
if (user.includes('john')) {
enrichedUsers.push('John de Brown, 1262-1423')
}
if (user.includes('mary')){
enrichedUsers.push('Mary de Purple, 1423-1262')
}
setTimeout(() => {
resolve(enrichedUsers);
}, 300);
});
}
export default{
data() {
return {
props : { users: ['john','mary'] },
enrichedUsers: []
}
},
mounted() {
// when mounted run this async function
this.myAsyncFunction()
},
methods: {
async myAsyncFunction() {
// call api passing the list of users
await fetchEnrichedAPI(this.props.users)
.then((data) => {
// if api work
this.enrichedUsers = data;
return true;
})
.catch((e) => {
// if the api doesn't work
console.error(e);
this.enrichedUsers = this.props.users;
})
}
},
}
</script>
I am aware that this does not use props, but it does work. If you would like to expand this to use props you may be able to do this with computed properties or functions in the v-for. See this post for more info on that.

Pass Vue js search filter functionality through single file components with EventBus

I have the following components:
/components/SearchBlogs.vue Search component to filter on blog.title and blog.description.
/components/BlogList.vue Here I list all the Blog items.
SearchBlogs.vue
<template>
<div>
<input type="text" v-model="search" #change="emitSearchValue" placeholder="search blog">
</div>
</template>
<script>
import { EventBus } from '../event-bus.js'
export default {
name: 'SearchBlogs',
data: () => {
return {
search: ''
}
},
methods: {
emitSearchValue() {
EventBus.$emit('search-value', 'this.search')
}
}
}
</script>
BlogList.vue
<template>
<div>
<div v-for="blog in filteredBlogs" :key="blog">
<BlogListItem :blog="blog" />
</div>
</div>
</template>
<script>
import BlogListItem from './BlogListItem'
import { EventBus } from '../event-bus.js'
export default {
name: 'BlogList',
components: {
BlogListItem,
},
data: () => {
return {
blogs: [],
searchvalue: ''
}
},
computed: {
filteredBlogs() {
return this.blogs.filter(blog =>
blog.name.toLowerCase().includes(
this.searchvalue.toLowerCase()
)
)
}
},
created() {
fetch('http://localhost:3000/blogs')
.then(response => {
return response.json();
})
.then(data => {
this.blogs = data;
}),
EventBus.$on('search-value', (search) => {
this.searchvalue = value;
})
}
}
</script>
In another page component Blogs I register both components:
<template>
<div>
<h1>Blog</h1>
<TheSidebar>
<SearchBlogs />
</TheSidebar>
<BlogList/>
</div>
</template>
Can anybody see what's missing here? I want, as soon as the user types something in the search input (from the SearchBlogs.vue component), it start filtering and updating the list.
Look at my solution condesandbox
Here is an explanation:
You don't need to use EventBus. You can communicate with Search Component by v-model, using prop value and emiting updated value from the Input.
Then your Main (List) Component is responsible for all the logic.
It keeps the state of a Search
It keeps the items and filtered Items
Thanks to that your Search Component is very clear and has no data, that means it has very little responsibility.
Please ask questions if I can add something to help you understand 😉
UPDATE:
EventBus is a great addition in some cases. Your case is simple enough, there is no need to add it. Right now your architecture is "over engineered".
When you have added listener on EventBus, on created:hookyou should always remove it while Component is being destroyed. Otherwise you can encounter a trouble with double calling function etc. This is very hard to debug, tryst me I'he been there 😉
Going with my suggestion gives you comfort of "no-need-to-remember-about-this" because Vue is doing it for you.
Hope that help.
Couple of issues but essentially the computed prop filteredData will look like:
computed: {
filteredData() {
return this.experiences.filter(
el => el.category.indexOf(this.search) > -1
);
}
}
Also, used quotes around 'this.search' when passing its value back which made it a string.
Fixed sandbox
https://codesandbox.io/s/reverent-lamarr-is8jz

Dynamic v-for rendering from http get results to Vue component

I'm trying to achieve a dynamic rendering of elements base from the return items array of the target API endpoint. Below is what I've tried
<template id="list-comp-tpl">
<button #click="searchNow">Search Now</button>
<ul v-for="item in searchResults">
<li>{{ item.name }}</li>
</ul>
</template>
<div id="app">
<list-comp></list-comp>
</div>
Vue.components('list-comp',{
template : '#list-comp-tpl',
data() {
return {
searchResults : [];
}
},
method : {
searchNow(){
// perform api query
axios.get('http://apiwebsite.com/search')
.then(function (response) {
// handle success
this.searchResults = response.data.msg;
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
}
}
});
new Vue({
el '#app'
});
But the component list-comp is not updating at all like when there's a return data from the api, it does not display as I expect it to. What can I try next?
for me "this" is not visible within function(response)
solutions that work for me
.then(response => this.searchResults = response.data.msg)
or, If you are keen on function(response) try this:
searchNow(){
_self = this;
// perform api query
axios.get('http://apiwebsite.com/search')
.then(function (response) {
// handle success
_self.searchResults = response.data.msg;
})
.catch(function (error) {
// handle error
console.log(error);
})
.then(function () {
// always executed
});
}
Try assigning values as shown.
this.searchResults = Object.assign({}, response.data.msg);
The way you are setting the values in array they are not set as reactive property on the DOM. Hence your component is not updated with new data.
You can also use Vue.set for updating array values.
Refer below link.
https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats

Vuejs testing value after click

<!-- template -->
<div>
<textarea v-model="someText">{{someText}}</textarea>
<div v-if="hasError">Something wrong here</div>
<input v-on:click="store" type="submit" value="update" />
</div>
//script
{
data() {
hasError: false,
someText: ""
},
store(){
return axios.post('/my/api/endpoint', { myvalue: this.someText })
.then(() => {
this.hasError= false;
})
.catch(() => {
this.hasError= true;
};
}
}
//test
import { mount } from 'vue-test-utils';
import MyComponent from "./component.vue";
import * as httpMock from 'moxios';
import Vue from "vue";
it("notifies when updates fail", (done) => {
const wrapper = mount(MyComponent);
httpMock.stubFailure("PUT", "/my/api/endpoint",
{
status: 500
});
httpMock.wait(() => {
wrapper.find(".button").trigger ("click");
Vue.nextTick(() => {
expect(wrapper.html()).toContain("Something wrong here");
done();
});
});
I have the above code to test error state in the vue app. Simply, i'm trying to test that if there is an error calling the server, a bit of text is displayed to say so. i've manually tested this in the browser and its all good, but i can't get a unit test around it. it just fails, saying expected '...' does not contain Something wrong here
probably something to do with the dom not being updated yet? But I thought that was what Vue.nextTick was for?
You're running wait before you actually trigger the axios call. Your call of the click event must be outside of wait.
wrapper.find(".button").trigger ("click");
httpMock.wait(() => {
Vue.nextTick(() => {
expect(wrapper.html()).toContain("Something wrong here");
done();
});
})
Also, I'm assuming you're importing axios in the component, as I don't actually see the import.

VueJS: TypeError: Cannot read property of undefined when Reload

I have a page like this:
<template>
<div class="row flex">
{{posts.id}}
</div>
</template>
<script>
import axios from 'axios'
export default {
async asyncData ({ route }) {
let { data } = await axios.get('http://localhost:8000/api/v1/feeds/' + route.params.id + '/')
return {
posts: data
}
}
}
</script>
When I click link with hot reload (router-link), it display well. But when I reload this window, it appear in 1 seconds and disappear then.
Video: http://g.recordit.co/ht0a0K2X81.gif
Error Log:
How can I fix this?
Add a property to your data i.e dataLoaded: false. When your ajax request has finished, set this.dataLoaded = true. On your template add v-if="dataLoaded. This will mean the template data won't render until you're ready.
You could also do v-if="posts" as another way but I generally have a consistent dataLoaded prop available to do this.
Edit: I just looked at your example again and doing something like this would work:
<template>
<div class="row flex" v-if="posts">
{{posts.id}}
</div>
</template>
<script>
import axios from 'axios'
export default {
data () {
return {
posts: null
}
}
methods:{
loadPosts () {
return axios.get('http://localhost:8000/api/v1/feeds/' + this.$route.params.id + '/')
}
},
created () {
this.loadPosts().then(({data}) => {
this.posts = data
})
}
}
</script>
I've removed the async and just setting posts when the axios request returns it's promise. Then on the template, it's only showing posts is valid.
Edit
You can also use your original code and just add v-if="posts" to the div you have in your template.