Vue.js - prop not reactive when initialized via PHP - javascript

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

Related

Binding v-model inside v-for

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?

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>

Mutate props in Vue.js 3 - or how do I reflect a prop's changes to it's parents

I am migrating things from Vue.js 2 to Vue.js 3. During my migration, I just mentioned that eslint does warn me, because I am mutating props.
This is an example of an element that causes this:
<template>
<ToggleField
v-model="item.checked"
:name="`item.${name}.checked`"/>
</template>
<script>
import ToggleField from "./ToggleField";
export default {
name: 'TestField',
components: {ToggleField},
props: {
name: {
type: String,
required: true,
},
item: {
type: Object,
},
},
}
</script>
This element is deeply nested and every parent element passes the :item-attribute to the next "level" until it's finally displayed and changeable due v-model.
This is an example:
Parent view
<template>
<CustomForm name="test" :item="item" />
<!-- Reflect changes on item here -->
{{ item }}
</template>
<script>
import CustomForm from "./CustomForm";
export default {
components: {
CustomForm
},
data: () => ({
item:
{name: 'Foo', 'checked': false},
}),
}
</script>
CustomForm
<template>
<!-- Do other fancy stuff here -->
<TestField :name="name" :item="item"/>
</template>
<script>
import TestField from "./TestField";
export default {
name: 'CustomForm',
components: {TestField},
props: {
name: {
type: String,
required: true,
},
item: {
type: Object,
},
},
}
</script>
TestField
<template>
<ToggleField
v-model="item.checked"
:name="`item.${name}.checked`"/>
</template>
<script>
import ToggleField from "./ToggleField";
export default {
name: 'TestField',
components: {ToggleField},
props: {
name: {
type: String,
required: true,
},
item: {
type: Object,
},
},
}
</script>
So my question is: How can I update the item and reflect the changes to it's parent (and it's parent, and it's parent again, if necessary) without running into the prop-mutation?

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>

Strange behavior when removing an object from an array of objects in Vue

I have a Vue component and a root Vue instance. The instance contains an array of objects (for products) and the component is displayed in my HTML using a v-for loop for each product. This is what products.js looks like:
/**
* Component to show products
*/
Vue.component('product', {
props: ['product'],
data: function() {
return {
localProduct: this.product
};
},
template: `<div class="products">
<span>{{ localProduct.product }}</span>
Remove
</div>`,
methods: {
remove: function() {
var removeIndex = productsList.products.map(function(i) { return i.id; }).indexOf(this.localProduct.id);
productsList.products.splice(removeIndex, 1);
}
}
});
/**
* Instantiate root Vue instance
*/
var productsList = new Vue({
el: '#products',
data: {
products: [{ id: 1, product: 'iPad' }, { id: 2, product: 'iPhone' }, { id: '3', product: 'AirPods' }]
}
});
Now, the loop renders 3 DIVs for iPad, iPhone and AirPods. What's strange is, when I click the remove button for iPhone (productsList.products[1]), the HTML displays iPad and iPhone instead of iPad and AirPods (since we removed iPhone). I just can't figure out what's going on.
My array splice code seems to be working correctly as well. I console.logged the updated array after the splice function and it only included iPad and AirPods (correct) but strangely, the view is different! Can someone tell me what I'm doing wrong here? Thanks.
You should use the :key to keep track of the elements.
<product v-for="product in products"
:product="product"
:key="product.id"
v-on:remove="removeProduct"></product>
I put together an example here.
/**
* Component to show products
*/
Vue.component('product', {
props: ['product'],
data: function() {
return {
localProduct: this.product
};
},
template: `<div class="products">
<span>{{ localProduct.product }}</span>
Remove
</div>`,
methods: {
remove: function() {
this.$emit('remove', this.localProduct.id);
}
}
});
/**
* Instantiate root Vue instance
*/
var productsList = new Vue({
el: '#products',
data: {
products: [{ id: 1, product: 'iPad' }, { id: 2, product: 'iPhone' }, { id: '3', product: 'AirPods' }]
},
methods: {
removeProduct: function(id) {
this.products = this.products.filter(function(p) {
return p.id !== id;
});
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.9/vue.min.js"></script>
<div id="products">
<product v-for="product in products"
:product="product"
:key="product.id"
v-on:remove="removeProduct"></product>
</div>
I also did a bit of cleanup to your code like using filter() instead of splice(). And having the child component emit an event that the parent acts upon instead of the child directly changing the data on the parent.
To learn more about list rendering check out the docs.
If you change localProduct from a data property to a computed one, you can keep the rest of your code identical and it seems to work. Just pop this guy in place of data, in between props and template.
computed: {
localProduct: function() {
return this.product
}
},

Categories