Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

More Mixin Magic

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

We created this mixin with the goal of sharing Vue logic between the product-show component and the new shopping-cart component that we're about to really start building. Specifically, both need a cart data and both need to make an Ajax call to populate that cart data. That stuff is now inside the mixin. Woo!

Both pages will also need the ability to add an item to the cart. It might seem weird to add an item to the cart from the shopping cart page, but we're going to add a "featured product" sidebar on this page with an "add the cart" button. So, in preparation for that, let's also move the add to cart functionality from product-show into the mixin.

... lines 1 - 75
<script>
... lines 77 - 84
export default {
name: 'ProductShow',
... lines 87 - 124
methods: {
async addToCart() {
if (this.product.colors.length && this.selectedColorId === null) {
alert('Please select a color first!');
return;
}
this.addToCartLoading = true;
this.addToCartSuccess = false;
await addItemToCart(this.cart, {
product: this.product['@id'],
color: this.selectedColorId,
quantity: this.quantity,
});
this.addToCartLoading = false;
this.addToCartSuccess = true;
document.getElementById('js-shopping-cart-items')
.innerHTML = getCartTotalItems(this.cart).toString();
},
updateSelectedColor(iri) {
this.selectedColorId = iri;
},
},
};
</script>
... lines 152 - 169

Moving addToCart to the Mixin

This literally means that we're going to move the addToCart method from the component into the mixin. So let's copy it, delete it, and then, in the mixin, I'll add methods and then paste.

import { addItemToCart, fetchCart, getCartTotalItems } from '@/services/cart-service';
export default {
... lines 4 - 11
methods: {
async addToCart() {
if (this.product.colors.length && this.selectedColorId === null) {
alert('Please select a color first!');
return;
}
this.addToCartLoading = true;
this.addToCartSuccess = false;
await addItemToCart(this.cart, {
product: this.product['@id'],
color: this.selectedColorId,
quantity: this.quantity,
});
this.addToCartLoading = false;
this.addToCartSuccess = true;
document.getElementById('js-shopping-cart-items')
.innerHTML = getCartTotalItems(this.cart).toString();
},
},
};

Oh, and this references two pieces of data that are related to this process: addToCartLoading and addToCartSuccess. Those should also now live in the mixin. Go find them, copy them, delete them, and paste them inside the mixin.

... lines 1 - 2
export default {
data() {
return {
cart: null,
addToCartLoading: false,
addToCartSuccess: false,
};
},
... lines 11 - 34
};

If we stopped now, this probably would work because the new data and the new method are, as I mentioned, basically copied into our component. But it is a bit weird because the mixin references things like this.product and this.selectedColorId... but those don't live in the mixin. That's technically okay... and we sometimes do things like this in PHP with traits... which are very similar to mixins.

But I don't like. It makes the mixin hard to use correctly. The only way you can use this mixin is if your component has data with these exact names... which you would need to, kind of, dig a little to find. It also makes it easy to break your mixin... because if you renamed a piece of data in the component and updated all the references in that component... it wouldn't be very obvious that the mixin is also relying on this.

Being Explicit about what your Mixin Needs

Fixing this properly is simple enough. Instead of referencing these three pieces of data down here, let's force them to be passed as arguments to the method. So: product, selectedColorId and quantity:

... lines 1 - 2
export default {
... lines 4 - 13
methods: {
async addToCart(product, selectedColorId, quantity) {
... lines 16 - 32
},
},
};

Then we can update the code below: change this.product to just product, and then product, selectedColorId and quantity.

Oh, but ESLint is mad because we can shorten this from quantity: quantity to just quantity.

... lines 1 - 2
export default {
... lines 4 - 13
methods: {
async addToCart(product, selectedColorId, quantity) {
if (product.colors.length && selectedColorId === null) {
... lines 17 - 18
}
... lines 20 - 22
await addItemToCart(this.cart, {
product: product['@id'],
color: selectedColorId,
quantity,
});
... lines 28 - 32
},
},
};

Ok! I like this function... but it will totally not work yet. This method is called directly in the component thanks to the @click on the add to cart button. But now that this method needs 3 arguments... we can't use it like this.

One option is to switch to the "inline" @click syntax, where we say addToCart() and pass it the three arguments it needs. That's a totally valid way to do it. Or, we could rearrange things a bit.

Change it back to just point to the addToCart method. Then, in the mixin, rename the method from addToCart to, how about, addProductToCart:

... lines 1 - 2
export default {
... lines 4 - 13
methods: {
async addProductToCart(product, selectedColorId, quantity) {
... lines 16 - 32
},
},
};

so that our @click doesn't call this directly. Finally, in the component, find methods, re-add addToCart() and call the mixin method: this.addProductToCart() with this.product, this.selectedColorId and this.quantity.

... lines 1 - 75
<script>
... lines 77 - 83
export default {
name: 'ProductShow',
... lines 86 - 121
methods: {
addToCart() {
this.addProductToCart(this.product, this.selectedColorId, this.quantity);
},
... lines 126 - 129
},
};
</script>
... lines 133 - 150

Ok! That should do it! Testing time! Find your browser and... I'll refresh to be safe. We just want to make sure that this all still works. Hit add to cart and... it... sort of worked? I didn't see any animations, but the header did change from 15 to 16.

What a Bad data key Looks Like

Let's check the console to see what's going on:

property or method, addToCartSuccess, and addToCartLoading are not defined on the instance.

And above that it says:

the data option should be a function that returns a per instance value in component definitions.

Ah! I think I know what I did wrong, and it's kind of cool! Go back to the mixin and scroll up to data. Yup! data should be a function that returns an object not just a key set to an object. That's easy to do because some Vue keys are functions and others are just set to values. Because of this mistake, our mixin was not actually defining any of these pieces of data.

But... wait. If that's true, then how did this work at all? I mean, I did see the cart header update. And if we refreshed, we would see that it did truly add the item to the cart. How is that possible if there is no cart key in data?

The answer is kind of cool! First, though there is no cart data, saying this.cart = does still work! How? Remember: each Vue component is a normal JavaScript object... and it is valid to say this.cart = on any JavaScript object, even if that property doesn't currently exist. So basically, our created function added a normal, non-reactive JavaScript property. And then, we referenced that normal JavaScript property via this.cart in other parts of our component. That was enough to make our add to cart work!

The reason that things appeared broken is that, for addToCartSuccess and addToCartLoading, we reference those in our template. Vue makes data available as variables in the template, but it does not make normal, non-Reactive properties that we randomly add to our object available as variables. So while we could have technically referenced this.addToCartSuccess in our component, referencing addToCartSuccess in the template does not work.

So... I messed up, but it was a nice chance to think about how things work under the hood inside our Vue instance. Let's fix it: change data to a function that returns our data object.

... lines 1 - 2
export default {
data() {
return {
cart: null,
addToCartLoading: false,
addToCartSuccess: false,
};
},
... lines 11 - 34
};

Back in the browser, it looks like it already reloaded. Hit add to cart and... it works!

Using the Mixin in shopping-cart

We now have some really nice, reusable code for our shopping-cart component.

So let's go use it there! Import ShoppingCartMixin from @/mixins/get-shopping-cart:

... lines 1 - 16
<script>
import ShoppingCartMixin from '@/mixins/get-shopping-cart';
... lines 19 - 27
</script>
... lines 29 - 39

Then, like before, add a mixins key set to an array with this inside:

... lines 1 - 16
<script>
import ShoppingCartMixin from '@/mixins/get-shopping-cart';
... lines 19 - 20
export default {
name: 'ShoppingCart',
... lines 23 - 25
mixins: [ShoppingCartMixin],
};
</script>
... lines 29 - 39

We're not going to do anything with that mixin yet, but we can already check if it's working. At your browser, click to the shopping cart and go down to the Vue dev tools. Find the ShoppingCart component and... yes! It has a cart data and you can see that it's already been populated via AJAX!

And that means we are dangerous. Next, let's use this cart data to build the page.

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