Binding v-model inside v-for - javascript

I have an array of objects, which i need to pass to child component via v-for to process in. Child component must tell the parent component that its data was changed; I tried to do it this way:
Parent component:
<template>
<row
v-for="(calc, index) in calculators"
:key="index">
<child-component v-model:calc="calculators[index]"></child-component>
</row>
<button #click="addCalculator"
</template>
<script>
....
data() {
return {
calculators: [
{
"name":"some name",
..
},
{
"name":"some name 2",
..
}
]
}
},
methods: {
addCalculator() {
this.calculators.push(someDefaultCalculatorObject)
}
}
</script>
Child component:
<template>
<input v-model="calc.name"/>
</template>
<script>
...
props: {
calc:Object
},
setup(props) {
const calc = ref(props.calc)
return calc
},
watch: {
calc: {
handler(val) {
this.$emit('update:calc', val)
},
deep: true
}
}
</script>
But when i'm trying to edit value of input of one object, every object in array updating to this value. How can i bind objects in array inside v-for?

Related

Vue.js - prop not reactive when initialized via PHP

I have a parent component (item-list) to which I pass items from a PHP view (Laravel/Blade). The item-list consists of list-items (child component). Inside a list-item I want to modify/delete this specific item. After triggering the deletion inside the child component, an event will be emitted back to parent component where the item will be finally removed from the items list.
It seems like this approach is not reactive. If I instead use data() with some dummy data for the list items of a prop filled via PHP, it's reactive.
How can I guarantee reactivity for props passed via PHP?
Parent component:
<template>
<my-list-item
v-for="item of items"
:key="item.id"
:item="item"
#remove="removed"
></my-list-item>
<br />
</template>
<script>
import MyListItem from './my-list-item'
export default {
// parent component
name: "my-list",
components: {
MyListItem
},
data() {
return {
// this will work. items are reactive
// items: [
//
// { id: 1, message: 'foo' },
// { id: 2, message: 'bar' },
//
// ]
}
},
props: {
// not reactive, items come from PHP view (Laravel/Blade).
items: {
type: Array,
default: undefined
}
},
methods: {
removed: function (item) {
_.remove(this.items, item);
}
}
};
</script>
Child component:
<template>
<!-- item stuff here (label, etc.)
...
-->
{{ item.message }}
<!-- remove link -->
remove
</template>
<script>
// child
export default {
name: "my-list-item",
props: {
item: {
type: Object,
}
},
emits: [
'remove'
],
methods: {
remove: function (item) {
this.$emit("remove", item);
}
},
};
</script>
PHP view (list.blade.php):
#extends('dashboard')
#section('title', 'List items')
#section('content')
<my-list :items="{{ Session::get('items') }}"></my-list>
#endsection

Vue array prop in child component not updating

I have a model from a backend where the property that normally contains an array of elements can be nullable. When this happens I'll init it to an empty array. However when doing that it seems to break my ability to update the array in the child component. Sample code can be found below.
Vue.component('Notes', {
template: '<div>{{items}}<ul><li v-for="item in items">{{ item.text }}</li></ul><button #click="add">Add</button></div>',
props: {
items: Array,
},
methods: {
add() {
console.log('added');
this.items.push({ text: "new item" });
}
}
});
new Vue({
el: "#demo",
data() { return { model: { } }},
created() { if(!this.model.items) this.model.items = []; }
});
<script src="https://unpkg.com/vue"></script>
<div id="demo">
<Notes :items="model.items" />
</div>
If data in the main component is model: { items : [] } everything works fine. But I don't have control over the backend data to guarantee that.
In your Notes component, you declare a model in the data, then, just underneath, you add an items[] if one doesn't exist already. This is not a good practice, and could be the cause of your problems. Vue needs to know about all the properties on objects it's watching. They need to be there when Vue first processes the object, or you need to add them with Vue.set().
You should emit an event to update the prop in the parent component
in child component :
this.$emit('add-item',{
text: "new item"
});
in parent template add a handler for the emitted event :
<Notes :items="model.items" #add-item="AddItem" />
Vue.component('Notes', {
template: '<div>{{items}}<ul><li v-for="item in items">{{ item.text }}</li></ul><button #click="add">Add</button></div>',
props: {
items: Array,
},
methods: {
add() {
console.log('added');
this.$emit('add-item', {
text: "new item"
});
}
}
});
new Vue({
el: "#demo",
data() {
return {
model: {
items: [] //init it here in order to avoid reactivity issues
}
}
},
methods: {
AddItem(item) {
this.model.items.push(item)
}
},
});
<script src="https://unpkg.com/vue"></script>
<div id="demo">
<Notes :items="model.items" #add-item="AddItem" />
</div>

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>

Vue Component Props only available on $vnode

I'm new to the World of Vue.js and I have to build a recursive Component renderer that turns JSON into rendered Vue components.
So far the recursive rendering works just fine, except for the props I'm passing to the createElement function (code below ;) ) is not available as props, but inside the $vnode.data object.
Any ideas what I'm missing?
The mock JSON I'm using looks like this:
{
"_id": "aslwi2",
"template": "NavigationBar",
"data": {},
"substructure": [
{
"_id": "assd2",
"template": "NavigationBarListItem",
"data": {
"title": "NavList Item 1"
}
},
{
"_id": "a2323uk",
"template": "NavigationBarListItem",
"data": {
"title": "NavList Item 2"
}
}
]
}
My Recursive Rendering Component:
const createComponent = function(node ,h){
if(!node || !node.template){
return;
}
let children = [];
if(node.substructure instanceof Array){
node.substructure.map( async childNode => {
const childComponent = createComponent(childNode, h);
children.push(childComponent);
});
}
return h(node.template, {xdata : clone(node.data)}, children.length > 0 ? children : null );
};
export default Vue.component('Structure', {
render: function(createElement){
if(!this.structure) return;
const component = createComponent(this.structure, createElement, registry);
console.log(component);
return component;
},
props: {
structure: {
type: Object
}
}
})
And my dynamically instantiated components:
<!-- Component: NavBar -->
<template>
<div class="nav-bar">
<slot></slot>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object
}
}
}
</script>
<!-- Component: NavBarListItem -->
<template>
<div class="nav-bar-item">
{{title}}
</div>
</template>
<script>
export default {
data () {
return {
title : "default"
}
},
created() {
console.log('list item: ', this)
}
}
</script>
Inside the log of the created method in my list item component we find the passed props within the $vnode...
The problem is here:
return h(node.template, {xdata : clone(node.data)}, ...
I don't know what xdata is but it isn't something you should be passing to h. The options are documented at https://v2.vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth.
To pass a props you'd use props.
return h(node.template, {props : clone(node.data)}, ...
While I'm struggling a little to follow the intent behind your code it looks like you may also have a problem with NavBarListItem not defining a title prop. You can't pass a title unless it's defined as a prop. Defining it in data isn't the same thing.

Adding properties to a vue component

I'm trying to create a "Dropdown List" component. This component should take in a title and a route. I'm having trouble setting up the javascript for this component within the parent. Here is what I'm working with.
PARENT COMPONENT:
<template>
<UserDropdownList v-for="item in items"></UserDropdownList>
</template>
<script>
import SignUp from "../forms/SignUp";
import SignIn from "../forms/SignIn";
import UserDropdownList from "./UserDropdownList";
import { mixin as clickaway } from 'vue-clickaway';
export default {
mixins: [ clickaway ],
components: {
SignUp,
SignIn,
UserDropdownList
},
data: function () {
return {
items: [
{ title: 'Profile', route: "/profile" },
{ title: 'Users', route: "/users" }
]
}
},
computed: {
isLoggedIn () {
return this.$store.getters.isLoggedIn
},
userName () {
return this.$store.getters.currentUser.userName
},
isDropdownOpen () {
return this.$store.getters.dropdownIsOpen
}
},
methods: {
signOut: function(event) {
this.$store.commit("CLOSE_DROPDOWN");
this.$store.commit("LOGOUT");
this.$router.push('/');
},
openDropdown: function() {
if (event.target.id != "button") {
this.$store.commit("OPEN_DROPDOWN");
}
},
closeDropdown: function() {
this.$store.commit("CLOSE_DROPDOWN");
}
}
}
</script>
USER DROPDOWN LIST
<template>
<li v-on:click="closeDropdown"><router-link to={{ item.route }} id="button">{{ item.title }}</router-link></li>
</template>
<script>
export default {
}
</script>
<style>
</style>
ERRORS:
Property or method "item" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
Error in render: "TypeError: Cannot read property 'title' of undefined"
Anyone understand what I am doing wrong?
you didn't pass item as a props to UserDropdownList
first, pass item as a prop to UserDropdownList
<template>
<UserDropdownList v-for="item in items" v-bind:item="item"></UserDropdownList>
</template>
then, setup UserDropdownList to receive item prop
export default {
props: ['item']
}

Categories