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 SubscribeThis 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!
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.
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.
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.
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!
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?
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">here</a>.
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 %}
];
// 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
}
}
Thank you for this formation, however could you do an episode on translation in Vue.js?