Cart API & Data

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

Ok 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/.

The Cart API Endpoints & Service

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.

<!DOCTYPE html>
<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>

Fetching the Cart via AJAX

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.

Leave a comment!

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