Learning Vue.js is fun – if I run into a problem that has taken me some head scratching time to solve and/or and no easy Stack Overflow answer, why not writing a blog post for you and my future self? 🙂 Today’s stumbling block is bi-directionally binding of a Component (v-model), to the root data object – being the Components generated in a v-for loop.
Sounds unclear? Think about a lot of instances of a Component containing, say, checkboxes or radiobuttons, automatically generated from an array. It’s a quite frequent scenario, at least in my projects, so let’s have a look.
UPDATE [October 2017]: The article refers to Vue 1.x – with version 2, things have slightly changed – I’ve updated the Checkbox section with code working with Vue 2.5.2.
Checkboxes
[See the Vue 2.5.2 update at the end of this section] I’m going to show you a couple of different setups and solutions. First initial arrangement is as follows:
While the Javascript and the Vue code is:
In this scenario it is particularly important for me that the v-model
of each checkbox is bound to the boxes
array of objects, so that other functions are able to refer to them (say, passing their values down to the Photoshop JSX layer). But clearly the checkboxes cry to be implemented as Vue Components! So let’s do that – first in the HTML as a template – that, by now, is mostly an empty placeholder:
We need to register the Component in the JS, so:
Please note that I’m using both camelCase and kebab-case, as the Vue.js doc suggests. I’ve declared three props:
boxIndex
: will be 0, 1, 2, 3… 9.boxName
: the checkbox string label, like"one"
,"two"
, etc.boxStatus
: will be the Checkbox checked status (either true or false), and will be bound to thedata.boxes
Object.
OK, how do we implement the v-for loop
, so that all the ten Components instances are rendered on the page? A good starting point is:
So far so good, nothing that you can’t figure by yourself. Here comes the tricky part, have a look.
The checkbox value
is { { boxIndex } }
, that is passed as a prop using the shorthand syntax :box-index
(which stands for v-bind:box-index
) and assigned to the special property provided by the v-for
loop $index
. Note here camelCase and kebab-case. Similarly, { {boxName} }
and { {boxStatus} }
are passed in the loop using :box-name
and :box-status
, and the latter is used to fill the checked
property of the input tag.
This way, the component “knows” whether to be initialized as checked / unchecked depending on the status
property of the respective object in the boxes
array. I’ve also added as a label for each box component a string containing them all, and at the bottom the JSON version of the boxes object to check if binding works properly.
We’re slowly getting there: each checkbox shows its index, name and status, and the one labelled “four” correctly initializes itself as “checked”. But if you try clicking them, neither their status
, (true/false in the label) nor the root boxes
object updates. This binding requires a couple of new lines of code, one in the Component’s template – i.e. a click handler:
and the corresponding function in the Components declaration in the JS:
This triggers a change in the Component’s boxStatus
property, so that when you click each checkbox, you see that its own status
(logged in the label) changes accordingly. Which is cool, but there’s just one piece of the puzzle missing – can you find it? The Component’s boxStatus
is updated, but the Component (or better: each Component) has an isolated scope of its own! In fact, the boxes object, logged as JSON is not changed (every status is false but one). In order to make a two way binding, you need to add the binding type modifier .sync to the prop, like:
This correctly set everything: each Component is initialized with the boxes object, and two-ways bound to it – the same way it was just before refactoring with Components. See the full code in this JSBin here
Vue 2.x October 2017 UPDATE With the new version, .sync
has been first deprecated, then re-introduced (see here). Yet, letting a child component modify parent data is considered an anti pattern in Vue: in this case, you should emit events and use a new Vue instance as a hub. See the following JSBin as an example.
RadioButtons
Following the same idea, let me show a different solution for RadioButtons, using events. The component looks like that:
You see that there are channelName
, channelIndex
and channelChecked
props, with a similar changeRadio
function for the click handler. The loop is based upon a channels
array, see the JS side:
Clicking on each radiobutton component now dispatches a 'radioClick'
message (defined in the component’s methods
object, and carrying the channelName
as the payload), handled by the root Vue instance (see both events
and methods
objects).
The handleRadioClick then adjust the checked
properties on each object within the channels
array, that the radiobuttons are bound to – please note that because of this, there’s no need to add .sync
in the template now.
See it in action in this JSBin here.
That’s it! It took me a while to figure this out. Possibly the second example – where the parent (root, here) Vue instance is in charge of updating the data object, to which the Component is bound – is “more proper” than the first one – where the Component did it by itself.
Hope this helps! 👐🏻