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

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);

Related

passing object to component using v-for

I am trying to send a series of objects that are in an array to a child component using v-for, but when I try to access them from the child component, it tells me that the props are not defined.
Im using Quasar Framework actually
This is how I pass the data:
<div class="row justify-center">
<foo
v-for="brand in brands"
:key="brand.id"
:brand="brand"
></foo>
</div>
<script>
import foo from "src/components/foo.vue";
export default {
components: {
foo
},
data() {
return {
brands: []
};
},
methods: {
async getData() {
let x = await get.getData();
this.brands = x.data;
console.log(this.brands);
}
},
mounted() {
this.getData();
}
};
</script>
brands is an array that obtains two objects from a request made to a local database, which I have already verified that it receives the data correctly
And this is the component file and how I try to get the properties:
<q-card class="my-card" flat bordered>
<q-img
:src="require(`../assets/${brand.img}`)"
:alt="brand.img + ' Logo'"
/>
<div class="text-h5 q-mt-sm q-mb-xs">{{ brand.name }}</div>
<div class="text-caption text-grey">
<p>
{{ brand.price }}
</p>
</div>
<script>
export default {
name: "foo",
props: ["brand"],
data() {
return {
expanded: false
};
},
};
</script>
but when I try to execute the code it gives me the following error:
Error in render: "Error: Cannot find module './undefined'
I know one way to make it work, and it is by creating a property for each of the object's values, for example:
<component
v-for="brand in brands"
:key="brand.id"
:name="brand.name"
:price="brand.price"
></component>
But I dont think thats the correct way to do this....
try to change
import component from "src/components/component.vue";
to
import foo from "src/components/component.vue";
on your components section you just call foo instead of foo:component
I am not sure, but:
Looks like ${brand} is empty. Your function GetData() is async, so the <foo> is created before the GetData() has its data set/returned.
You can change
<foo v-for="brand in brands" :key="brand.id" :brand="brand"></foo>
To
<foo v-if="brands.length> 0" v-for="brand in brands" :key="brand.id" :brand="brand"></foo>
To make sure that the element is renderd after the data if set.
Note: v-if is when the html is rendered, v-show is just a css display hide, but the html is always renderd

VueJS: Not printing data returned in method

I'm successfully getting data into the console. When I try to print that data to the page by calling the method in double moustache braces it doesn't appear on screen. All other data in template appears just fine.
Template:
<template>
<div>
<div v-for="data in imageData" :key="data.id">
<div class="card">
<img :src="data.source" :alt="data.caption" class="card-img" />
<div class="text-box">
<p>{{ moment(data.timestamp.toDate()).format("MMM Do YYYY") }}</p>
<p>{{ data.caption }}</p>
// The Geocoding method is the problem
<p>{{reverseGeocode(data.location.df, data.location.wf)}}</p>
</div>
</div>
</div>
</div>
</template>
Method:
methods: {
reverseGeocode: (lat, long) => {
fetch(`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
).then((res) =>
res.json().then((data) => {
console.log(data.results[0].formatted_address); // works fine
return data.results[0].formatted_address;
})
);
},
},
Here's the image data I'm getting in props
Your problem is a common problem when you start making requests in JavaScript.
The date requests are asynchronous so the method cannot return a value after the execution of the method has finished.
Imagine the following call stack:
Start method.
Throw fetch. <- Asynchronous
Finish method.
Fetch ends.
You are trying to do a return in step 4 and it should be in 3.
To solve this you should use async with await. You could also solve it by making a component and passing the data (this is my favorite since you are using vue).
Component parent
<template>
<div>
<component-card v-for="data in imageData" :key="data.id" :dataItem="data">
</component-card>
</div>
</template>
Child component
<template>
<div class="card">
<img :src="dataItem.source" :alt="dataItem.caption" class="card-img" />
<div class="text-box">
<p>{{ moment(dataItem.timestamp.toDate()).format("MMM Do YYYY") }}</p>
<p>{{ dataItem.caption }}</p>
<p>{{formattedAddress}}</p>
</div>
</div>
</template>
<script>
export default {
props: {
dataItem: {
type: {},
default: () => ({})
}
},
data() {
return {
formattedAddress: ""
};
},
created() {
this.reverseGeocode(this.dataItem.location.df, dataItem.location.wf)
},
methods: {
reverseGeocode(lat, long) {
fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
).then(res =>
res.json().then(data => {
console.log(data.results[0].formatted_address); // works fine
this.formattedAddress = data.results[0].formatted_address;
})
);
}
}
};
</script>
I have not tried it, surely some things are missing but the template should be that.
The above I think is correct as well, but I would push for async
async reverseGeocode(lat, long) {
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${long}&key=API_KEY&result_type=locality`
);
const data = response.json();
return data.results[0].formatted_address;
}
You should change your approach to the following:
Do all requests in the created() lifecycle method and store the results in a data attribute then iterate over the data attribute. The created() lifecycle method executes before the DOM is mounted so all data fetching APIs should be called there. FYR: https://v2.vuejs.org/v2/guide/instance.html
Please also refer to Vue.js - Which component lifecycle should be used for fetching data?

How to keep the currentPage variable after clicking on the detail link Using Vue

I am starting with vue.js, the code below is fine, the idea is when a person clicks in detail, I send it to another component to see the detail of the user this is fine, what I need is when the user clicks on it link back inside the detail component, keep the currentPage variable, what it does is set the currentPage variable to 1
I found something called keepAlive, but I really don't know how it works.
Also some way for mounted to run once.
this is my view here i call the Users component
<template>
<div>
<h1>Lista de Usuarios</h1>
<Users/>
</div>
</template>
<script>
import Users from "#/components/Users.vue";
export default {
name:"Usuarios",
components:{
Users
}
};
</script>
Users component
<template>
<div id="ejemplo">
<b-table
id="tabla"
:items="usuarios"
:per-page="perPage"
:current-page="currentPage"
:fields="fields"
small
>
<template v-slot:cell(Detalle)="fila">
<b-link :to="{ name: 'Usuario', params: { id:fila.item.id} }">Ver Más</b-link>
</template>
<template v-slot:cell(Elimina)="fila">
<b-button size="sm" #click="elimina(fila.item.id)" class="mr-2">
Elimina</b-button >
</template>
</b-table>
<b-pagination
v-model="currentPage"
:total-rows="total"
:per-page="perPage"
aria-controls="tabla"
></b-pagination>
</div>
</template>
<script>
export default {
name:"Users",
data(){
return{
perPage: 3,
currentPage: 1,
usuarios:[],
fields: ['id', 'name', 'Detalle','Elimina'],
total:0,
}
},
methods:{
async Listar(){
let response = await fetch('https://jsonplaceholder.typicode.com/users')
let data= await response.json()
this.usuarios= await data
this.total= await this.rows()
},
rows() {
return this.usuarios.length
},
elimina(id){
this.usuarios=this.usuarios.filter(usuario=>usuario.id !== id)
}
},
mounted(){
this.Listar()
}
}
</script>
Maybe in your case it makes sense to use Vuex. Vuex is something like a a globally store for your Vue-Application. You could set the "currentPage"-Variable in there and change it's value by using "mutations".

How to prevent the data/method sharing of looped components in Vue.js

I have the vue component with $emit into component and let it return the data from the component. I will use the component to update current page's data. the codes below
Template:
<Testing
#update="update">
</Testing>
<AnotherComponent
:data="text"
>
</AnotherComponent>
Script:
method(){
update: function(data){
this.text = data.text
}
}
it work perfectly if only this one.
Now , i need to make a button to add one more component.
I use the for loop to perform this.
Template
<div v-for="index in this.list">
<Testing
:name="index"
#update="update">
</Testing>
<AnotherComponent
:data="text"
>
</AnotherComponent>
</div>
Script:
method(){
addList : function(){
this.list +=1;
},
deleteList : function(){
this.list -=1;
},
update: function(data){
this.text = data.text
}
}
The add and delete function run perfectly.
However , they share the "update" method and the "text" data.
so , If I change the second component , the first component will also changed.
I think this is not the good idea to copy the component.
Here are my requirements.
This component is the part of the form, so they should have different name for submit the form.
The another component" will use the data from the "testing component" to do something. the "testing" and "another component" should be grouped and the will not change any data of another group.
Any one can give me the suggestion how to improve these code? Thanks
What happends is that both are using the data form the parent, and updating that same data.
It seems that you are making some kind of custom inputs. In that case in your child component you can use 'value' prop, and 'input' event, and in the parent user v-model to keep track of that especific data data.
Child component BaseInput.vue:
<template>
<div>
<input type="text" :value="value" #keyup="inputChanged">
</div>
</template>
<script>
export default {
props: ['value'],
data () {
return {
}
},
methods: {
inputChanged (e) {
this.$emit('input', e.target.value)
}
}
}
</script>
And this is the code on the parent:
<template>
<div class="container">
<div class="row">
<div class="col-xs-12 col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3">
<base-input v-model="firstInputData"></base-input>
<p>{{ firstInputData }}</p>
<hr>
<base-input v-model="secondInputData"></base-input>
<p>{{ secondInputData }}</p>
</div>
</div>
</div>
<script>
import BaseInput from './BaseInput.vue'
export default {
components: {BaseInput},
data() {
return{
firstInputData: 'You can even prepopulate your custom inputs',
secondInputData: ''
}
}
}
</script>
In the parent you could really store the diferent models in an object as properties, and pass that object to that "The another component" , pass them as individual props... pass an array ....

Vue Sync Data on v-for

inside my template I have this element:
<template>
<div>
<span v-for="item in items">{{item.name}}</span>
</div>
</template>
in my script I have:
import axios from 'axios';
export default {
data: () => ({ items: null }),
mounted: function () {
var vm = this;
axios.get('/items')
.then(function (response) {
vm.items = response.data;
});
}
}
My question is how to do I get <span> elements show up and for the v-for execute once the response returned with a list of items?
You need to wrap spans created by v-for in a single root div:
<template>
<div>
<span v-for="item in items">{{item.name}}</span>
</div>
</template>
And also there seems to be a problem with vm.items = response; which should be vm.items = response.data;, See axios response schema for more detail.