Loop through several props objects in vue js - javascript

I want to simply loop through several props object data in my component, so for example:
I have created a card component and passing as props objects:
<template>
<div class="container">
<div v-for="(title, index) in titles" :key="index" class="counter">
<div class="title ">
<span>{{ title }}</span>
</div>
<div class="content">
<span>{{ values.value1 }}</span>
</div>
<div>
<Button :prop1="moreActions.prop1" :prop2="moreActions.prop2"
:prop3="moreActions.prop3" />
</div>
</div>
<!-- <div class="counter">
<div class="title">
<span>{{ titles.usersA }}</span>
</div>
<div class="content">
<span>{{ values.values2 }}</span>
</div>
<div>
<Button :prop1="moreActions.prop1" :prop2="moreActions.prop2"
:prop3="moreActions.prop3" />
</div>
</div> -->
// more div here.....
</div>
</template>
<script>
import Button from "../test/testButton";
export Test {
name: "Card",
components: {
Button,
},
**//Comment: I want to loop through each objects ( titles, values, moreActions)**
props: {
titles: {
type: Object,
default: () => ({}),
},
values: {
type: Object,
default: () => ({}),
},
moreActions: {
type: Object,
default: () => ({}),
},
},
};
</script>
and in app.vue , i am passing the data something like this:
export const test = createTest ({
.........
props: // I can say 'data' too
{
titles: {
testA: "testA",
usersA: "TestB",
testc: "test3",
textd: "Test4",
},
values: {
value1: "9000",
value2: "600",
value3: "100",
value4: "2",
ofTotal: " of 9",
},
moreActions: {
prop1: "Test",
prop2: "testIcon",
prop3: "test",
},
}
});
in the second div I can loop through like this and so on:
<div v-for="(value, index) in values" :key="index" class="content">
<span>{{ value }}</span>
</div>
but then it doesnot look goods, I want to loop through every props objects in one v-for loop instead giving v-for loop to each div. how can I do that in efficient way.
Thanks in advance!

Related

Keep track of child component values in parent without using emit?

I have a custom component called HobbyForm which is a simple form with two controls, a checkbox and an input, this component is being called from a parent component called Content, along with other similar 'form' components.
<template>
<form>
<div class="row align-items-center">
<div class="col-1">
<Checkbox id="isHobbyActive" :binary="true" v-model="isActive"/>
</div>
<div class="col-5">
<InputText id="hobby" placeholder="Hobby" type="text" autocomplete="off" v-model="hobby"/>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'HobbyForm',
data() {
return {
hobby: {
isActive: false,
hobby: null
}
}
},
}
</script>
My Content component is something like:
<template>
<language-form></language-form>
<hobby-form v-for="(hobbie, index) in hobbies" :key="index" v-bind="hobbies[index]"></hobby-form>
<Button label="Add Hobby" #click="addHobby"></Button>
</template>
<script>
export default {
name: "Content",
components: {
LanguageForm,
HobbyForm
},
data() {
return {
language: '',
hobbies: [
{
isActive: false,
hobby: null
}
]
};
},
methods: {
addHobby() {
this.hobbies.push({
isActive: false,
hobby: null
});
}
},
};
</script>
The idea is to be able to add more instances of the HobbyForm component to add another hobby record to my hobby data property; but I don't know how to keep track of these values from my parent without using an emit in my child components, since I don't want to manually trigger the emit, I just want to have the data updated in my parent component.
How should I access my child component's data from my parent and add it to my array?
In the current form passing parent data into a child component via v-bind="hobbies[index]" makes no sense as the child component (HobbyForm) has no props so it does not receive any data from the parent...
To make it work:
Remove data() from the child HobbyForm
Instead declare a prop of type Object
Bind form items to the properties of that Object
Pass the object into each HobbyForm
<template>
<form>
<div class="row align-items-center">
<div class="col-1">
<Checkbox id="isHobbyActive" :binary="true" v-model="hobby.isActive"/>
</div>
<div class="col-5">
<InputText id="hobby" placeholder="Hobby" type="text" autocomplete="off" v-model="hobby.hobby"/>
</div>
</div>
</form>
</template>
<script>
export default {
name: 'HobbyForm',
props: {
hobby: {
type: Object,
required: true
}
}
}
</script>
Even tho props are designed to be one way only so child should not mutate prop value, this is something else as you do not mutate prop value, you are changing (via a v-model) the properties of the object passed via a prop (see the note at the bottom of One-Way Data Flow paragraph)
Also change the parent to:
<hobby-form v-for="(hobby, index) in hobbies" :key="index" v-bind:hobby="hobby"></hobby-form>
Demo:
const app = Vue.createApp({
data() {
return {
hobbies: [{
isActive: false,
hobby: null
}]
};
},
methods: {
addHobby() {
this.hobbies.push({
isActive: false,
hobby: null
});
}
},
})
app.component('hobby-form', {
props: {
hobby: {
type: Object,
required: true
}
},
template: `
<form>
<div class="row align-items-center">
<div class="col-1">
<input type="checkbox" id="isHobbyActive" v-model="hobby.isActive"/>
</div>
<div class="col-5">
<input type="text" id="hobby" placeholder="Hobby" autocomplete="off" v-model="hobby.hobby"/>
</div>
</div>
</form>
`
})
app.mount('#app')
<script src="https://unpkg.com/vue#3.1.5/dist/vue.global.js"></script>
<div id='app'>
<hobby-form v-for="(hobby, index) in hobbies" :key="index" v-bind:hobby="hobby"></hobby-form>
<button #click="addHobby">Add Hobby</button>
<hr/>
<pre> {{ hobbies }} </pre>
</div>

How can I get Vue to correctly update data dynamically?

The component I have below allows a user to view a products macro nutrient info and then also modify the serving size which in return updates the macro nutrient amounts.
The issue I'm having is that Im not getting the values to be updated correctly even when using vue set.
I'm using a watcher to run the calcNewNutriValues function.
<template>
<div class="product">
<div class="topbar">
<div class="left">
<p class="left__name">{{ product.name }}</p>
<p class="left__energy">{{ product.energy }}</p>
</div>
<div class="right">
<button class="cancel" #click="removeItem">
<inline-svg
:src="require('../assets/svg/addition-icon.svg')"
></inline-svg>
</button>
</div>
</div>
<div class="details">
<div class="macros">
<p class="details__heading">Macros</p>
<div class="macros__container container">
<div class="wrapper" v-for="(macro, name, index) in product.macros" :key="index">
<p>{{ name }}</p>
<p>{{ product.macros[name] }}</p>
</div>
</div>
</div>
<div class="serving">
<p class="details__heading">Serving Size (g)</p>
<input type="number" placeholder="40" v-model.number="productServSize">
</div>
</div>
</div>
</template>
export default {
data () {
return {
productServSize: 0,
ogServSize: 0,
macros: {
protein: '',
carbs: '',
fats: '',
fibre: ''
},
micros: {},
energy: ''
}
},
props: [
'product',
],
methods: {
serving () {
const num = this.product.servingSize.split(' ')[0]
this.productServSize = parseFloat(num)
this.ogServSize = parseFloat(num)
},
removeItem () {
this.$emit('removeProduct', this.product)
},
calcNewNutriValues () {
Object.keys(this.product.macros).forEach(key => {
let num = parseFloat(this.product.macros[key].split(' ')[0])
let perGram = parseFloat(num / this.ogServSize)
let newTotal = `${(perGram * this.productServSize).toFixed(1)} g`
this.$set(this.macros, key, newTotal)
})
}
},
mounted () {
this.serving()
Object.assign(this.macros, this.product.macros)
this.energy = this.product.energy
},
watch: {
productServSize: {
handler () {
this.calcNewNutriValues()
this.$emit('updatedNutriValues', this.product)
}
}
}
}
It only seems like macros isn't updating because your template displays product.macros instead of macros:
<div class="wrapper" v-for="(macro, name, index) in product.macros" :key="index">
<p>{{ name }}</p>
<!-- <p>{{ product.macros[name] }}</p> DON'T DO THIS -->
<p>{{ macros[name] }}</p>
</div>
demo

How can I work around the limitation of multiple root elements in Vue.js? [duplicate]

This question already has answers here:
A way to render multiple root elements on VueJS with v-for directive
(6 answers)
Closed 2 years ago.
hopefully someone here will be able to help me with this problem.
I have the following data:
[
{
title: 'Header',
children: [
{
title: 'Paragraph',
children: [],
},
],
},
{
title: 'Container',
children: [
{
title: 'Paragraph',
children: [],
},
],
},
]
I want to render this in a list of <div> like this:
<div class="sortable-item" data-depth="1" data-index="0">Header</div> <!-- Parent -->
<div class="sortable-item" data-depth="2" data-index="0">Paragraph</div> <!-- Child-->
<div class="sortable-item" data-depth="1" data-index="1">Container</div> <!-- Parent -->
<div class="sortable-item" data-depth="2" data-index="0">Paragraph</div> <!-- Child-->
I have built a component that would be recursive, this is what I have so far:
<template>
<template v-for="(item, index) in tree">
<div
class="sortable-item"
:data-depth="getDepth()"
:data-index="index"
:key="getKey(index)"
>
{{ item.title }}
</div>
<Multi-Level-Sortable
:tree="item.children"
:parent-depth="getDepth()"
:parent-index="index"
:key="getKey(index + 0.5)"
></Multi-Level-Sortable>
</template>
</template>
<script>
export default {
name: 'MultiLevelSortable',
props: {
tree: {
type: Array,
default() {
return [];
},
},
parentDepth: {
type: Number,
},
parentIndex: {
type: Number,
},
},
methods: {
getDepth() {
return typeof this.parentDepth !== 'undefined' ? this.parentDepth + 1 : 1;
},
getKey(index) {
return typeof this.parentIndex !== 'undefined' ? `${this.parentIndex}.${index}` : `${index}`;
},
},
};
</script>
As you can see not only I have a <template> as the root element I also have a v-for, two "no no" for Vue.js. How can I solve this to render the list of elements like I pointed out above?
Note: I have tried vue-fragment and I was able to achieve the structure I wanted, but then when I tried using Sortable.js it didn't work, as if it wouldn't recognise any of the .sortable-item elements.
Any help will be greatly appreciated! Thank you!
Thanks to #AlexMA I was able to solve my problem by using a functional component. Here is what it looks like:
import SortableItemContent from './SortableItemContent.vue';
export default {
functional: true,
props: {
tree: {
type: Array,
default() {
return [];
},
},
},
render(createElement, { props }) {
const flat = [];
function flatten(data, depth) {
const depthRef = typeof depth !== 'undefined' ? depth + 1 : 0;
data.forEach((item, index) => {
const itemCopy = item;
itemCopy.index = index;
itemCopy.depth = depthRef;
itemCopy.indentation = new Array(depthRef);
flat.push(itemCopy);
if (item.children.length) {
flatten(item.children, depthRef);
}
});
}
flatten(props.tree);
return flat.map((element) => createElement('div', {
attrs: {
'data-index': element.index,
'data-depth': element.depth,
class: 'sortable-item',
},
},
[
createElement(SortableItemContent, {
props: {
title: element.title,
indentation: element.indentation,
},
}),
]));
},
};
The SortableItemContent component looks like this:
<template>
<div class="item-content">
<div
v-for="(item, index) in indentation"
:key="index"
class="item-indentation"
></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">{{ title }}</div>
</div>
</div>
</template>
<script>
export default {
name: 'SortableItemContent',
props: {
title: String,
indentation: Array,
},
};
</script>
Given the data I have posted on my question, it now renders the HTML elements like I wanted:
<div data-index="0" data-depth="0" class="sortable-item">
<div class="item-content">
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Header</div>
</div>
</div>
</div>
<div data-index="0" data-depth="1" class="sortable-item">
<div class="item-content">
<div class="item-indentation"></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Paragraph</div>
</div>
</div>
</div>
<div data-index="1" data-depth="0" class="sortable-item">
<div class="item-content">
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Container</div>
</div>
</div>
</div>
<div data-index="0" data-depth="1" class="sortable-item">
<div class="item-content">
<div class="item-indentation"></div>
<div class="item-wrapper">
<div class="item-icon"></div>
<div class="item-title">Paragraph</div>
</div>
</div>
</div>
Thank you again #AlexMA for the tip on Functional Components.

How do I fetch JSON data with Vue and Axios

I'm trying to fetch product data from a JSON file, but can't get it to work.
I've tried several things and searched the internet for a solution but none of the examples on the internet equals my situation.
I'm new to both vue and axios, so please excuse my ignorance.
This is what I have so far:
Vue.component('products',{
data: {
results: []
},
mounted() {
axios.get("js/prods.json")
.then(response => {this.results = response.data.results})
},
template:`
<div id="products">
<div class="productsItemContainer" v-for="product in products">
<div class="productsItem">
<div class="">
<div class="mkcenter" style="position:relative">
<a class="item">
<img class="productImg" width="120px" height="120px" v-bind:src="'assets/products/' + product.image">
<div class="floating ui red label" v-if="product.new">NEW</div>
</a>
</div>
</div>
<div class="productItemName" >
<a>{{ product.name }}</a>
</div>
<div class="mkdivider mkcenter"></div>
<div class="productItemPrice" >
<a>€ {{ product.unit_price }}</a>
</div>
<div v-on:click="addToCart" class="mkcenter">
<div class="ui vertical animated basic button" tabindex="0">
<div class="hidden content">Koop</div>
<div class="visible content">
<i class="shop icon"></i>
</div>
</div>
</div>
</div>
</div>
</div>
`
})
new Vue({
el:"#app",
});
The json file is as follows
{
"products":[
{
"name": "Danser Skydancer",
"inventory": 5,
"unit_price": 45.99,
"image":"a.jpg",
"new":true
},
{
"name": "Avocado Zwem Ring",
"inventory": 10,
"unit_price": 123.75,
"image":"b.jpg",
"new":false
}
]
}
The problem is only with the fetching of the data from a JSON file, because the following worked:
Vue.component('products',{
data:function(){
return{
reactive:true,
products: [
{
name: "Danser Skydancer",
inventory: 5,
unit_price: 45.99,
image:"a.jpg",
new:true
},
{
name: "Avocado Zwem Ring",
inventory: 10,
unit_price: 123.75,
image:"b.jpg",
new:false
}
],
cart:0
}
},
template: etc.........
As the warnings suggest, please do the following:
Rename the data array from results to products since you are referencing it by the latter one as a name during render.
Make your data option a function returning an object since data option must be a function, so that each instance can maintain an independent copy of the returned data object. Have a look at the docs on this.
Vue.component('products', {
data() {
return {
products: []
}
},
mounted() {
axios
.get("js/prods.json")
.then(response => {
this.products = response.data.products;
});
},
template: `
//...
`
}
<div id="products">
<div class="productsItemContainer" v-for="product in products">
<div class="productsItem">
...
Also, since you're not using CDN (I think), I would suggest making the template a component with a separate Vue file rather than doing it inside template literals, something like that:
Products.vue
<template>
<div id="products">
<div class="productsItemContainer" v-for="product in products">
<div class="productsItem">
<!-- The rest of the elements -->
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Products',
data() {
return {
products: []
}
},
mounted() {
axios
.get("js/prods.json")
.then(response => {
this.products = response.data.products;
});
}
}
</script>
And then in your main JS file or anywhere else requiring this component:
import Products from './components/Products.vue';
new Vue({
el: '#app',
data() {
return {
//...
}
},
components: {
Products
}
})
<div id="app">
<Products />
</div>

Nuxt.js/Vue.js resuable modal

My question is about core logic for the thing below
1) I have a Section Component and a Modal Component
2) In the Section Component i have an array of objects with title and description
3) In the Modal Component i have a template for the modal (im using Bulma framework for the modal)
Now, in the Section i have 7 buttons with a v-for loop to assign 'is-active' class and on each button click an individual modal opens.
Question: How do i parse data into the modal ? I want my modal to be reusable. So at the start my modal is empty at all. On button1 click i have title1 descr1, on button2 title2 descr2 and etc
My code:
Section Component:
<template>
<base-section name="clusters">
<div class="section-map">
<button
class="section-btn"
v-for="(item, index) in sectionItems"
:key="index"
:class="[`section-btn-num${index + 1}`, {'is-active': item.state}]"
#click="toggleModal(item)"
>
<div class="section-btn__text">
{{ item.title }}
</div>
</button>
</div>
<div class="modal"
v-for="(item, index) in sectionItems"
:key="index"
:class="{'is-active': item.state}"
>
<div class="modal-background"></div>
<div class="modal-content">
</div>
<button
#click="item.state = false"
class="modal-close is-large"
aria-label="close">
</button>
</div>
</base-section>
</template>
<script>
import BaseSection from './Section';
import BaseModal from './Modal';
export default {
components: {
BaseSection,
BaseModal,
},
methods: {
toggleModal: (item) => {
item.state = !item.state;
}
},
data() {
return {
sectionItems: [
{
title: 'title1',
},
{
title: 'title2',
description: 'descr',
},
{
title: 'title3',
description: 'descr',
},
{
title: 'title4',
description: 'descr',
},
{
title: 'title5',
description: 'descr',
},
{
title: 'title6',
description: 'descr',
},
{
title: 'title7',
description: 'descr',
},
].map(item => ({ ...item, state: false }))
};
}
};
</script>
<styles>
/* are skipped */
</styles>
Modal Component:
<template>
<div class="modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="modal__header">
<slot name="modal__header">
</slot>
</div>
<div class="modal__body">
<slot name="modal__body">
</slot>
</div>
</div>
<button
#click="item.state = false"
class="modal-close is-large"
aria-label="close">
</button>
</div>
</template>
<script>
import ClustersSection from './ClustersSection';
export default {
components: {
ClustersSection,
},
props: {
}
},
};
</script>
<style lang="scss">
</style>
Since you already defined slots in your component you can pass data to it via slots. E.g.
<your-modal-component>
<template slot="modal__header">
header bla bla a
</template>
<template slot="modal__body">
body anything here like <p> hehe</p>
</template>
</your-modal-component>

Categories