Selected values in a v-for loop of components - javascript

I implemented a component that have a select element inside and it is like following more or less:
<!-- child component -->
<template>
<b-form-select
v-model="selectedProject"
:options="projects"
#change="changedValue">
<template v-slot:first>
<option :value="null" disabled>-- Please select a project --</option>
</template>
</b-form-select>
</template>
<script>
export default {
name: 'AllocationItem',
props: {
projects: {
type: Array,
default: () => [{ value: Number}, { text: String}]
}
},
data() {
return {
selectedProject: null,
}
},
methods: {
changedValue(value) {
this.selectedProject = value;
}
}
}
</script>
I use this component in a parent componenent where it can be possible add other AllocationItem clicking on a button.
To do that i have used an array where i push a new item every time that there is a click on add button (i don't know if it's the right way...)
Follow parent component code:
<!-- parent component -->
<template>
<b-button class="btnAction" #click="addItem()">Add</b-button>
<b-button class="btnAction" #click="sendAllocation()">Send</b-button>
<b-row v-for="allocation in resourceItem.allocations" v-bind:key="allocation.id">
<allocation-item v-bind:projects="projects"></allocation-item>
</b-row>
</template>
<script>
export default {
name: 'Management',
components: {
AllocationItem
},
data() {
return {
allocations: []
}
},
methods: {
addItem() {
this.allocations.push(AllocationItem);
},
sendAllocation() {
this.allocations.forEach((allocation) => {
// I WOULD LIKE TO HAVE ALL SELECTED VALUE HERE!!!
});
},
},
created() {
const dataProjects = this.getProjectsData();
dataProjects.then(response => {
this.projects = response.map((item) => {
return {value: item.id, text: item.name}
})
});
}
</script>
In my application i have another button, send button, that should be read values selected in all child component (allocation-item).
How can i do to have an array with that selected values?
Thank you in advance to all

This depends on the relationship from your 'Send Button'-component with your parent component.
You can either:
Use the $emit() method to propagate data up the component tree to the shared parent component. Then prop it down to the 'Send Button'-component.
Have a single source of truth by using Vuex. This is a store that keeps all your data in a centralised object.
Perhaps you can provide us with more information on the project structure?

First of all, ask yourself if this component is being used anywhere else but here. If you only use it once, build it into the parent component and your problems are solved.
Otherwise I'd go with #laurensvm's approach of using emit or Vuex.

After some researches on google i have founded a solution. I don't know if it the right way but it works fine and it seems clean.
My solution consists to use emit on child component and v-model on parent like show following example.
<!-- child component -->
<template>
<b-form-select
v-model="selectedProject"
:options="projects"
#change="changedValue">
<template v-slot:first>
<option :value="null" disabled>-- Please select a project --
</option>
</template>
</b-form-select>
</template>
<script>
export default {
name: 'AllocationItem',
props: {
projects: {
type: Array,
default: () => [{ value: Number}, { text: String}]
},
value: {
type: Number
}
},
data() {
return {
selectedProject: this.value
}
},
methods: {
changedValue(value) {
this.$emit('input', value);
}
}
}
</script>
And on parent using an array variable on v-model.
Something like this:
<!-- parent component -->
<template>
<b-container>
<b-row>
<b-button class="btnAction" variant="success" #click="addItem(index)">Add</b-button>
<b-button class="btnAction" #click="sendAllocation(index)">Send</b-button>
</b-row>
<b-row v-for="(allocation, index) in resourceItem.allocations" v-bind:key="allocation.id">
<allocation-item v-bind:projects="projects" v-model="allocationItemSelected[index]"></allocation-item>
</b-row>
</b-container>
</template>
See a runnable example clicking on codesandbox link below:
https://codesandbox.io/s/vue-template-4f9xj?autoresize=1&expanddevtools=1&fontsize=14&hidenavigation=1&moduleview=1

Related

Getting mutation error in Nuxt JS while binding form with state

I am trying to bind my form's fields with my vuex store in Nuxt JS. It works fine with normal text fields with get() set() in computed. But having trouble in customizing getting and setting manually. I am trying to push objects to an array in a specific format from my template to store and also keep the binding among them. Here is my code:
<template>
<div class="container setting-form-area-business">
<b-form-group v-for="(input, index) in phoneNumbers" :key="`phoneInput-${index}`">
<label>Mobile Number {{index+1}}*</label>
<b-input-group>
<b-form-input v-model="input.phone" #input="updateStore" class="custom-form-input-business">
</b-form-input>
<b-input-group-append v-show="phoneNumbers.length > 1">
<b-button class="mobile-number-remove-btn" #click="removeField(index, phoneNumbers)"></b-button>
</b-input-group-append>
</b-input-group>
</b-form-group>
<b-form-group>
<b-button class="jh-btn2" #click="addField">Add More Mobile Number</b-button>
</b-form-group>
</div>
</template>
<script>
export default {
props: [
'visited'
],
data() {
return {
phoneNumbers: this.$store.state.business.formvalue.mobileNumber.length ? this.$store.state.business.formvalue
.mobileNumber : [{
phone: ""
}],
}
},
computed: {
mobilenumbers() {
return this.$store.state.business.formvalue.mobileNumber
},
},
methods: {
addField() {
this.phoneNumbers.push({
value: ""
});
},
removeField(index, fieldType) {
fieldType.splice(index, 1);
console.log('fieldType', fieldType);
this.emitErrorStatus();
},
updateStore() {
this.$store.commit('business/setformmobileNumber', {
mobileNumber: this.phoneNumbers
})
}
},
}
</script>
this was working fine when I was in vue, but coming to nuxt, it is giving me error
[vuex] do not mutate vuex store state outside mutation handlers.
As told by the error, you should not mutate the state. There are several ways to handle this one. A quick search here could give you a lot of answers.
This is mine (using Lodash's cloneDeep): https://stackoverflow.com/a/66262659/8816585

Vue.js - no access to this.$parent when child component is inside <transition>

What I want: I have two components, the parent component (Wall.vue) and the child component (PostItem.vue). Every PostItem has a delete button. On click, a request to my API is sent and the item gets deleted from the database. Then I want to call the getPosts function of the parent component to get all the posts again (this time without the deleted post).
The Problem: Inside the child component, I have no access to the this.$parent Object (or more specific, it's just empty and doesn't contain the functions), so I can't call the getPosts-Function. When I remove the <transition-group> in the parent component that surrounds also the child-component, everything works fine.
What is the problem here?
Parent-Component (Wall.vue)
template-portion:
<template>
<div class="Wall view">
<transition-group name="wallstate">
<template v-else-if="messages">
<PostItem
v-for="(message, index) in messages"
:key="index"
:message="message"
:index="index"
class="PostItem"
/>
</template>
<h1 v-else>
Could not load messages. Please try later.
</h1>
</transition-group>
</div>
</template>
script-portion:
<script>
import { mapGetters } from 'vuex';
import { postsAPI } from '../services/posts.service.js';
import PostItem from '../components/PostItem.vue';
export default {
components: {
PostItem,
},
data() {
return {
messages: null,
};
},
methods: {
getPosts() {
///////Do stuff
}
}
};
</script>
Child-Component (PostItem.vue)
template-portion
<template>
<div class="PostItem__message frosted">
<p class="PostItem__messageContent">{{ message.content }}</p>
<p>
by: <strong>{{ message.user.username }}</strong>
</p>
<a
#click="deletePost"
:data-id="message._id"
v-if="message.user._id === user.id"
>
Delete
</a>
</div>
</template>
script-portion:
<script>
import { postsAPI } from '../services/posts.service.js';
import { mapGetters } from 'vuex';
export default {
name: 'PostItem',
props: {
message: {
type: Object,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
...mapGetters({
user: 'auth/user',
}),
},
methods: {
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$parent.getPosts(); <-------- PROBLEM HERE
});
},
},
};
</script>
It's generally considered a bad practice to use this.$parent (it couples the components and reduces encapsulation / code clarity.) The child component should emit an event when it wants to send information to an ancestor component.
Remove the direct access and $emit an event called 'deleted':
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$emit('deleted'); // Emitting the event
});
},
The parent should listen for that deleted event and run an event handler:
<PostItem
v-for="(message, index) in messages"
:key="index"
:message="message"
:index="index"
class="PostItem"
#deleted="getPosts"
/>
The parent will call the getPosts method when triggered by the #deleted event listener.
inside the methods part, instead of :
methods: {
deletePost(e) {
const id = e.target.dataset.id;
postsAPI.removeOne(id).then((res) => {
this.$parent.getPosts();
});
},
},
you may try this:
methods: {
deletePost(e) {
const id = e.target.dataset.id;
let self=this;
postsAPI.removeOne(id).then((res) => {
self.$parent.getPosts();
});
}
Because of the scope chain, 'this' inside .then() does not point to the same variable environment as variable 'self' does. So perhaps it's the reason it fails to work.

How to pass props to component in slot?

I am developing a Vue app with pimcore and twig in the backend. I have to create a component that receives the slot (another component), and render it inside, but with dynamic props.
Here is root in viani.twig.html:
<div>
<viani-accordion>
<viani-filter-checkbox v-slot="{filterRows}"></viani-filter-checkbox>
</viani-accordion>
</div>
There is nothing special. viani-accordion is a parent component and the viani-filter-checkbox is a slot, which I have to render with appropriate props.
Here you can see the VianiAccordion.vue:
<template>
<div class="wrapper">
<AccordionTitle v-for="(item, index) in dataToRender" :item="item" :key="index">
/*I think this is incorrect, but I'm trying to prop data that I need in viani-filter-checkbox*/
<slot :filter-rows="item.items"></slot>
</AccordionTitle>
</div>
</template>
<script>
import AccordionTitle from './Accordion-Title';
export default {
name: "Viani-Accordion",
components: {AccordionTitle},
data() {
return {
dataToRender: [
{
name: 'first item',
items: [
{
name: 'oil olive',
quantity: 10,
},
{
name: 'subitem 2',
quantity: 11,
},
]
},
{
name: 'second item',
items: [
{
name: 'subitem 1',
quantity: 10,
},
{
name: 'subitem 2',
quantity: 11,
},
]
}
]
}
},
}
</script>
Then I have another deeper child component Accordion-Title.vue that is responsible for rendering the slot (so I have to pass the slot through the multiple components):
<template>
<div v-if="isOpen" class="child-wrapper">
/*I think this is incorrect, but I'm trying to prop data that I need in viani-filter-checkbox*/
<slot :filterRows="item.items"></slot>
</div>
</template>
<script>
export default {
name: "Accordion-Title",
props: {
item: {
type: Object,
default: null
}
}
}
</script>
and finally Viani-FiltersCheckbox.vue:
<template>
<div>
//child component which we don't need in this case
<FilterCheckboxRow v-for="(item, index) in filterRows" :item="item" :key="index"/>
</div>
</template>
<script>
import FilterCheckboxRow from './FilterCheckboxRow'
export default {
name: "VianiFilterCheckbox",
components: {
FilterCheckboxRow
},
props: {
//here I expect to get array to render, but got default value (empty array)
filterRows: {
type: Array,
default: function () {
return []
}
},
},
}
</script>
So I need to pass the props (filterRows) to the component (Viani-FiltersCheckbox.vue), which is rendered as a slot. I have read this and this, but still don't get where the mistake and how to get the props I need.
It looks like you're trying to access your props through props.XXX. That's typically only done in templates for functional components. Otherwise, the props would be accessed without the props. prefix (i.e., props.item.items should be item.items).
And to pass filterRows from the scope data to the child component, declare a <template>, and then move your child into that, binding filterRows there:
<viani-accordion>
<!-- BEFORE: -->
<!-- <viani-filter-checkbox v-slot="{filterRows}"></viani-filter-checkbox> -->
<template v-slot="{filterRows}">
<viani-filter-checkbox :filterRows="filterRows"></viani-filter-checkbox>
</template>
</viani-accordion>

How to call api when change value in vue multiselect

How to call api when there is a change value event in vue multiselect?
i want change value in multiselect , call api and show in console. The following is my code.
<template>
<div>
<multiselect v-model="value" placeholder="Select Your Currency" label="title" track-by="title" :options="options" :option-height="10" :show-labels="false">
<template slot="singleLabel" slot-scope="props"><img class="option__image" :src="props.option.img" alt="No Man’s Sky"><span class="option__desc"><span class="option__title">{{ props.option.title }}</span></span></template>
<template slot="option" slot-scope="props"><img class="option__image" :src="props.option.img" alt="No Man’s Sky">
<div class="option__desc"><span class="option__title" :id="props.option.id">{{ props.option.title }}</span><span class="option__small">{{ props.option.desc }}</span></div>
</template>
</multiselect>
</div>
</template>
<script>
import Vue from 'vue';
import Multiselect from 'vue-multiselect'
Vue.component('multiselect', Multiselect);
export default {
props: ['options'],
components: {
Multiselect
},
data () {
return {
value: { title: 'Bitcoin', img: 'https://coinnik.com/uploads/crypto-
logos/06fa2ab3d5a0f1d60e7d236aeccd6c6a.png' },
}
},
methods: {
}
}
</script>
app.js
methods: {
stepChanged(step) {
this.currentstep = step;
},
apiCalc(){
let self = this;
axios.get('/ajax-calculation?includes=direction,direction.receiveCurrency&send_currency=2').then((response) => {
self.calcApi = response.data;
console.log('api:',self.calcApi.currency_id);
// self.calcApi.currency_id;
})
}
},
components:{
'multiselect': Multiselect
},
created() {
this.apiCalc();
},
});
html
<div class="col-md-12 mb-2">
<multiselect :options="[ { title: 'Tether', img: 'https://coinnik.com/uploads/crypto-logos/006fe133d48ea7cd45cf8ccb8cb7ec42.png' },
{ title: 'value', img: 'https://coinnik.com/uploads/crypto-logos/006fe133d48ea7cd45cf8ccb8cb7ec42.png' }]">
</multiselect>
</div>
How to define a method that I use when changing item then call api and show inside the console?
You can use the deep watch on the value variable.
watch: {
'value': {
handler: function (after, before) {
this.apiCalc();
},
deep: true
},
},
Ok, so first, you can use the #input event to recognize when something has been picked, like so:
<multiselect v-model="value" :options="options" #input="doApi" multiple></multiselect>
Then in doApi, you can check the current value.
doApi() {
console.log(this.value);
}
The value of this.value will be an array of what you have picked. You can pass these to your API. Full codepen here: https://codepen.io/cfjedimaster/pen/gOOjjrb?editors=1111
Have you tried to use the #select event? Vue Multiselect's documentation includes several events you can try, check it out here: https://vue-multiselect.js.org/#sub-events
So try
<multiselect v-model="value" :options="options" #select="actionToExecute" multiple></multiselect>
And then define that in your methods. I'm currently using vue multiselect too so let me know if that works for you.

How to import javascript from component to another component?

i'm trying to do mini shop app in VueJS without backend, only adding items to shopping cart.
I have component One(Nmdgrey.vue), where i have e.g. boots and "Add to cart" button.
<button #click="addToCart">Add to cart</button>
And function in first component:
methods: {
addToCart() {
const boots = { name: 'adidas nmd' };
this.cart.push(boots);
}
}
And i have component Two, where i have shopping cart
<div v-for="item in cart">
{{item.name}}
</div>
And JS
import Nmdgrey from '#/components/Nmdgrey.vue';
export default {
name: 'Shoppingcart',
components:
Nmdgrey,
data() {
return {
cart: [
{ name: 'adidas adizero' },
]
}
},
};
How i can add boots from component One to list in component Two?
I have this.cart.push(boots); in component One but it didn't work
This is what i want but button didn't work: codesandbox
Use $emit, when child components need to communicate with parent.
Refer official doc:Listening-to-Child-Components-Events
Nmdgrey.vue
<template>
<div>
<!-- component 1 -->
<button #click="add">Add to cart</button>
</div>
</template>
<script>
export default {
name: "Numdgrey",
methods: {
add() {
const boots = { name: "adidas nmd" };
this.$emit("add", boots);
}
}
};
</script>
Shoppingcart.vue
<template>
<div>
<!-- component 2 -->
<nmdgrey #add="addCart"></nmdgrey>
<br>
<div v-for="(item, index) in cart" :key="index">{{item.name}}</div>
</div>
</template>
<script>
import Nmdgrey from "./Nmdgrey.vue";
export default {
name: "ShoppingCart",
components: {
Nmdgrey
},
data() {
return {
cart: [{ name: "adidas adizero" }]
};
},
methods: {
addCart(good) {
this.cart.push(good);
}
}
};
</script>
Codesandbox demo : https://codesandbox.io/s/z2qz6oy8yp
From your post I've deduced that what you want to do is to share data from two or more Vue components. For this purpose, you could use Vuex, which provides centralized state management.
This way you could use a vuex mutation to add items to the cart, which could be used from any component. You would also be able to retrieve the cart data from any of them by using vuex getters.
you need to create a post method where you take over everything you need from one page to another page. after that everything will get into the cart and you will be able to create your page.
Codesandbox demo : https://codesandbox.io/s/94l44j8j14
Use props to pass the values to cart component.
Nmdgrey.vue
<template>
<div>
<b>Component 1 : NmdGrey</b><br><br>
<button v-for="(product, index) in boots" :key="index"
#click="addToCart(product.name)" >Add {{product.name}} to Cart</button>
<br><br><hr><br>
<shoppingcart :cart="cart" />
</div>
</template>
<script>
import shoppingcart from './shoppingcart.vue';
export default {
name: 'Nmdgrey',
components:{shoppingcart},
data() {
return {
boots:[{name: 'adidas adizero'}, {name: 'puma walker'}, {name: 'nike shoe'}, {name: 'adidas plain'}],
cart:[],
}
},
methods: {
addToCart(boots) {
this.cart.push({ name: boots });
}
}
}
</script>
Shoppingcart.vue
<template>
<div>
<b>Component 2 : Shopping cart</b>
<br>
<br>
<div v-for="(product, index) in cart" :key="index">{{product.name}}</div>
</div>
</template>
<script>
export default {
name: "Shoppingcart",
props: ["cart"],
data() {
return {};
}
};
</script>
Well, If your application is small, then you can create Vue mixins for addToCart and call it whenever you'll require in your component.
Similar to the methods, you can share data across the components with the use of mixins.
Here is the mixins official docs
Here is the working JsFiddle
Hope this helps!

Categories