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 SubscribeOk team! We have two great pages - a product catalog and a product show page. We're ready to start adding products to our cart.
To help with this, in the tutorial/
directory - which you should have if you download the course code - copy the cart-service.js
file into assets/services/
.
Just like product, category and color, "cart" is one of the "resources" in our API. Check it out: go to http://localhost:8000/api
. Yep, we have a set of endpoints to create, view or modify a cart. And each cart has an "id".
Now, on the server, we're storing the current user's cart ID in the session. And if the user does currently have a cart, we're dumping its ID onto the page as a global variable. Check it out: right click in the browser, go to "View Page Source" and search for "cart". There we go: window.cartIri = null
because - right now - we do not have a cart associated with our session yet.
The cart-service.js
file holds a number of functions for fetching the cart, adding items to the cart and so on. To get the cart id, this calls getCartIrI()
, which reads that global variable:
... lines 1 - 14 | |
/** | |
* Gets the Cart IRI or null if there is none | |
* | |
* @return {string|null} | |
*/ | |
function getCartIri() { | |
return window.cartIri; | |
} | |
... lines 23 - 149 |
Back in addItemToCart()
:
... lines 1 - 61 | |
/** | |
* Adds an item to the cart and saves it | |
* | |
* @param {CartCollection} cart | |
* @param {CartItem} item | |
* @return {Promise} | |
*/ | |
export async function addItemToCart(cart, item) { | |
... lines 70 - 79 | |
if (cartIri !== null) { | |
response = await axios.put(cartIri, cart); | |
} else { | |
response = await axios.post('/api/carts', cart); | |
setCartIri(response.data['@id']); | |
} | |
... lines 86 - 87 | |
} | |
... lines 89 - 149 |
this is smart enough to work even if the user doesn't have a cart yet. If the cartIri
is not null, it updates it. But if it is null, it creates a new cart, which our API will automatically associate with the current user's session.
By the way, the cart data itself is also stored in the session instead of the database. That fact is completely not important for us in Vue. I just mention it in case you're an API Platform geek and want to see how that was implemented. In a real app, I would store carts in the database.
So from a JavaScript perspective, the important thing is that we have a traditional set of API endpoints for our cart and the current user's cart IRI is available to us on page load.
Now, here's the plan: in product-show
, we're going to first load the full cart data via an Ajax call if there is one. Once we have that, we'll use it to add new items to the cart. Wait... but why do we need to fetch the cart data in order to add an item to it?
The reason is... well... in part because I'm trying to make our life difficult. But that's not the entire reason. Look at addItemToCart
:
... lines 1 - 61 | |
/** | |
* Adds an item to the cart and saves it | |
* | |
* @param {CartCollection} cart | |
* @param {CartItem} item | |
* @return {Promise} | |
*/ | |
export async function addItemToCart(cart, item) { | |
const cartIri = getCartIri(); | |
const itemIndex = findItemIndex(cart, item.product, item.color); | |
if (itemIndex !== -1) { | |
cart.items[itemIndex].quantity += item.quantity; | |
} else { | |
cart.items.push(item); | |
} | |
... lines 78 - 87 | |
} | |
... lines 89 - 149 |
the cart
object is the first argument. That's needed so that if we add a product to the cart and it's already in the cart, it can read the existing quantity and increase it. Now, we could have moved this smartness into our API, which would make our life easier here. But since we don't always have that luxury in the real world, we'll handle it in JavaScript.
Oh, and speaking of complications, we could also make our life simpler by rendering the entire cart object as the global variable... instead of just the IRI. In templates/base.html.twig
, at the bottom, this where we're setting the cartIri
variable. If we output the entire cart as JSON, then we could avoid an AJAX call in Vue. I probably would do that in a real app. But for the tutorial, this AJAX call is going to complicate things in wonderful ways.
<html lang="en-US"> | |
... lines 3 - 9 | |
<body> | |
... lines 11 - 59 | |
{% block javascripts %} | |
<script> | |
{% if app.session.has('_cart_id') %} | |
window.cartIri = '{{ app.session.get('_cart_'~app.session.get('_cart_id'))|iri }}'; | |
{% else %} | |
window.cartIri = null; | |
{% endif %} | |
</script> | |
{{ encore_entry_script_tags('app') }} | |
{% endblock %} | |
</body> | |
</html> |
Anyways, let's get to work! Inside product-show.vue
, we need the cart
object so that we can use it when an item is added to the cart. Down in data
, add a new key to store this: cart
.
... lines 1 - 63 | |
<script> | |
... lines 65 - 71 | |
export default { | |
name: 'ProductShow', | |
... lines 74 - 84 | |
data() { | |
return { | |
cart: null, | |
... lines 88 - 89 | |
}; | |
}, | |
... lines 92 - 111 | |
}; | |
</script> | |
... lines 114 - 131 |
Next, in created()
, call one of the new functions - fetchCart()
and let PhpStorm auto-complete that: we want the one from assets/
. But, hmm: let's make sure PhpStorm stops looking at the tutorial/
directory: right click on it and go to "Mark Directory as Excluded".
Anyways, when I auto-completed that, PhpStorm added the fetchCart
import for us. Back in create, since fetchCart()
returns a Promise
, we can say .then()
and pass a callback with a cart
argument. Inside, all we need is this.cart = cart
.
... lines 1 - 63 | |
<script> | |
import { fetchCart } from '@/services/cart-service.js'; | |
... lines 66 - 71 | |
export default { | |
name: 'ProductShow', | |
... lines 74 - 100 | |
async created() { | |
fetchCart().then((cart) => { | |
this.cart = cart; | |
}); | |
... lines 105 - 110 | |
}, | |
}; | |
</script> | |
... lines 114 - 131 |
If you're wondering why I didn't use await
, good... wondering! If we had used await
, it would mean that this first AJAX call would need to finish before the second one could even start. By using .then()
, both AJAX calls will effectively start instantly.
Ok! Let's make sure the data loads! Refresh the page, go over to the Vue Dev tools, Components... and find ProductShow
. Yes! We have a cart
data set to an object with an items
key. It's empty because, in reality, we don't have a cart yet. But the fetchCart()
function is nice enough to create an empty object for us in this case.
Next, let's hook up the "Add to Cart" button and make sure the user cannot add any items until we're ready.
Hey Cecile!
If you download the course code form this page, this iri
filter comes from a custom Twig extension that we include in that code :). Just in case, you can find the source for the twig extension here: https://gist.github.com/weaverryan/61f09456bcdace4fb857b55106eba439
Let me know if that helps!
Cheers!
Ah yes, sorry for changing a few things! I know that an be inconvenient as a watcher... but it's really useful for us to add some "extra: stuff between tutorials so that we can keep moving, but not cover boring details (like aa Twig extension in a Vue course). There are various other things that have been tweaked - so you might want to use fresh code from part 2... or beware ;).
Cheers!
// 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
}
}
Hi,
I have this error :
The filter
iri
not found inbase.html.twig
`<br />window.cartIri = '{{ app.session.get('_cart_'~app.session.get('_cart_id'))|iri }}';
When I check 'src/twig' indeed it is not defined!
thanks