Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Accidentally Modifying Prop Objects

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Let's render the rest of the details for each row.

Rendering the Color Square

Inside cart-item, start by adding some some structure - <div class="col-2"> - with the name inside:

<template>
<div :class="[$style.component, 'row', 'p-3']">
<div class="col-2">
{{ item.product.name }}
</div>
... lines 6 - 11
</div>
</template>
... lines 14 - 34

Then add another div with col-1 to hold the product color, if there is one. Do that with a <span class="color-square" />:

<template>
<div :class="[$style.component, 'row', 'p-3']">
<div class="col-2">
{{ item.product.name }}
</div>
<div class="col-1">
<span
class="color-square"
/>
</div>
</div>
</template>
... lines 14 - 34

Head down to the bottom to style this: .color-square with some CSS to make this a 25px square with rounded borders. I'm also going to add a :global after .component. We talked about this in the first Vue tutorial: because we're nesting the .color-square inside of .component, it's safe to add :global and will avoid an extra, unnecessary module prefix before color-square in the final CSS.

... lines 1 - 26
<style lang="scss" module>
... lines 28 - 29
.component :global {
... lines 31 - 32
.color-square {
display: inline-block;
width: 25px;
height: 25px;
border-radius: 4px;
}
}
</style>

All we need to do now is add a background-color set to the dynamic color. Add a style attribute - actually a :style attribute - set to an object with a backgroundColor key. Remember: instead of background-color, Vue wants you to use camel-case names: it handles converting things. Set this to a string using our magic "ticks": #, ${} then, to keep my template readable, use a new computed prop: hexColor.

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 6
<div class="col-1">
<span
class="color-square"
:style="{
backgroundColor: `#${hexColor}`
}"
/>
</div>
</div>
</template>
... lines 17 - 49

Copy that, then head down to define it: computed, hex(), then return, if this.item.color - which will either be null or an object - return this.item.color.hexColor else fff, which will be an invisible white box. You could also use transparent. Or, if there is no color, you could avoid rendering any color box with v-if.

... lines 1 - 17
<script>
export default {
name: 'ShoppingCartItem',
... lines 21 - 26
computed: {
hexColor() {
return this.item.color ? this.item.color.hexColor : 'fff';
},
},
};
</script>
... lines 34 - 49

Anyways, let's see how it looks. Very nice!

The Quantity Input

Next up is quantity. I want this to render an input box so the user can easily change the quantity. Add a div with class="col-3" so that it lines up with the headers, which are each col-3. Then say <input.

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 15
<div class="col-3">
<input
... lines 18 - 21
/>
</div>
</div>
</template>
... lines 26 - 62

Let's think: we want the value of this input to be equal to the item.quantity prop. And when we change the input, we want that to update the item.quantity value. Whelp, that is exactly the job of v-model. Add v-model - with the cool .number so that Vue converts the input into a number - equals item.quantity. Finish with class="form-control" and type="number".

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 15
<div class="col-3">
<input
v-model.number="item.quantity"
class="form-control"
type="number"
min="1"
/>
</div>
</div>
</template>
... lines 26 - 62

Oh, and to make sure the input is small. At the bottom, add an input style with width: 60px.

... lines 1 - 43
<style lang="scss" module>
... lines 45 - 46
.component :global {
... lines 48 - 56
input {
width: 60px;
}
}
</style>

Let's try it! Back at the browser, it instantly shows the correct numbers!

Whoops! We Modified a Prop!

But... there's a problem with this... a big problem. Do you remember how we're never supposed to modify a prop? Well... we just did. My bad!

The item is passed to us as a prop. Then, thanks to v-model, we're modifying the quantity key on that prop. Yikes!

In reality, this isn't quite as bad as changing, or replacing an entire prop. Think about it: on the top-level ShoppingCart component... this completeCart computed property is what is passed down to the other components. And each object inside of the items array will be the exact same object in memory as the item prop in the cart-item component. So if we change the item.quantity key in cart-item, that will also change the quantity property on that object in the top-level ShoppingCart component.... because... those are literally the same object!

We can see this! Change our floppy disk quantity to 12. In the Vue dev tools, find the first ShoppingCartItem component. Its item.quantity value is, of course, 12... which makes sense because we just set that value directly.

Now go look at the top-level ShoppingCart component, find the completeCart computed property and open the first item. Hey! It has quantity 12!

So... if a prop is an object... is modifying a key on that object really a problem? After all, everything seems to stay "in sync"!

In some cases... maybe not. But, you're playing with fire. In our situation, it causes things to... well... get weird.

Change the quantity back to 10. Look at the Vue dev tools! The quantity prop did not update! In reality, it did update... but you can't see it until you click off of the ShoppingCart component and re-open it. There is quantity 10.

The problem is that this just isn't how Vue is supposed to work... even if we're "kind of" getting away with it. And so, weird things like this start to happen.

And the Vue dev tool is not the only weird thing. Our situation is especially strange because, by accident, we're literally changing the quantity of a computed property!

That means that the we are not updating the quantity on the actual "source of truth", which is the cart data. Find the cart data and open the first item. Yep, it still has quantity 15: the original quantity. That is not changing. So if the shopping cart re-rendered right now, it would completely wipe out the updated value on the computed property and replace it with the one from data.

In other words... we accidentally modified a prop by modifying a key of an object! And while you might sometimes get away with this, you're asking for trouble.

Replacing v-model with value=

For us, it means that we cannot use v-model in this situation. But... that's ok! We already know the correct solution! Change this to :value="item.quantity" so that it renders the correct value. In a few minutes, we'll add some code so that when the input changes, we emit an event, which is the proper way to notify a parent component that something has changed.

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 15
<div class="col-3">
<input
:value="item.quantity"
... lines 19 - 21
/>
</div>
</div>
</template>
... lines 26 - 62

Rendering the Price

Before we do that, there are two more quick things I want to render. Add another <div class="col-3">, a $ then print a totalPrice computed property:

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 24
<div class="col-3">
${{ totalPrice }}
</div>
</div>
</template>
... lines 30 - 71

Next, import the formatPrice helper that we've been using... then add a totalPrice function under computed. Return formatPrice() of this.item.product.price times this.item.quantity.

... lines 1 - 30
<script>
import formatPrice from '@/helpers/format-price';
export default {
name: 'ShoppingCartItem',
... lines 36 - 41
computed: {
... lines 43 - 45
totalPrice() {
return formatPrice(this.item.product.price * this.item.quantity);
},
},
};
</script>
... lines 52 - 71

Finally, up in the template, add one more column with a button, some styling classes and the word Remove. We'll make this actually do something in a few minutes.

<template>
<div :class="[$style.component, 'row', 'p-3']">
... lines 3 - 28
<div class="col-3">
<button class="btn btn-info btn-sm">
Remove
</button>
</div>
</div>
</template>
... lines 36 - 77

Moment of truth! Find your browser. Yes! That looks wonderful.

Next: when the user changes the quantity, we need to update the cart data on the ShoppingCart component and save the changes back to the server. Let's do that with - of course - events!

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

This course is also built to work with Vue 3!

What JavaScript libraries does this tutorial use?

// 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
    }
}
userVoice