If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeVue reactivity is magic. What I mean by "reactivity" is how Vue is smart enough to re-render whenever a piece of data changes. Or, even more impressive, if that piece of data is an object and you change just one property on it, Vue will still figure out that it needs to re-render any components that depend on that.
But there are a few limitations to Vue reactivity: a few edge cases where Vue can't work its magic and does not realize that it needs to re-render. Well, to be clear: Vue 2 has a few limitations... that Vue 3 solves. So if you're using Vue 3, feel free to skip ahead: it does not suffer from this issue.
Search for "Vue reactivity" to find a page on their docs. Scroll down to "change detection caveats".
There aren't many situations like this, but this first situation talks about what is currently happening to us: Vue cannot detect property addition or deletion. And a property must be present in the data object in order for Vue to convert it and make it reactive.
We talked about how reactivity works under the hood in part one of this series. The short explanation for Vue 2 is that when a piece of data is an object, Vue replaces each property on that object with a getter and setter method. This is invisible to us, but it allows Vue to be notified - via the setter method - whenever someone changes a property.
Our problem starts in the checkout form component's data
function. We initialize validationErrors
to an empty object. And then, in the validateField()
method, we add a new property to validationErrors
:
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 95 | |
data() { | |
return { | |
... lines 98 - 105 | |
validationErrors: {}, | |
... lines 107 - 108 | |
}; | |
}, | |
methods: { | |
... lines 112 - 153 | |
validateField(event) { | |
... lines 155 - 165 | |
if (!this.form[validationField]) { | |
this.validationErrors[validationField] = validationMessages[validationField]; | |
} else { | |
delete this.validationErrors[validationField]; | |
} | |
}, | |
}, | |
}; | |
</script> |
That's the "property addition" that Vue was talking about. Vue doesn't have a way to detect that the new property was added. And so, it can't add the getter and setter methods that are the key to making that property reactive. We can still read from and write to that property... but Vue isn't aware that we're doing that.
This is a long way of saying that if you have a piece of data that's an object like validationErrors
, be sure to include all of its properties when you initialize it, even if some are null.
Head up to data
and add all 6 properties to the object - setting each one to null
. Thanks to this, from the very first moment the data is initialized, it will have all of its properties:
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 95 | |
data() { | |
return { | |
... lines 98 - 105 | |
validationErrors: { | |
customerName: null, | |
customerEmail: null, | |
customerAddress: null, | |
customerZip: null, | |
customerCity: null, | |
customerPhone: null, | |
}, | |
... lines 114 - 115 | |
}; | |
}, | |
... lines 118 - 179 | |
}; | |
</script> |
Then, we're not creating a property down inside validateField()
: we're just changing its value!
Oh, and now, instead of deleting the property, set it to null.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 117 | |
methods: { | |
... lines 119 - 160 | |
validateField(event) { | |
... lines 162 - 172 | |
if (!this.form[validationField]) { | |
this.validationErrors[validationField] = validationMessages[validationField]; | |
} else { | |
this.validationErrors[validationField] = null; | |
} | |
}, | |
}, | |
}; | |
</script> |
Ok! Let's test this! Go to checkout and... perfect! It instantly updates! But if we submit the form... funny things start to happen.
No validation error on Ryan. Right? That makes sense. But if I clear that out and hit tab... hey! Why didn't I get my validation error? This... is the same problem, but I want to show it to you in more detail.
Back in the component, at the top of validateField()
, let's console.log(this.validationErrors)
.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 117 | |
methods: { | |
... lines 119 - 160 | |
validateField(event) { | |
console.log(this.validationErrors); | |
... lines 163 - 179 | |
}, | |
}, | |
}; | |
</script> |
Head back over: my page already refreshed. Go to checkout and open the console. Now notice: when I first blur, the entire object has a ...
. That's because the validationErrors
data is wrapped in a getter method, which is Vue's way of adding reactivity to it. And if we click to open this, each property also has a ...
next to it. That's an easy way for us to see that each property is reactive: Vue did have the opportunity to wrap it in a getter and setter.
Now submit the form, focus the name field... and hit tab again. Scroll down on the console to see the new log. The object does not have the ...
anymore. And more importantly, each property under it also does not have ...
. The fact that those are gone means that each property lost reactivity. If we set the customerCity
property, there is no setter, and so Vue would not be notified that it needs to re-render.
The reason this is happening is, up at the top of onSubmit()
, we're resetting validationErrors
back to an empty object. Then, when we set a key on validationErrors
later, we are, once again, creating new properties.
Let's reinitialize just one field to start: set customerName
to null
.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 117 | |
methods: { | |
... lines 119 - 132 | |
async onSubmit() { | |
... lines 134 - 135 | |
this.validationErrors = { | |
customerName: null | |
}; | |
... lines 139 - 161 | |
}, | |
... lines 163 - 182 | |
}, | |
}; | |
</script> |
Now go back, head to the checkout form and re-submit it. Click on the name field and blur it to get the log. Oooo: customerName
now does still have its getter method! But the other fields do not. By including the customerName
property when we replaced the validationErrors
data, Vue was able to wrap it and make it reactive at that moment.
So the full solution is this. Either use Vue 3... and this will all just work, or whenever you set a full key on data that's an object, whether you're setting it inside the data
function or somewhere else - be sure to include every property it needs. There are other work arounds the docs mention, but this is what I like.
To do this without repeating ourselves, let's add a new method called getEmptyValidationErrors()
that will return an object. Go up to our initial data, steal those fields, head down and paste. Perfect.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 110 | |
methods: { | |
... lines 112 - 173 | |
getEmptyValidationErrors() { | |
return { | |
customerName: null, | |
customerEmail: null, | |
customerAddress: null, | |
customerZip: null, | |
customerCity: null, | |
customerPhone: null, | |
}; | |
}, | |
}, | |
}; | |
</script> |
We can use this up inside data()
: validationErrors
set to this.getEmptyValidationErrors()
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 95 | |
data() { | |
return { | |
... lines 98 - 105 | |
validationErrors: this.getEmptyValidationErrors(), | |
... lines 107 - 108 | |
}; | |
}, | |
methods: { | |
... lines 112 - 173 | |
getEmptyValidationErrors() { | |
return { | |
customerName: null, | |
customerEmail: null, | |
customerAddress: null, | |
customerZip: null, | |
customerCity: null, | |
customerPhone: null, | |
}; | |
}, | |
}, | |
}; | |
</script> |
Do the same thing down here in onSubmit()
: this.getEmptyValidationErrors()
.
... lines 1 - 77 | |
<script> | |
... lines 79 - 83 | |
export default { | |
name: 'CheckoutForm', | |
... lines 86 - 95 | |
data() { | |
return { | |
... lines 98 - 105 | |
validationErrors: this.getEmptyValidationErrors(), | |
... lines 107 - 108 | |
}; | |
}, | |
methods: { | |
... lines 112 - 125 | |
async onSubmit() { | |
... lines 127 - 128 | |
this.validationErrors = this.getEmptyValidationErrors(); | |
... lines 130 - 152 | |
}, | |
... lines 154 - 173 | |
getEmptyValidationErrors() { | |
return { | |
customerName: null, | |
customerEmail: null, | |
customerAddress: null, | |
customerZip: null, | |
customerCity: null, | |
customerPhone: null, | |
}; | |
}, | |
}, | |
}; | |
</script> |
Let's check it! Go back to the checkout form, submit it... see the errors, type a name, hit tab and... it's gone! Reactivity is back!
Let's celebrate by removing the console.log()
.
Woh team, we're done! You did it! You are now massively dangerous in Vue. So go build something really cool and tell us about it. I would love to know.
In a future tutorial, we'll cover the Vue 3 composition API: that's the really big new, optional feature in Vue 3 that has the potential to make sharing code and data a lot nicer.
If there's something else that you want to know about, let us know down in the comments.
Alright friends, see you next time!
Hey Chris,
Look closer at the table of content on the course intro page: https://symfonycasts.com/sc... - every challenges should be green and you should see a little tick in front of each chapter. I bet there's a missing challenge or chapter there, you just need to complete the missing challenge or watch the chapter till the end.
If you still need help with this - write to us using contact form here: https://symfonycasts.com/co... from your account email and I'll take a look at it myself.
Cheers!
Thanks Victor
You are right, one of the chapters did not have a tick. This reminds me of something I noticed during the course, it would replay chapters I had already watched and I would have to redo the challenge/s that followed. It would step back one or two chapters (I think back to the chapter that followed any previous challenge).
I use the Brave browser so I wondered if it was struggling to keep my state? I used the same computer each time.
Thanks again
Chris
(Ah, looks like Disqus has also forgotten me - the burden of just wanting to not be quite so tracked)
Hey Chris,
Ah, if you have a tricky browser - it might be the reason why it missed the check. If somehow it was log you out globally on SymfonyCasts but the page still was loaded - you were able to watch the video I suppose, but the system didn't get a request about finishing it because you were not logged in :) Anyway, the fix should be pretty easy - just re-watch that chapter (or cheat a bit and just rewind it to the end :) )
Let me know if that didn't help ;)
Cheers!
Great couple of tutorials. Good job fixing the vimeo streaming issue btw! The only thing stopping me from migrating my existing app to Vue is that I don't know how to integrate the symfony translation bundles. I am using lexik/translation-bundle. Should I just add it to the twig template as usual and then use it within js? Do you recommend using another bundle?
Hey Manuel!
I don't have advise at this time as per a specific bundle to deal with i18n in Vue. When it comes to Vue and just Vue, there is a package called (<a href="https://www.npmjs.com/package/vue-i18n">Vue-i18n</a>) which can help but might not adapt to your current method of doing it.
If you are using a Symfony bundle to do translations (and it works for you) my advise would be... Don't throw it away! You can dump your translation strings all in the template inside a Javascript object. These will likely be cached by twig anyway and you can then use these strings inside your Vue App. For example:
<blockquote>`
window.myTranslations = {
trans_string_a: 'put twig escaped string for js here',
trans_string_b: 'put your other strings ...',
};
`</blockquote>
Then you can easily reference the global variable.
Hope this helps!
Hey Julien!
Thanks for your feedback! While a Vue SPA tutorial (featuring Vue Router) is inot in our immediate plans, we are considering adding this to our JS Frameworks track at some point. Stay tuned!
Thanks a lot for tutorials!
Really want to get know about how to combine Vue with Symfony Forms (maybe without API Platform, but with plain action with formView from controller) in proper way, and maybe how to compile Twig markup to Vue, so for example if i already have form-blocks in twig (form_widget, form_row, etc..) how it's possible to use them with Vue.
Hi Kirill!
In my view, Symfony Forms and Vue go separate ways. When it comes to forms specifically, if you use one system, you won't be using the other as their purpose are the same!
// package.json
{
"devDependencies": {
"@fortawesome/fontawesome-free": "^5.15.1", // 5.15.1
"@symfony/webpack-encore": "^0.30.0", // 0.30.2
"axios": "^0.19.2", // 0.19.2
"bootstrap": "^4.4.1", // 4.5.3
"core-js": "^3.0.0", // 3.6.5
"eslint": "^6.7.2", // 6.8.0
"eslint-config-airbnb-base": "^14.0.0", // 14.2.0
"eslint-plugin-import": "^2.19.1", // 2.22.1
"eslint-plugin-vue": "^6.0.1", // 6.2.2
"regenerator-runtime": "^0.13.2", // 0.13.7
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^8.0.0", // 8.0.2
"vue": "^2.6.11", // 2.6.12
"vue-loader": "^15.9.1", // 15.9.4
"vue-template-compiler": "^2.6.11", // 2.6.12
"webpack-notifier": "^1.6.0" // 1.8.0
}
}
I finished the tutorial but didn't get a certificate and it shows as 99% complete on my profile :(