Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

No Data Duplication! Fancy Computed Prop

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

This cool new completeItems array combines data from two AJAX calls: one for the cart and another for the products that are in that cart. If we could make this available to our template, we could loop over it and start printing out real product data.

... lines 1 - 28
<script>
... lines 30 - 34
export default {
name: 'ShoppingCart',
... lines 37 - 41
watch: {
async cart() {
... lines 44 - 48
const completeItems = this.cart.items.map((cartItem) => (
{
product: products.find((product) => product['@id'] === cartItem.product),
color: cartItem.color,
quantity: cartItem.quantity,
}
));
console.log(completeItems);
},
},
};
</script>
... lines 61 - 71

So... how can we do that? Easy! Create a new completeItems data, set it here, then reference it in the template! We're unstoppable!

Be Careful with Duplicate Data

But... there's a problem with that approach, and I bet some of you see it too. What is it? Duplicated data. Duh, duh, duh!

If we set completeItems onto data, then some of the basic cart item data - the product IRI, color IRI and quantity - would be duplicated in two places. They would be reflected in completeItems... but they would also be on the cart data itself.

And we never want to store a piece of data in multiple places. Because, if we later changed a piece of data - like the quantity of an item - it would change in one spot... but not the other... unless we added extra code to keep them in sync. Yuck. It's really no different than a database: you typically don't want to store a piece of data in multiple places because they could get out of sync.

Computed Prop to the Rescue

So... let's be smarter. Think about it: the only new piece of data we have is the products array that we get back from the AJAX call. If we stored that as data... we could still access this nice completeItems array in the template via a computed property.

Let's do it! Start by adding a data key, which is a function, then returning an object with products initialized to null.

... lines 1 - 28
<script>
... lines 30 - 34
export default {
name: 'ShoppingCart',
... lines 37 - 41
data() {
return {
products: null,
};
},
... lines 47 - 63
};
</script>
... lines 66 - 76

Down below in the watcher function, instead of const products, say this.products... and reference this.products below in the loop.

... lines 1 - 28
<script>
... lines 30 - 34
export default {
name: 'ShoppingCart',
... lines 37 - 41
data() {
return {
products: null,
};
},
watch: {
async cart() {
... lines 49 - 51
this.products = productsResponse.data['hydra:member'];
... line 53
const completeItems = this.cart.items.map((cartItem) => (
{
product: this.products.find((product) => product['@id'] === cartItem.product),
... lines 57 - 58
}
));
console.log(completeItems);
},
},
};
</script>
... lines 66 - 76

Next, add a computed key with one new computed prop. Call it completeCart().

... lines 1 - 28
<script>
... lines 30 - 34
export default {
name: 'ShoppingCart',
... lines 37 - 46
computed: {
completeCart() {
... lines 49 - 63
},
},
... lines 66 - 73
};
</script>
... lines 76 - 86

Before we do anything else in this function, if someone calls us and the cart data is not ready yet, we should also return null. So if !this.cart... or if !this.products - if we also haven't finished loading the products - then return null. This means that if completeCart returns null, things are still loading.

... lines 1 - 28
<script>
... lines 30 - 34
export default {
name: 'ShoppingCart',
... lines 37 - 46
computed: {
completeCart() {
if (!this.cart || !this.products) {
return null;
}
... lines 52 - 63
},
},
... lines 66 - 73
};
</script>
... lines 76 - 86

Now, copy all this completeItems stuff from watch, move it here, but instead of logging, return a new object. We'll make this look just like the cart... just for consistency: with an items key set to completeItems.

... lines 1 - 28
<script>
... lines 30 - 34
export default {
name: 'ShoppingCart',
... lines 37 - 46
computed: {
completeCart() {
if (!this.cart || !this.products) {
return null;
}
const completeItems = this.cart.items.map((cartItem) => (
{
product: this.products.find((product) => product['@id'] === cartItem.product),
color: cartItem.color,
quantity: cartItem.quantity,
}
));
return {
items: completeItems,
};
},
},
... lines 66 - 73
};
</script>
... lines 76 - 86

That should do it! If we've done everything correctly, after the cart data has finished loading, the watcher will call our function, we will make an AJAX request for the products, and then, finally, in our template, we will reference the completeCart variable, which will combine all that data once it's available. Remember: one of the magic things about computed properties is that Vue will automatically re-render and re-call our computed function whenever any piece of data that it references - like this.cart or this.products - changes.

Rendering Complete Cart Data

So let's go to our template. We basically want to update everything from cart to completeCart. Copy that, use it on the v-if and inside the v-for.

Then, since cartItem.product will now be an object, we can prove everything works by printing cartItem.product.name. Oh, and I'll change one more spot to completeCart.

<template>
<div :class="[$style.component, 'container-fluid']">
<div class="row">
... lines 4 - 5
<div class="col-xs-12 col-lg-9">
... lines 7 - 8
<div class="content p-3">
<loading v-show="completeCart === null" />
<div v-if="completeCart !== null">
<div
v-for="(cartItem, index) in completeCart.items"
:key="index"
>
{{ cartItem.product.name }} ({{ cartItem.quantity }})
</div>
<div v-if="completeCart.items.length === 0">
Your cart is empty! Get to shopping!
</div>
</div>
</div>
</div>
</div>
</div>
</template>
... lines 28 - 86

Testing time! Back at the browser... ha! I didn't even need to refresh! It's already printing the product name. I know, it doesn't look that impressive yet, but we did just put together a lot of data.

Check out the Vue dev tools for this component: cart data, products data and a beautiful completeCart computed prop that allows us to easily use the data we need in the template, without duplicating anything.

Next: we're still missing one piece of data inside completeCart: the color. This is still an IRI string... but what we need is the color data, which will include the hex color so that we can render a color box on the screen. Let's finish that and learn a cool trick for making AJAX requests in parallel.

Leave a comment!

4
Login or Register to join the conversation
Cecile Avatar

Thank you for this formation, however could you do an episode on translation in Vue.js?

Reply

Hi sidi!

As of today, we have no plans on releasing a tutorial covering i18n and Vue, although this has been added to our ideas list!

In my view (and my coworkers here might be able to share better insights of this). i18n is very well covered by Symfony, and what sites usually do is: use Twig to create a JavaScript object with the translated phrases your application uses (this can be also generated into separated js files and cached), then use this object to populate your various strings along the app.

Aside from this, there is a plugin called Vue-i18n that is worth looking at which might provide a better solution for client-side only translations!

Reply
Cecile Avatar

Hi Matias!
Thank you very much for your answer, how do you do in twig to get the list of all the translations by language you have in your symfony apps?

Reply

Hey Sidi!

As far as I know, the best course of action would be to isolate the phrases that the Vue app needs. Set them up in variables inside the controller (it could be an associative array for multiple phrases) like this:


 $translated = $translator->trans('Symfony is great');

As stated in the Translations official documentation <a href="https://symfony.com/doc/current/translation.html#basic-translation&quot;&gt;here&lt;/a&gt;.

Once you have this, you can easily transfer those translated phrases onto the twig template, where you can get them into a JavaScript object.


const translations = [
    {% for key, phrase in translations %}
    '{{ key|e('js')  }}':  '{{ phrase|e('js') }}',

    {% endfor %}

];
Reply
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