Can I modify Vue.js VNodes? - javascript

I want to assign some attributes and classes to the children VNode through data object. That just works. But during my Vue.js investigation, I have not seen such pattern in use, that's why I don't think it's good idea to modify children VNode's.
But that approach sometimes comes in handy – for example I want to assign to all the buttons in default slot the aria-label attribute.
See example below, using default stateful components:
Vue.component('child', {
template: '<div>My role is {{ $attrs.role }}</div>',
})
Vue.component('parent', {
render(h) {
const {
default: defaultSlot
} = this.$slots
if (defaultSlot) {
defaultSlot.forEach((child, index) => {
if (!child.data) child.data = {}
if (!child.data.attrs) child.data.attrs = {}
const {
data
} = child
data.attrs.role = 'button'
data.class = 'bar'
data.style = `color: #` + index + index + index
})
}
return h(
'div', {
class: 'parent',
},
defaultSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<parent>
<child></child>
<child></child>
<child></child>
<child></child>
<child></child>
</parent>
</div>
And here is examples using stateless functional components:
Vue.component('child', {
functional: true,
render(h, {
children
}) {
return h('div', {
class: 'bar'
}, children)
},
})
Vue.component('parent', {
functional: true,
render(h, {
scopedSlots
}) {
const defaultScopedSlot = scopedSlots.default({
foo: 'bar'
})
if (defaultScopedSlot) {
defaultScopedSlot.forEach((child, index) => {
child.data = {
style: `color: #` + index + index + index
}
child.data.attrs = {
role: 'whatever'
}
})
}
return h(
'div', {
class: 'parent',
},
defaultScopedSlot,
)
},
})
new Vue({
el: '#app',
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<parent>
<template v-slot:default="{ foo }">
<child>{{ foo }}</child>
<child>{{ foo }}</child>
<child>{{ foo }}</child>
</template>
</parent>
</div>
I am waiting for the following answers:
Yes, you can use it, there are no potential problems with this approach.
Yes, but these problem(s) can happen.
No, there are a lot of problem(s).
UPDATE:
That another good approach I have found it's to wrap child VNode into the another created VNode with appropriate data object, like this:
const wrappedChildren = children.map(child => {
return h("div", { class: "foo" }, [child]);
});
Using this approach I have no fear modifying children VNode's.
Thank you in advance.

There are potential problems with doing this. Used very sparingly it can be a useful technique and personally I would be happy to use it if no simple alternative were available. However, you're in undocumented territory and if something goes wrong you'll likely have to debug by stepping through Vue internals. It is not for the faint-hearted.
First, some of examples of something similar being used by others.
Patching key:
https://medium.com/dailyjs/patching-the-vue-js-virtual-dom-the-need-the-explanation-and-the-solution-ba18e4ae385b
An example where Vuetify patches a VNode from a mixin:
https://github.com/vuetifyjs/vuetify/blob/5329514763e7fab11994c4303aa601346e17104c/packages/vuetify/src/components/VImg/VImg.ts#L219
An example where Vuetify patches a VNode from a scoped slot: https://github.com/vuetifyjs/vuetify/blob/7f7391d76dc44f7f7d64f30ad7e0e429c85597c8/packages/vuetify/src/components/VItemGroup/VItem.ts#L58
I think only the third example is really comparable to the patching in this question. A key feature there is that it uses a scoped slot rather than a normal slot, so the VNodes are created within the same render function.
It gets more complicated with normal slots. The problem is that the VNodes for the slot are created in the parent's render function. If the child's render function runs multiple times it'll just keep getting passed the same VNodes for the slot. Modifying those VNodes won't necessarily do what you'd expect as the diffing algorithm just sees the same VNodes and doesn't perform any DOM updates.
Here's an example to illustrate:
const MyRenderComponent = {
data () {
return {
blueChildren: true
}
},
render (h) {
// Add a button before the slot children
const children = [h('button', {
on: {
click: () => {
this.blueChildren = !this.blueChildren
}
}
}, 'Blue children: ' + this.blueChildren)]
const slotContent = this.$slots.default
for (const child of slotContent) {
if (child.data && child.data.class) {
// Add/remove the CSS class 'blue'
child.data.class.blue = this.blueChildren
// Log it out to confirm this really is happening
console.log(child.data.class)
}
children.push(child)
}
return h('div', null, children)
}
}
new Vue({
el: '#app',
components: {
MyRenderComponent
},
data () {
return {
count: 0
}
}
})
.red {
border: 1px solid red;
margin: 10px;
padding: 5px;
}
.blue {
background: #009;
color: white;
}
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<my-render-component>
<div :class="{red: true}">This is a slot content</div>
</my-render-component>
<button #click="count++">
Update outer: {{ count }}
</button>
</div>
There are two buttons. The first button toggles a data property called blueChildren. It's used to decide whether or not to add a CSS class to the children. Changing the value of blueChildren will successfully trigger a re-render of the child component and the VNode does get updated, but the DOM is unchanged.
The other button forces the outer component to re-render. That regenerates the VNodes in the slot. These then get passed to the child and the DOM will get updated.
Vue is making some assumptions about what can and can't cause a VNode to change and optimising accordingly. In Vue 3 this is only going to get worse (by which I mean better) because there are a lot more of these optimisations coming along. There's a very interesting presentation Evan You gave about Vue 3 that covers the kinds of optimisations that are coming and they all fall into this category of Vue assuming that certain things can't change.
There are ways to fix this example. When the component is performing an update the VNode will contain a reference to the DOM node, so it can be updated directly. It's not great, but it can be done.
My own feeling is that you're only really safe if the patching you're doing is fixed, such that updates aren't a problem. Adding some attributes or CSS classes should work, so long as you don't want to change them later.
There is another class of problems to overcome. Tweaking VNodes can be really fiddly. The examples in the question allude to it. What if data is missing? What if attrs is missing?
In the scoped slots example in the question the child component has class="bar" on its <div>. That gets blown away in the parent. Perhaps that's intentional, perhaps not, but trying to merge together all the different objects is quite tricky. For example, class could be a string, object or array. The Vuetify example uses _b, which is an alias for Vue's internal bindObjectProps, to avoid having to cover all the different cases itself.
Along with the different formats are the different node types. Nodes don't necessarily represent components or elements. There are also text nodes and comments, where comment nodes are a consequence of v-if rather than actual comments in the template.
Handling all the different edge cases correctly is pretty difficult. Then again, it may be that none of these edge cases cause any real problems for the use cases you actually have in mind.
As a final note, all of the above only applies to modifying a VNode. Wrapping VNodes from a slot or inserting other children between them in a render function is perfectly normal.

Related

How to make a sample that shows the 'Props are overwritten when re-rendering' anti pattern

I would like to be convinced that 'Props are overwritten when re-rendering' is an anti pattern.
const MyButton = Vue.extend({
props: {
obj: {}
},
template:
"<button #click=\"obj.text = 'Value'+Math.round(Math.random()*100)\">Change text in child ({{obj.text}})</button>"
});
const app = new Vue({
el: "#main",
data: {
obj: { text: "Value2" }
},
components: {
MyButton
},
template: `
<div>
<Button #click='obj.text = "Value"+Math.round(Math.random()*100)'>Change text in parent ({{obj.text}})</Button><br>
<MyButton :obj='obj'/> 👈 Pressing here 'mutate' the prop. It is an anti-pattern (Props are overwritten when re-rendering). But it seems to work just fine, how to change this sample so it shows the anti-pattern problem? (https://eslint.vuejs.org/rules/no-mutating-props.html)
</div>`
});
UPDATED Codepen:
https://codepen.io/SVSMITH/pen/LYrXRzW
Can anyone help?
In your example you are actually not mutating the prop, because what you send in to the child is a reference to an object. This reference stays the same. See what happens if you change the template code in your child to:
"<button #click=\"obj = {}\">Change text in child ({{obj.text}})</button>"
Then you will see that the prop is overwritten and the value of the child differs from that in the parent. When the parent updates the value, the updated value in the child will be overwritten. Which can cause serious and hard to find bugs. Therefore use emit in the child to update the value in the parent, which will update the child value through the prop.
If you have a lot of these props and emits, you should look into using a store like Pinia.

Wait for definition before calling function - Vue.js

When it comes to creating methods in child components I'm having a hard time figuring a particular feature out.
I have this parent route/component (League.vue):
In this league.vue I render a child component:
<router-view :league="league" />
Child component:
<template>
<div v-if="teams_present">
<div class="page-container__table">
<h3 class="page-container__table__header">Teams</h3>
</div>
</div>
</template>
<script>
export default {
name: 'LeagueTeams',
props: [
'league'
],
data () {
},
computed: {
teams_present: function () {
return this.league.teams.length > 0
}
}
}
</script>
ERROR:
"TypeError: Cannot read property 'length' of undefined"
So it appears that the computed callback is called before the prop can be set, I think? and if a change it to methods it never gets called. How do I handle this case?
As Ali suggested, you can return this.league.teams && this.league.teams.length > 0, which definitely will work.
However, as my experience, to avoid these situation, and for good practice, always declare the type of the Props. So in your props:
export default {
name: 'LeagueTeams',
props: {
league: {
type: Object, // type validation Object
default() { return {teams: [] }} // add a default empty state for team, you can add more
}
},
data () {
},
computed: {
teams_present: function () {
return this.league.teams.length > 0 // now the old code should work
}
}
}
</script>
By doing this, you don't need to care much about checking the edge case of this.league.teams every time, since you may need to call it again in methods or in the <template> html
Update: Another suggestion is if you are using vue-cli 4, you can use Optional chaining and nullish coalescing.
return this.league?.teams.length ?? false // replace with only this line will work
Hope this will help you 2 more ways to deal with in these situations, and depends on situations you can choose the most suitable one

How to create Vue.js slot programmatically?

I have the following component with a slot:
<template>
<div>
<h2>{{ someProp }}</h2>
<slot></slot>
</div>
</template>
For some reasons, I have to manually instantiate this component. This is how I am doing it:
const Constr = Vue.extend(MyComponent);
const instance = new Constr({
propsData: { someProp: 'My Heading' }
}).$mount(body);
The problem is: I am not able to create slot contents programmatically. So far, I can create simple string based slot:
const Constr = Vue.extend(MyComponent);
const instance = new Constr({
propsData: { someProp: 'My Heading' }
});
// Creating simple slot
instance.$slots.default = ['Hello'];
instance.$mount(body);
The question is - how can I create $slots programmatically and pass it to the instance I am creating using new?
Note: I am not using a full build of Vue.js (runtime only). So I don't have a Vue.js compiler available to compile the template on the fly.
I looked into TypeScript definition files of Vue.js and I found an undocumented function on Vue component instance: $createElement(). My guess is, it is the same function that is passed to render(createElement) function of the component. So, I am able to solve it as:
const Constr = Vue.extend(MyComponent);
const instance = new Constr({
propsData: { someProp: 'My Heading' }
});
// Creating simple slot
const node = instance.$createElement('div', ['Hello']);
instance.$slots.default = [node];
instance.$mount(body);
But this is clearly undocumented and hence questionable approach. I will not mark it answered if there is some better approach available.
I think I have finally stumbled on a way to programmatically create a slot element. From what I can tell, the approach does not seem to work for functional components. I am not sure why.
If you are implementing your own render method for a component, you can programmatically create slots that you pass to child elements using the createElement method (or whatever you have aliased it to in the render method), and passing a data hash that includes { slot: NAME_OF_YOUR_SLOT } followed by the array of children within that slot.
For example:
Vue.config.productionTip = false
Vue.config.devtools = false;
Vue.component('parent', {
render (createElement) {
return createElement('child', [
createElement('h1', { slot: 'parent-slot' }, 'Parent-provided Named Slot'),
createElement('h2', { slot: 'default' }, 'Parent-provided Default Slot')
])
}
})
Vue.component('child', {
template: '<div><slot name="parent-slot" /><slot /></div>'
})
new Vue({
el: '#app',
template: '<parent />'
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.17/dist/vue.js"></script>
<div id='app'>
</div>
(This doesn't really answer How to create Vue.js slot programatically?. But it does solve your problem.)
This trick is less hackish compared to using $createElement().
Basically, create a new component that register MyComponent as a local component.
const Constr = Vue.extend({
template: `
<MyComponent someProp="My Heading">
<div>slot here !!!</div>
</MyComponent>
`,
components: {
MyComponent: MyComponent
}
});
const instance = new Constr().$mount('#app');
Demo: https://jsfiddle.net/jacobgoh101/shrn26p1/
I just came across an answer to this in vue forum:
slots
The principle is: There is nothing like createElement('slot'..)
Instead there is a render function which provides the slotted innerHtml as function:
$scopedSlots.default()
Usage:
render: function (createElement) {
const self = this;
return createElement("div", this.$scopedSlots.default());
}
If you want to provide a default in case there is no content given for the slots, you need to code a disctinction yourself and render something else.
(The link above holds a more detailed example)
The function returns an array, therefore it can not be used as a root for the render function. It need to be wrapped into single container node like div in the example above.

VueJS pass props to children in render function call

I have a parent component that wraps multiple children 'child'. I want the parent to essentially have the same template as the vanilla markup which is why I am using the render function.
I want the parent to manage the state of which child is active. But the props don't appear to be passing down to the children.
I have tried applying what the documentation says, but props are undefined on the children. However if I do item.data.staticClass = 'testing-class'; that class applies to each child.
Vue.component('parent', {
data: function() {
return {
activeIndex: 0
}
},
render: function(createElement) {
this.$options._renderChildren.forEach(function(item, index) {
if (item.data === undefined) //whitespace?
return;
item.data.props = {
activeindex: this.activeIndex,
index: index
}
}.bind(this));
return createElement('div', {}, this.$options._renderChildren);
}
});
Vue.component('child', {
template: '<div>Im a child</div>',
props: ['activeindex', 'index'],
mounted: function() {
console.log(this.$props); //undefined
}
});
new Vue({
el: '#app'
});
JSFIDDLE DEMO
First of all, I think that this.$props will always be undefined. The $props property is accessible in a template like {{ $props }}, but those inline properties (frustratingly) don't always map up directly to the this variable available in the component's script. You can see the component's prop values using this.$options.propsData.
Secondly, you can use item.componentOptions.propsData to set the property values of the child component. (I think item.data.props is a misnomer referencing something else). Here's a fiddle with the change.

Vue 2 - Mutating props vue-warn

I started https://laracasts.com/series/learning-vue-step-by-step series. I stopped on the lesson Vue, Laravel, and AJAX with this error:
vue.js:2574 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "list" (found in component )
I have this code in main.js
Vue.component('task', {
template: '#task-template',
props: ['list'],
created() {
this.list = JSON.parse(this.list);
}
});
new Vue({
el: '.container'
})
I know that the problem is in created() when I overwrite the list prop, but I am a newbie in Vue, so I totally don't know how to fix it. Does anyone know how (and please explain why) to fix it?
This has to do with the fact that mutating a prop locally is considered an anti-pattern in Vue 2
What you should do now, in case you want to mutate a prop locally, is to declare a field in your data that uses the props value as its initial value and then mutate the copy:
Vue.component('task', {
template: '#task-template',
props: ['list'],
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
You can read more about this on Vue.js official guide
Note 1: Please note that you should not use the same name for your prop and data, i.e.:
data: function () { return { list: JSON.parse(this.list) } } // WRONG!!
Note 2: Since I feel there is some confusion regarding props and reactivity, I suggest you to have a look on this thread
The Vue pattern is props down and events up. It sounds simple, but is easy to forget when writing a custom component.
As of Vue 2.2.0 you can use v-model (with computed properties). I have found this combination creates a simple, clean, and consistent interface between components:
Any props passed to your component remains reactive (i.e., it's not cloned nor does it require a watch function to update a local copy when changes are detected).
Changes are automatically emitted to the parent.
Can be used with multiple levels of components.
A computed property permits the setter and getter to be separately defined. This allows the Task component to be rewritten as follows:
Vue.component('Task', {
template: '#task-template',
props: ['list'],
model: {
prop: 'list',
event: 'listchange'
},
computed: {
listLocal: {
get: function() {
return this.list
},
set: function(value) {
this.$emit('listchange', value)
}
}
}
})
The model property defines which prop is associated with v-model, and which event will be emitted on changes. You can then call this component from the parent as follows:
<Task v-model="parentList"></Task>
The listLocal computed property provides a simple getter and setter interface within the component (think of it like being a private variable). Within #task-template you can render listLocal and it will remain reactive (i.e., if parentList changes it will update the Task component). You can also mutate listLocal by calling the setter (e.g., this.listLocal = newList) and it will emit the change to the parent.
What's great about this pattern is that you can pass listLocal to a child component of Task (using v-model), and changes from the child component will propagate to the top level component.
For example, say we have a separate EditTask component for doing some type of modification to the task data. By using the same v-model and computed properties pattern we can pass listLocal to the component (using v-model):
<script type="text/x-template" id="task-template">
<div>
<EditTask v-model="listLocal"></EditTask>
</div>
</script>
If EditTask emits a change it will appropriately call set() on listLocal and thereby propagate the event to the top level. Similarly, the EditTask component could also call other child components (such as form elements) using v-model.
Vue just warns you: you change the prop in the component, but when parent component re-renders, "list" will be overwritten and you lose all your changes. So it is dangerous to do so.
Use computed property instead like this:
Vue.component('task', {
template: '#task-template',
props: ['list'],
computed: {
listJson: function(){
return JSON.parse(this.list);
}
}
});
If you're using Lodash, you can clone the prop before returning it. This pattern is helpful if you modify that prop on both the parent and child.
Let's say we have prop list on component grid.
In Parent Component
<grid :list.sync="list"></grid>
In Child Component
props: ['list'],
methods:{
doSomethingOnClick(entry){
let modifiedList = _.clone(this.list)
modifiedList = _.uniq(modifiedList) // Removes duplicates
this.$emit('update:list', modifiedList)
}
}
Props down, events up. That's Vue's Pattern. The point is that if you try to mutate props passing from a parent. It won't work and it just gets overwritten repeatedly by the parent component. Child component can only emit an event to notify parent component to do sth. If you don't like these restrict, you can use VUEX(actually this pattern will suck in complex components structure, you should use VUEX!)
You should not change the props's value in child component.
If you really need to change it you can use .sync.
Just like this
<your-component :list.sync="list"></your-component>
Vue.component('task', {
template: '#task-template',
props: ['list'],
created() {
this.$emit('update:list', JSON.parse(this.list))
}
});
new Vue({
el: '.container'
})
According to the VueJs 2.0, you should not mutate a prop inside the component. They are only mutated by their parents. Therefore, you should define variables in data with different names and keep them updated by watching actual props.
In case the list prop is changed by a parent, you can parse it and assign it to mutableList. Here is a complete solution.
Vue.component('task', {
template: ´<ul>
<li v-for="item in mutableList">
{{item.name}}
</li>
</ul>´,
props: ['list'],
data: function () {
return {
mutableList = JSON.parse(this.list);
}
},
watch:{
list: function(){
this.mutableList = JSON.parse(this.list);
}
}
});
It uses mutableList to render your template, thus you keep your list prop safe in the component.
The answer is simple, you should break the direct prop mutation by assigning the value to some local component variables(could be data property, computed with getters, setters, or watchers).
Here's a simple solution using the watcher.
<template>
<input
v-model="input"
#input="updateInput"
#change="updateInput"
/>
</template>
<script>
export default {
props: {
value: {
type: String,
default: '',
},
},
data() {
return {
input: '',
};
},
watch: {
value: {
handler(after) {
this.input = after;
},
immediate: true,
},
},
methods: {
updateInput() {
this.$emit('input', this.input);
},
},
};
</script>
It's what I use to create any data input components and it works just fine. Any new data sent(v-model(ed)) from parent will be watched by the value watcher and is assigned to the input variable and once the input is received, we can catch that action and emit input to parent suggesting that data is input from the form element.
do not change the props directly in components.if you need change it set a new property like this:
data() {
return {
listClone: this.list
}
}
And change the value of listClone.
I faced this issue as well. The warning gone after i use $on and $emit.
It's something like use $on and $emit recommended to sent data from child component to parent component.
one-way Data Flow,
according to https://v2.vuejs.org/v2/guide/components.html, the component follow one-Way
Data Flow,
All props form a one-way-down binding between the child property and the parent one, when the parent property updates, it will flow down to the child but not the other way around, this prevents child components from accidentally mutating the parent's, which can make your app's data flow harder to understand.
In addition, every time the parent component is updates all props
in the child components will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do .vue will warn you in the
console.
There are usually two cases where it’s tempting to mutate a prop:
The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards.
The prop is passed in as a raw value that needs to be transformed.
The proper answer to these use cases are:
Define a local data property that uses the prop’s initial value as its initial value:
props: ['initialCounter'],
data: function () {
return { counter: this.initialCounter }
}
Define a computed property that is computed from the prop’s value:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
If you want to mutate props - use object.
<component :model="global.price"></component>
component:
props: ['model'],
methods: {
changeValue: function() {
this.model.value = "new value";
}
}
I want to give this answer which helps avoid using a lot of code, watchers and computed properties. In some cases this can be a good solution:
Props are designed to provide one-way communication.
When you have a modal show/hide button with a prop the best solution to me is to emit an event:
<button #click="$emit('close')">Close Modal</button>
Then add listener to modal element:
<modal :show="show" #close="show = false"></modal>
(In this case the prop show is probably unnecessary because you can use an easy v-if="show" directly on the base-modal)
You need to add computed method like this
component.vue
props: ['list'],
computed: {
listJson: function(){
return JSON.parse(this.list);
}
}
Vue.component('task', {
template: '#task-template',
props: ['list'],
computed: {
middleData() {
return this.list
}
},
watch: {
list(newVal, oldVal) {
console.log(newVal)
this.newList = newVal
}
},
data() {
return {
newList: {}
}
}
});
new Vue({
el: '.container'
})
Maybe this will meet your needs.
Vue3 has a really good solution. Spent hours to reach there. But it worked really good.
On parent template
<user-name
v-model:first-name="firstName"
v-model:last-name="lastName"
></user-name>
The child component
app.component('user-name', {
props: {
firstName: String,
lastName: String
},
template: `
<input
type="text"
:value="firstName"
#input="$emit('update:firstName',
$event.target.value)">
<input
type="text"
:value="lastName"
#input="$emit('update:lastName',
$event.target.value)">
`
})
This was the only solution which did two way binding. I like that first two answers were addressing in good way to use SYNC and Emitting update events, and compute property getter setter, but that was heck of a Job to do and I did not like to work so hard.
Vue.js props are not to be mutated as this is considered an Anti-Pattern in Vue.
The approach you will need to take is creating a data property on your component that references the original prop property of list
props: ['list'],
data: () {
return {
parsedList: JSON.parse(this.list)
}
}
Now your list structure that is passed to the component is referenced and mutated via the data property of your component :-)
If you wish to do more than just parse your list property then make use of the Vue component' computed property.
This allow you to make more in depth mutations to your props.
props: ['list'],
computed: {
filteredJSONList: () => {
let parsedList = JSON.parse(this.list)
let filteredList = parsedList.filter(listItem => listItem.active)
console.log(filteredList)
return filteredList
}
}
The example above parses your list prop and filters it down to only active list-tems, logs it out for schnitts and giggles and returns it.
note: both data & computed properties are referenced in the template the same e.g
<pre>{{parsedList}}</pre>
<pre>{{filteredJSONList}}</pre>
It can be easy to think that a computed property (being a method) needs to be called... it doesn't
For when TypeScript is your preferred lang. of development
<template>
<span class="someClassName">
{{feesInLocale}}
</span>
</template>
#Prop({default: 0}) fees: any;
// computed are declared with get before a function
get feesInLocale() {
return this.fees;
}
and not
<template>
<span class="someClassName">
{{feesInLocale}}
</span>
</template>
#Prop() fees: any = 0;
get feesInLocale() {
return this.fees;
}
Assign the props to new variable.
data () {
return {
listClone: this.list
}
}
Adding to the best answer,
Vue.component('task', {
template: '#task-template',
props: ['list'],
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
Setting props by an array is meant for dev/prototyping, in production make sure to set prop types(https://v2.vuejs.org/v2/guide/components-props.html) and set a default value in case the prop has not been populated by the parent, as so.
Vue.component('task', {
template: '#task-template',
props: {
list: {
type: String,
default() {
return '{}'
}
}
},
data: function () {
return {
mutableList: JSON.parse(this.list);
}
}
});
This way you atleast get an empty object in mutableList instead of a JSON.parse error if it is undefined.
YES!, mutating attributes in vue2 is an anti-pattern. BUT...
Just break the rules by using other rules, and go forward!
What you need is to add .sync modifier to your component attribute in the parent scope.
<your-awesome-components :custom-attribute-as-prob.sync="value" />
Below is a snack bar component, when I give the snackbar variable directly into v-model like this if will work but in the console, it will give an error as
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.
<template>
<v-snackbar v-model="snackbar">
{{ text }}
</v-snackbar>
</template>
<script>
export default {
name: "loader",
props: {
snackbar: {type: Boolean, required: true},
text: {type: String, required: false, default: ""},
},
}
</script>
Correct Way to get rid of this mutation error is use watcher
<template>
<v-snackbar v-model="snackbarData">
{{ text }}
</v-snackbar>
</template>
<script>
/* eslint-disable */
export default {
name: "loader",
data: () => ({
snackbarData:false,
}),
props: {
snackbar: {type: Boolean, required: true},
text: {type: String, required: false, default: ""},
},
watch: {
snackbar: function(newVal, oldVal) {
this.snackbarData=!this.snackbarDatanewVal;
}
}
}
</script>
So in the main component where you will load this snack bar you can just do this code
<loader :snackbar="snackbarFlag" :text="snackText"></loader>
This Worked for me
Vue.js considers this an anti-pattern. For example, declaring and setting some props like
this.propsVal = 'new Props Value'
So to solve this issue you have to take in a value from the props to the data or the computed property of a Vue instance, like this:
props: ['propsVal'],
data: function() {
return {
propVal: this.propsVal
};
},
methods: {
...
}
This will definitely work.
In addition to the above, for others having the following issue:
"If the props value is not required and thus not always returned, the passed data would return undefined (instead of empty)". Which could mess <select> default value, I solved it by checking if the value is set in beforeMount() (and set it if not) as follows:
JS:
export default {
name: 'user_register',
data: () => ({
oldDobMonthMutated: this.oldDobMonth,
}),
props: [
'oldDobMonth',
'dobMonths', //Used for the select loop
],
beforeMount() {
if (!this.oldDobMonth) {
this.oldDobMonthMutated = '';
} else {
this.oldDobMonthMutated = this.oldDobMonth
}
}
}
Html:
<select v-model="oldDobMonthMutated" id="dob_months" name="dob_month">
<option selected="selected" disabled="disabled" hidden="hidden" value="">
Select Month
</option>
<option v-for="dobMonth in dobMonths"
:key="dobMonth.dob_month_slug"
:value="dobMonth.dob_month_slug">
{{ dobMonth.dob_month_name }}
</option>
</select>
I personally always suggest if you are in need to mutate the props, first pass them to computed property and return from there, thereafter one can mutate the props easily, even at that you can track the prop mutation , if those are being mutated from another component too or we can you watch also .
Because Vue props is one way data flow, This prevents child components from accidentally mutating the parent’s state.
From the official Vue document, we will find 2 ways to solve this problems
if child component want use props as local data, it is best to define a local data property.
props: ['list'],
data: function() {
return {
localList: JSON.parse(this.list);
}
}
The prop is passed in as a raw value that needs to be transformed. In this case, it’s best to define a computed property using the prop’s value:
props: ['list'],
computed: {
localList: function() {
return JSON.parse(this.list);
},
//eg: if you want to filter this list
validList: function() {
return this.list.filter(product => product.isValid === true)
}
//...whatever to transform the list
}
You should always avoid mutating props in vue, or any other framework. The approach you could take is copy it into another variable.
for example.
// instead of replacing the value of this.list use a different variable
this.new_data_variable = JSON.parse(this.list)
A potential solution to this is using global variables.
import { Vue } from "nuxt-property-decorator";
export const globalStore = new Vue({
data: {
list: [],
},
}
export function setupGlobalsStore() {
Vue.prototype.$globals = globalStore;
}
Then you would use:
$globals.list
Anywhere you need to mutate it or present it.

Categories