I'm working on a vue cli project where items have two state equipped and unequipped.
This State is controlled by a Boolean located in the Props. Since you can switch the state I had to create a data isEquipped set to false by default.
I then added a watcher but it doesn't change my data value if my props is set to True.
Here's the code
name: "Item",
props: {
Index : Number,
name: String,
desc : String,
bonus: Array,
equipped : Boolean
},
data() {
return {
isEquipped : false
}
},
watch: {
equipped: function(stateEquipped) {
this.isEquipped = stateEquipped;
},
},
So for instance let's say I created a new item with equipped set to True, the watcher doesn't trigger and isEquipped stays at False, is there any reason to that ?
I came across multiple similar questions like this one Vue #Watch not triggering on a boolean change but none of them helped me
If you want to use watch then you can try define it as:
equipped: {
handler () {
this.isEquipped = !this.isEquipped;
},
immediate: true
}
This will change the value of this.isEquipped whenever the value of equipped will change.
I am not sure what is the use case of isEquipped but seeing your code you can use the props directly unless there is a situation where you want to mutate the isEquipped that is not related to the props.
Why not just use a computed value instead?
{
// ...
computed: {
isEquipped () {
// loaded from the component's props
return this.equipped
}
}
}
You can then use isEquipped in your components just as if it was defined in your data() method. You could also just use equipped in your components directly as you don't transform it in any way.
<p>Am I equipped? - <b>{{ equipped }}</b></p>
Watchers are "slow" and they operate on vue's next life-cycle tick which can result in hard to debug reactivity problems.
There are cases where you need them, but if you find any other solution, that uses vue's reactivity system, you should consider using that one instead.
The solution using a computed value from #chvolkmann probably also works for you.
There is a imho better way to do this:
export default {
name: "Item",
props: {
Index : Number,
name: String,
desc : String,
bonus: Array,
equipped : Boolean
},
data() {
return {
isEquipped : false
}
},
updated () {
if (this.equipped !== this.isEquipped) {
this.isEquipped = this.equipped
// trigger "onEquip" event or whatever
}
}
}
The updated life-cycle hook is called -as the name suggests- when a component is updated.
You compare the (unchanged) isEquipped with the new equipped prop value and if they differ, you know that there was a change.
Related
How can I watch a reactive array so that I know what is inserted, removed and replaced when push(), splice(), etc is called?
Since splice etc doesn't directly assign a new value to the array, Vue's reactivity system won't pick up on the changes and the watcher won't fire.
You can, however, create a computed that copies the array and then watch that:
computed: {
myArray() {
return this.my_array.slice();
},
},
data() {
return {
my_array: [],
};
},
watch: {
myArray(newArray, oldArray) {
// do stuff
},
},
Demo
On Vue3 you can do this on your script
<script setup>
watchEffect(() => { console.log(nameOfYourArray)})
</script>
Or on Vue 2 and Vue3 with Option API
Watch: {
nameofYourArray: {
deep: true,
immediate:true,
handler(newVal,oldVal){
console.log(newVal)
}
}
}
if you use 2 solutions you don't forget to add deep otherwise it won't work on child array
I'm trying to make a function where users can added multiple resume posts (from child component) to an array (in parent).
The problem is, every object I push to parent array stays reactive with the child form/object. So if I for example clear the form in child component, the Object I pushed to the parent array gets all it's values cleared as well. How to I emit and push the post-object to parent array and stop it from being reactive, so I can add new/more resume posts?
CreateProfile.vue
<template>
<ResumePostInput :resume_posts="form.resume_posts" #resumeHandler="handleResume"/>
</template>
<script>
export default {
data() {
form: {
resume_posts: []
}
}
methods: {
handleResume(post) {
this.form.resume_posts.push(post)
}
}
}
</script>
ResumePostInput.vue
<template
-- Input fields binded to post object --
</template>
<script>
export default {
emits: ["resumeHandler"],
props: {
resume_posts: Array
},
data() {
return {
post: {
title: '',
sub_title: '',
text: '',
year_from: '',
year_to: '',
type: ''
}
}
},
methods: {
addResume() {
this.$emit("resumeHandler", this.post)
}
}
}
</script>
you emit unknown property, it's a post, not posts
and learn about JS object, there are copy by reference & value
maybe you just need to update your addResume method like this
addResume() {
const post = JSON.parse(JSON.stringify(this.post))
this.$emit("resumeHandler", post)
}
It's not the problem that the object is reactive but that it's the same object because objects are passed by reference in JavaScript. If it's modified in one place, it's modified everywhere.
In order to avoid this, the object needs to be explicitly copied. For shallow object this can be done with object spread:
this.$emit("resumeHandler", {...this.post})
For deeply nested objects, multiple spreads or third-party clone function can be used.
How can I use the return value of a computed property inside a data element that is rendered as HTML?
I have a data element that is HTML, and it looks like this:
contractContent: `<p>Hi ${this.brideName},</p>`
I've also tried this:
contractContent: `<p>Hi {{this.brideName}},</p>`
I am trying to pass in the name via this computed property:
brideName() {
return this.returnContracts[0].brideName.split(' ')[0]
},
But all I'm getting is undefined. If I just put brideName on the component as a test, it returns the first name of the bride just fine.
Where did I go astray?
Here is a fiddle with my dilemma
data() is invoked on component creation and is not reactive. contractContent should be a computed prop for your code to work:
export default {
data() {
return {
name: "Martina Navratilova",
}
},
computed: {
brideName() {
return `<p>Hi ${this.name.split(' ')[0]},</p>`
},
contractContent() {
return `<p>Hi there ${this.brideName}</p>`
},
},
}
updated fiddle
Instead of going with data -> computed -> data -> render,
go directly with computed -> render (html)
In your template you can render html like this
<span v-html="brideName" />
assuming your data structure of returnContracts to be like this
data: {
returnContracts: [
{
"brideName": "Emma Watson"
}
]
}
Then you can directly render brideName from computed
brideName(){
// assuming you'll have correct data, if data isn't valid this will cause crash.
return `<p>Hi ${this.returnContracts[0].brideName.split(' ')[0]},</p>`
}
here is a fiddle to help you out implementation
It seems to me that I found some sort of bug. Basically I want to get components object by index (for the tag). But I've experienced a strange issue. I've included necessary pieces of my code below:
Working example:
let steps = ['Handler', 'Categories', 'Finalize'];
export default {
components: {
Handler,
Categories,
Finalize
},
data() {
return {
step: 0,
currentStep: steps[0] // When specifying index without a variable
}
},
}
Broken example:
let steps = ['Handler', 'Categories', 'Finalize'];
export default {
components: {
Handler,
Categories,
Finalize
},
data() {
return {
step: 0,
currentStep: steps[this.step] // When specifying index by a variable
}
},
}
In working example I am getting component (as expected), but in broken I am getting currentStep: undefined in Vue DevTools. However, no errors in the console. What am I doing wrong?
Your best bet is to move the currentStep to a computed property. Also, steps need to exist in data so they are reactive:
let steps = ['Handler', 'Categories', 'Finalize'];
export default {
components: {
Handler,
Categories,
Finalize
},
data() {
return {
step: 0,
steps,
}
},
computed: {
currentStep() {
return this.steps[this.step];
}
}
}
If possible, prefer to stick the steps directly in data:
data() {
return {
step: 0,
steps: ['Handler', 'Categories', 'Finalize'];,
}
},
(But that may not be possible if you're importing them from the outside. I don't know about your specific use case).
In general, in Vue, when something directly depends on the value of some component properties, computed properties are the way to go: they are performant and clear.
In your original code, should it have worked, currentStep would not react to a change in step. Using a computed property, instead, whenever the step updates, the currentStep will update accordingly.
let's say I have following tree:
[
{
name: 'asd',
is_whatever: true,
children: [
{
name: 'asd',
is_whatever: false,
children: [],
},
],
},
],
The tree is stored in a module via Vuex under key 'tree' and looped through with following recursive component called 'recursive-item':
<li class="recursive-item" v-for="item in tree">
{{ item.name }}
<div v-if="item.is_whatever">on</div>
<div v-else>off</div>
<ul v-if="tree.children.length">
<recursive-item :tree="item.children"></recursive-item>
</ul>
</li>
Now i want to toggle item's property 'is_whatever', so i attach a listener
<div v-if="item.is_whatever"
#click="item.is_whatever = !item.is_whatever">on</div>
<div v-else>off</div>
When i click it, it works, but emits following
"Error: [vuex] Do not mutate vuex store state outside mutation handlers."
[vuex] Do not mutate vuex store state outside mutation handlers.
How am I supposed to implement it without this error? I can see no way how to dispatch an action or emit event to the top of the tree because it's nested and recursive, so I haven't got a path to the specific item, right?
After consulting with some other devs later that evening we came with few ways how to achieve it. Because the data are nested in a tree and I access the nodes in recursive manner, I need to either get the path to the specific node, so for example pass the index of a node as a property, then add the child index while repeating that in every node recursively, or pass just the id of a node and then run the recursive loop in the action in order to toggle its properties.
More optimal solution could be flattening the data structure, hence avoiding the need for a recursion. The node would be then accessible directly via an id.
Right now you're changing the state object directly by calling item.is_whatever = !item.is_whatever, what you need to do is create a mutation function that will execute that operation for you to guarantee proper reactivity:
const store = new Vuex.Store({
state: { /* Your state */ },
mutations: {
changeWhatever (state, item) {
const itemInState = findItemInState(state, item); // You'll need to implement this function
itemInState.is_whatever = !item.is_whatever
}
}
})
Then you need to expose this.$store.commit('changeWhatever', item) as an action in your view that'll be trigger by the click.
There is a debatable solution, but I'll just leave it here.
State:
state: {
nestedObject: {
foo: {
bar: 0
}
}
}
There is Vuex mutation:
mutateNestedObject(state, payload) {
const { callback } = payload;
callback(state.nestedObject);
},
And this is an example of use in a component:
this.$store.commit('mutateNestedObject', {
callback: (nestedObject) => {
nestedObject.foo.bar = 1;
},
});