I have a component that gets data from the API and passes it to another component that displays it (passing down again to multiple child components). So I tried to split that information into multiple ref objects for passing down to each child component.
The problem is, doing that I loose reactivity... the new ref object does not update when parent component changes their data.
I created an example to show the problem.
ParentComponent
<template>
<p>All data: {{ data }}</p>
<child-component :data="data"></child-component>
</template>
<script setup>
import { ref } from "vue";
import ChildComponent from "./ChildComponent.vue";
let data = ref({
a: {},
b: {},
c: {}, // More properties...
});
setTimeout(() => { // FAKE API
data.value.a = {
name: "prop A",
description: "aaa"
};
data.value.b = {
name: "prop B",
description: "bbb"
};
data.value.c = {
name: "prop C",
description: "ccc"
};
// More properties updated...
}, 2000);
</script>
ChildComponent
<template>
<p>child component props: {{data}}</p>
<p>child component manipulated: {{manipulated}}</p>
</template>
<script setup>
import { ref } from "vue";
const props = defineProps(['data'])
let manipulated = ref({ // Only need a and b properties.
a: props.data.a,
b: props.data.b
})
</script>
Output when mount components
All data: { "a": {}, "b": {}, "c": {} }
child component props: { "a": {}, "b": {}, "c": {} }
child component manipulated: { "a": {}, "b": {} }
Output after updated parent component data:
All data: { "a": { "name": "prop A", "description": "aaa" }, "b": { "name": "prop B", "description": "bbb" }, "c": { "name": "prop C", "description": "ccc" } }
child component props: { "a": { "name": "prop A", "description": "aaa" }, "b": { "name": "prop B", "description": "bbb" }, "c": { "name": "prop C", "description": "ccc" } }
child component manipulated: { "a": {}, "b": {} }
Desired result after updated parent component data:
All data: { "a": { "name": "prop A", "description": "aaa" }, "b": { "name": "prop B", "description": "bbb" }, "c": { "name": "prop C", "description": "ccc" } }
child component props: { "a": { "name": "prop A", "description": "aaa" }, "b": { "name": "prop B", "description": "bbb" }, "c": { "name": "prop C", "description": "ccc" } }
child component manipulated: { "a": { "name": "prop A", "description": "aaa" }, "b": { "name": "prop B", "description": "bbb" } }
How can achieve that? With primitives works... but with objects there is missing something to maintain the reactivity...
Option 1: Use deep watcher
You could use a deep watcher (i.e., watch with the deep flag) on props.data that updates manipulated with the new values of props.data:
// ChildComponent.vue
import { watch } from 'vue'
⋮
watch(
() => props.data,
newValue => {
const { a, b } = newValue
manipulated.value = { a, b }
},
{ deep: true }
)
demo 1
Option 2: Use computed
If manipulated were just a read-only prop, you could switch from the ref and watch to a computed instead:
import { computed } from 'vue'
const props = defineProps(['data'])
let manipulated = computed(() => ({
a: props.data.a,
b: props.data.b,
}))
demo 2
Related
I have products two types: simple and configurable:
"products" : [
{
"type": "simple",
"id": 1,
"sku": "s1",
"title": "Product 1",
"regular_price": {
"currency": "USD",
"value": 27.12
},
"image": "/images/1.png",
"brand": 9
},
{
"type": "configurable",
"id": 2,
"sku": "c1",
"title": "Product 2",
"regular_price": {
"currency": "USD",
"value": 54.21
},
"image": "/images/conf/default.png",
"configurable_options": [
{
"attribute_id": 93,
"attribute_code": "color",
"label": "Color",
"values": [
{
"label": "Red",
"value_index": 931,
"value": "#ff0000"
},
{
"label": "Blue",
"value_index": 932,
"value": "#0000ff"
},
{
"label": "Black",
"value_index": 933,
"value": "#000"
}
]
},
{
"attribute_code": "size",
"attribute_id": 144,
"position": 0,
"id": 2,
"label": "Size",
"values": [
{
"label": "M",
"value_index": 1441,
"value": 1
},
{
"label": "L",
"value_index": 1442,
"value": 2
}
]
}
],
"variants": [
{
"attributes": [
{
"code": "color",
"value_index": 931
},
{
"code": "size",
"value_index": 1441
}
],
"product": {
"id": 2001,
"sku": "c1-red-m",
"image": "/image/conf/red.png"
}
},
{
"attributes": [
{
"code": "color",
"value_index": 931
},
{
"code": "size",
"value_index": 1442
}
],
"product": {
"id": 2002,
"sku": "c1-red-l",
"image": "/image/conf/red.png"
}
},
{
"attributes": [
{
"code": "color",
"value_index": 932
},
{
"code": "size",
"value_index": 1441
}
],
"product": {
"id": 2003,
"sku": "c1-blue-m",
"image": "/image/conf/blue.png"
}
},
{
"attributes": [
{
"code": "color",
"value_index": 933
},
{
"code": "size",
"value_index": 1442
}
],
"product": {
"id": 2004,
"sku": "c1-black-l",
"image": "/image/conf/black.png"
}
}
],
"brand": 1
}
]
The above data I get with actions (Vuex)
GET_PRODUCTS_FROM_API({ commit }) {
return axios('http://localhost:8080/products', {
method: 'GET',
})
.then((products) => {
commit('SET_PRODUCTS_TO_STATE', products.data);
return products;
})
.catch((e) => {
console.log(e);
return e;
});
}
then I mutate the data:
SET_PRODUCTS_TO_STATE: (state, products) => {
state.products = products
}
and get from in getters
PRODUCTS(state) {
return state.products = state.products.map((product) => {
const brand = state.brands.find((b) => b.id === product.brand)
return {...product, brandName: brand?.title || 'no brand'}
})
}
after which i get the data in the component
At the moment I'm stuck on how to render the color and size attributes of a configurable product. Tell me how to do it right? Do I need to write logic in vuex or parent component?
I tried to push data from parent component to child. But it stopped there again.
I also tried to separate the color and size attributes separately using getters.
For Vuex, the syntax is the following
<template>
<div>
<div v-for="product in products" :key="product.id">
<span>type: {{ product.type }}</span>
<span>type: {{ product.title }}</span>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
computed: {
...mapGetters(['products']),
...mapGetters('fancyNamespace', ['products']), // if namespaced
},
}
</script>
As of where to call it, directly into the component I guess. Otherwise, as explained here it may not be relevant to use Vuex at all.
PS: you can even rename on the fly if you want.
Solved this issue by Computed Properties and transfer props to child components
computed: {
getAttributeColors() {
let attributes_colors = []
this.product_data.configurable_options.map((item) => {
if(item.label === 'Color') {
attributes_colors.push(item.values)
}
})
return attributes_colors
},
getAttributeSize() {
let attributes_size = []
this.product_data.configurable_options.map((item) => {
if(item.label === 'Size') {
attributes_size.push(item.values)
}
})
return attributes_size
}
}
I want to use emit and props in Nuxtjs3, but not sure, what i am doing wrong here.
in My project, i need to find airport data at a lot of pages, so i created a airportsearch component.
this is inside
components/AirportSearch.vue
<template>
<AutoComplete
class="ttc-w-full ttc-h-12"
v-model="AirportData"
:suggestions="airports"
#complete="getairports($event)"
field="name"
placeholder="Search Pickup Location"
forceSelection
#item-select="Update"
>
<template #item="slotProps">
<div
style="display: flex; justify-content: space-between"
class="ttc-font-bold ttc-text-md"
>
{{ slotProps.item.name }}
<span class="ttc-font-normal ttc-text-xs">
{{ slotProps.item.code }}
</span>
</div>
</template>
</AutoComplete>
</template>
<script setup>
import { ref } from "vue";
const { apiUrl } = useRuntimeConfig();
const airports = ref([]);
const emit = defineEmits(["submit"])
const props = defineProps(['AirportData']);
const getairports = useDebounce(async (event) => {
const { data } = await useFetch(`${apiUrl}/airports/find`, {
method: "post",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
aptsearchkey: event.query,
}),
});
airports.value = data.value;
}, 500);
function Update(event) {
emit("submit", props.AirportData);
}
</script>
this is another component inside components folder
TaxiSearchForm.vue
<template>
<div>
<AirportSearch
:AirportData="pickuppoint"
#submit="handleUpdate"
/>
</div>
</template>
<script setup>
import {ref} from 'vue'
const airportTransferData = useCookie("airportTransferData", {
maxAge: 60 * 60 * 24 * 7,
});
let pickuppoint = ref();
function handleUpdate(pickuppoint) {
pickuppoint.value = pickuppoint;
};
onMounted(() => {
if (airportTransferData.value) {
pickuppoint.value = airportTransferData.value.pickuppoint;
}
});
</script>
I am not able to pass data from AirportSearch Component to TaxiSearchForm.
When i Select dropdown in AirportSearch, I see that AirportData Prop is getting populated. but I want same Data to be passed to var pickuppoint in TaxiSearchForm.
this is sample of response from api in AirportSearch.vue
const airports = [
{
"aptid": "1439",
"code": "DXB",
"name": "Dubai Intl Airport",
"cityCode": "DXB",
"cityName": "Dubai",
"countryName": "UNITED ARAB EMIRATES",
"countryCode": "AE",
"continent_id": null,
"timezone": "4",
"lat": "25.252778",
"lon": "55.364444",
"city": "true",
"id": "6135ee7e91649f157edff402"
},
{
"aptid": "3101",
"code": "XNB",
"name": "Dubai Bus Station Airport",
"cityCode": "DXB",
"cityName": "Dubai",
"countryName": "UNITED ARAB EMIRATES",
"countryCode": "AE",
"continent_id": null,
"timezone": "3",
"lat": "0",
"lon": "0",
"city": "false",
"id": "6135ee7e91649f157edffa80"
},
{
"aptid": "3475",
"code": "DWC",
"name": "Al Maktoum International Airport",
"cityCode": "DXB",
"cityName": "Dubai",
"countryName": "UNITED ARAB EMIRATES",
"countryCode": "AE",
"continent_id": null,
"timezone": "4",
"lat": "24.898504",
"lon": "55.143231",
"city": "false",
"id": "6135ee7e91649f157edffbf6"
},
{
"aptid": "7609",
"code": "SHJ",
"name": "Sharjah Airport",
"cityCode": "DXB",
"cityName": "Dubai",
"countryName": "UNITED ARAB EMIRATES",
"countryCode": "AE",
"continent_id": null,
"timezone": "4",
"lat": "25.328575",
"lon": "55.51715",
"city": "false",
"id": "6135ee7f91649f157ee00c1c"
}
]
The problem is the handleUpdate() argument (pickuppoint) shadows the outer ref of the same name, so the function incorrectly modifies its argument instead of the ref:
let pickuppoint = ref()
function handleUpdate(pickuppoint) {
// 👇 this is the function argument, not the outer ref
pickuppoint.value = pickuppoint
}
The fix is to make the names different. For instance, rename the function argument:
let pickuppoint = ref()
function handleUpdate(newPickuppoint) {
pickuppoint.value = newPickuppoint
}
demo
This is driving me crazy, I have nested data in an array and have not been successful in getting it to render using components, I am able to render it without a component though.
var data2 = {
"team": [{
"id":0,
"author": "Johnnie Walker",
"title": "Aging Your Own Whisky",
"content": "A bunch of steps and a whole lot of content",
"members": [
{
"id": "0",
"name": "name 1",
"text": "bio"
},
{
"id": "1",
"name": "name 2",
"text": "bio"
}
]
},
{
"id":1,
"author": "Captain Morgan",
"title": "Rum stories",
"content": "A bunch of steps and a whole lot of contentttt",
"members": [
{
"id": "3",
"name": "name 3",
"text": "bio"
}
]
}
]}
What I am trying to do is loop through members, here is my current code
index.html
<div id="app2">
<entry v-for="t in team" v-bind:cat="t" v-bind:key="t.id">
<detail v-for="mem in t.members" v-bind:ember="mem" v-bind:key="mem.id"></detail>
</entry>
</div>
and here is my js file
Vue.component('entry', {
props:['cat'],
template: '<div>{{cat.author}}</div>'
})
Vue.component('detail', {
props:['ember'],
template: '<div>{{ember.id}}</div>',
})
var vm2 = new Vue({
el: '#app2',
data: function() {
console.log(data2.team)
return data2;
}
});
The data in entry renders but nothing happens with detail, no warnings either, how do I proceed?
Note: When I approach it this way it DOES work, but this is not using a component:
var vm = new Vue({
el: '#app',
data: function() {
console.log(data2.team)
return data2;
}
});
and the html
<div id="app">
<div v-for="t in team" v-bind:key="t.id" v-bind:author="t.author">
{{t.author}}
<div v-for="m in t.members" v-bind:key="m.id">
{{m.name}}
</div>
</div>
</div>
You either need to add the detail component to the entry template, or you need to add a slot to the entry component.
Here is your code modified to use a slot.
console.clear()
var data2 = {
"team": [{
"id": 0,
"author": "Johnnie Walker",
"title": "Aging Your Own Whisky",
"content": "A bunch of steps and a whole lot of content",
"members": [{
"id": "0",
"name": "name 1",
"text": "bio"
},
{
"id": "1",
"name": "name 2",
"text": "bio"
}
]
},
{
"id": 1,
"author": "Captain Morgan",
"title": "Rum stories",
"content": "A bunch of steps and a whole lot of contentttt",
"members": [{
"id": "3",
"name": "name 3",
"text": "bio"
}]
}
]
}
Vue.component('entry', {
props: ['cat'],
template: '<div>{{cat.author}}<slot/></div>'
})
Vue.component('detail', {
props: ['ember'],
template: '<div>{{ember.id}}</div>',
})
var vm2 = new Vue({
el: '#app2',
data: function() {
console.log(data2.team)
return data2;
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app2">
<entry v-for="t in team" v-bind:cat="t" v-bind:key="t.id">
<detail v-for="mem in t.members" v-bind:ember="mem" v-bind:key="mem.id">
</detail>
</entry>
</div>
And here it is modifying the entry component to include the detail.
console.clear()
var data2 = {
"team": [{
"id": 0,
"author": "Johnnie Walker",
"title": "Aging Your Own Whisky",
"content": "A bunch of steps and a whole lot of content",
"members": [{
"id": "0",
"name": "name 1",
"text": "bio"
},
{
"id": "1",
"name": "name 2",
"text": "bio"
}
]
},
{
"id": 1,
"author": "Captain Morgan",
"title": "Rum stories",
"content": "A bunch of steps and a whole lot of contentttt",
"members": [{
"id": "3",
"name": "name 3",
"text": "bio"
}]
}
]
}
Vue.component('detail', {
props: ['ember'],
template: '<div>{{ember.id}}</div>',
})
Vue.component('entry', {
props: ['cat'],
template: `<div>
{{cat.author}}
<detail v-for="mem in cat.members" v-bind:ember="mem" v-bind:key="mem.id">
</detail>
</div>`
})
var vm2 = new Vue({
el: '#app2',
data: function() {
return data2;
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.js"></script>
<div id="app2">
<entry v-for="t in team" v-bind:cat="t" v-bind:key="t.id"></entry>
</div>
I used this way, but don't render
this.res = Object.assign({},
this.res, {
"employees": [{
"firstName": "John",
"lastName": "Doe"
}, {
"firstName": "Anna",
"lastName": "Smith"
}, {
"firstName": "Peter",
"lastName": "Jones"
}],
'haha': 'baba',
'papa': {
'mimi': 'mimi'
}
})
Instead of assigning to the object in get method do it in the created life hook, so the res property will be populated with data before rendering the page
var vue = new Vue({
el: "#app",
data: function() {
return {
res:{}
}
},
created: function() {
this.res = {
"employees": [{
"firstName": "John",
"lastName": "Doe"
}, {
"firstName": "Anna",
"lastName": "Smith"
}, {
"firstName": "Peter",
"lastName": "Jones"
}],
'haha': 'baba',
'papa': {
'mimi': 'mimi'
}
}
}
})
<script src="https://vuejs.org/js/vue.min.js"></script>
<div id="app" >
<div>{{res.employees[0].firstName}}</div>
<div>{{ res.papa.mimi}}</div>
<div>{{ res.haha}}</div>
</div>
employees is not defined at render time (creation).
You should write the res object directly in data.
"data": {
"offset": 0,
"limit": 20,
"total": 1,
"count": 1,
"results": [
{
"id": 1009144,
"name": "A.I.M.",
"modified": "2013-10-17T14:41:30-0400",
"thumbnail": {
"path": "i.annihil.us/u/prod/marvel/i/mg/6/20/52602f21f29ec",
"extension": "jpg"
},
"resourceURI": "gateway.marvel.com/v1/public/characters/1009144",
"comics": {
"available": 33,
"collectionURI": "gateway.marvel.com/v1/public/characters/1009144/comics",
"items": [
{
"resourceURI": "gateway.marvel.com/v1/public/comics/36763",
"name": "Ant-Man & the Wasp (2010) #3"
},
{
"resourceURI": "gateway.marvel.com/v1/public/comics/17553",
"name": "Avengers (1998) #67"
}
]
}
}
]
}
I am using axios to fetch datas from an api inside a React component. I would like to access to the key items in my json response in order to setState but I can't.
export default class Hero extends React.Component {
constructor(props) {
super(props);
this.state = {
details : [],
comics :[]
};
}
componentDidMount() {
axios.get(infoUrl).then((res) => {
this.setState({details : res.data.data.results,
comics : res.data.data.results.results[6].items});
})
}
render() {
(<div>
</div>)
}
}
I can access to my state details but not the comics one.
items being present in comics is not the 6th item in the result array but the 6th item in the first object of the result array and hence you need to access it like.
res.data.data.results[0].comics.items
Change you componentDidMount function to
componentDidMount() {
axios.get(infoUrl).then((res) => {
this.setState({details : res.data.data.results,
comics : res.data.data.results[0].items});
})
}
Results array contains only one item, so you need to use index 0 instead of 6. Another thing is items is present inside comics, so first access comics then access items, use this:
componentDidMount() {
axios.get(infoUrl).then((res) => {
this.setState({
details : res.data.data.results,
comics : res.data.data.results[0].comics.items});
})
}
Run this snippet:
let data = {"data": {
"offset": 0,
"limit": 20,
"total": 1,
"count": 1,
"results": [
{
"id": 1009144,
"name": "A.I.M.",
"modified": "2013-10-17T14:41:30-0400",
"thumbnail": {
"path": "i.annihil.us/u/prod/marvel/i/mg/6/20/52602f21f29ec",
"extension": "jpg"
},
"resourceURI": "gateway.marvel.com/v1/public/characters/1009144",
"comics": {
"available": 33,
"collectionURI": "gateway.marvel.com/v1/public/characters/1009144/comics",
"items": [
{
"resourceURI": "gateway.marvel.com/v1/public/comics/36763",
"name": "Ant-Man & the Wasp (2010) #3"
},
{
"resourceURI": "gateway.marvel.com/v1/public/comics/17553",
"name": "Avengers (1998) #67"
}
]
}
}
]
}
}
console.log('items', data.data.results[0].comics.items)