Now for this,
I start on the product-details.ejs file
because there, we have the addToCart button.
And it's now this button which I wanna handle
with JavaScript.
Now to make it selectable,
we could add an ID or we use this ID on the main section
and then look for the first and only button
that we have somewhere as a child
or as a descendant element in that main element,
and that's the approach I'll use.
So therefor all we got to do here
is add a new public script,
so a browser site script,
which I'll name cart-management.js.
And in there, I want to get access to that button
I just explained,
and then trigger a function that sends a request
to add a item whenever we click that button.
So the addToCartButtonElement can be accessed
by using document query selector like this.
And then, I want to select the item with this ID
with product details and then the button in there.
So #product-details to select the item by ID
and then change the CSS selector to this
to select the first matching button descendant element
that's anywhere in an element with the product details ID,
that's what else liked here,
and this will be my addToCart button.
Then I add a function, addToCart,
that's my function here.
And I wanna connect this with a click listener
by adding addEventListener click here
to my addToCart button element
and a point at the addToCart function here.
Inside of addToCart, I now need to send this request.
And for this, I need to target a route
which we have yet to add.
So let's go to the routes folder
and actually add a brand new cart.routes.js file
to keep our routes organized.
And here, I'll just copy the routes set up
from the auth.routes file, paste it in there,
but remove the authController import,
and instead, add the cartController import
by requiring going up one level,
controllers/cart.controller, like this.
And then of course get rid of these routes.
And instead, now I wanna add a new route, a post route.
You could also argue for put or even patch
since adding an item to a cart
could also be considered an update to a cart.
But I'll go for a post.
And the path here should actually be /cart/items
so that we can send the request to this path
to add a new item.
This is one way of writing this.
And actually just as with the admin routes,
all my cart related routes
should start with /cart,
therefor I will omit this here
and just connect this here
to the addCartItem action in the cart controller.
And add this /cart prefix in app.js
as I did it for the admin routes.
So here in app.js, I will import my cart routes
by requiring ./routes/cart.routes,
and I will register them down here
before I have the protect routes middleware
because the cart routes should be accessible
to unauthenticated users,
so here, I'll add cart routes like this.
And now, I will actually add a prefix here
of ./cart so that only requests that start with ./cart,
so where the path starts with ./cart
will make it into the cart routes.
And then the prefix or this part off the path
will be removed by express
and the rest of the path will be evaluated
by the cart routes.
So this here is still reached with /cart/items,
but in this file here, it's just /items,
just as we defined it before for the admin routes.
So, that is the post route for addCartItem,
and with that back in cart management,
we can now send such a post request to that route.
For this I'll again use the fetch function,
this is built into the browser and it returns a promise,
so I'll handled this with async await here
to get the response.
But first of all, we need a path,
and the path is /cart/items as we just saw,
and then we need to configure this request.
And here we got a couple of things that we have to do.
For example, we need to set the method here to post
so to be send the post request,
and then we also have to add a body to that request,
so the data that we wanna attach to the request.
This has to be in the so-called JSON format,
and we can convert an object from Java script to JSON
with the built in JSON class,
which has to static stringify method,
which converts a JavaScript object
into a JSON formatted string,
so into text that follows the JSON format.
I'll pass the object that should be transformed
to JSON stringify,
and then there, I wanna provide the product ID
of the product that should be added to the cart.
We need to do that because in the cart controller,
we do actually look for that.
Here, I'm trying to get the product ID
from the incoming request body.
That of course, means that in the cart.management.js file,
we need that product ID.
And for this, we can again use data attributes.
In the product details view,
on this button which will be clicked,
we can add the data-productid attribute
and populate that with EJS, like this, for example.
This then sets this data-productid attribute,
and we can access this and our front-end JavaScript code
to get hold of that product ID,
which we do wanna add to the cart there.
So now here in addToCart in this function,
which will be triggered upon a click on this button,
we can get the productId by reaching out to this button
on which we added the data property, the data attribute,
and we can access .dataset
to get access to all the data-attribute values.
And then product ID was the name I chose
here after the -productid,
so that's therefor what we access on the dataset,
and that stand this productid
which we set with EJS in the template.
So therefor that is standard value I wanna store
in the productId field in this object
that will be stringified.
Now last, but definitely not least and very important,
for this case that we're sending a body
along with that Ajax request,
we also need to set headers.
Headers are extra metadata
that are added to both requests and responses,
and the browser does add a lot of metadata by default,
for example, it sends our cookie automatically by default,
our session cookie,
but we sometimes need to add our own metadata.
And here, we need to add one specific header
the Content-Type header.
That's a header our backend code will later look for
to correctly parse the incoming request data.
With content type, we describe the kind of data
that will be sent and for forward submissions
that are not handled manually with JavaScript,
this header will be set automatically by the browser.
But for our custom HTTP requests
that are sent with JavaScript, we wanna set it ourselves.
And here, I'm sending some JSON data
so to content type is application/json.
And that's very important,
otherwise the backend later won't work correctly.
And actually, later is now.
We are sending this kind of request
with data attached to it,
and currently, our backend is not prepared for that.
In app.js in our backend code,
we are using this middleware,
express.urlencoded to extract incoming requests data
for requests that were sent upon form submissions.
Now we also have requests that are sent as Ajax requests
and that carry data.
To extract data in the JSON format from such requests,
we need to register and never middleware,
a another middleware built into express,
and that would be the JSON middleware.
We execute JSON and this function then returns
the middleware function,
which is then registered as a middleware.
And now all incoming requests
are checked for JSON data as well,
and that's where we need the extra header
because this tells the backend,
hey, I got some JSON data,
please parse me with the appropriate middleware,
so that's important.
Then we can await this
and ultimately, we'll get back a response.
And we can then check if the response
is maybe not okay,
in which case, I'll just show an alert where I say
something went wrong and I return,
and otherwise, we continue.
Actually, this could also fail for a technical reasons
as you learned before in the course,
so can all Ajax requests.
So you could consider wrapping this
into a try catch block,
like this,
and then also showing an error if we have a technical error.
So not an error response code,
which is a successful response,
but has an error response code,
but a technical error where we don't even get a response.
That could occur if we, for example,
lose connectivity whilst working on this website.
In that case, I'll also show this something went wrong error
and return.
Now, in order to be able to use the response
outside of the try block,
I'll again, turn it into a variable
which I set inside of try,
and therefore it's also available after try,
for example, in this if check.
If we made it past this if check,
and we had no error before,
then we know we have a valid response,
and in that case, I wanna look into the response,
extract the response data,
and in my case, then use this new total items key
that will be part of the response sent back by the server
to update my badge in the navigation.
So therefore now, we need to get the response data
in our front-end JavaScript code here.
We can get this by awaiting response.json,
which is a JSON method that exists on the response object
in the front-end code.
This is a method which will decode the response data
from the JSON format to regular JavaScript,
but it yields a promise
and therefore, we have to await this.
And then once we got the decoded response data,
which will be a regular JavaScript object then,
we can get our newTotalQuantity
by accessing responseData.,
and then I named it a newTotalItems,
so .newTotalItems here.
And now we wanna update the badge here in the navigation.
Now, to get hold of that badge,
we need a way of uniquely selecting it,
and if we have a look at our nav items,
where I have the badge here,
we see that we should be good
if we select the first item with a class of badge
instead of an item with a class of nav-items,
that should uniquely identify this badge next to the cart.
So in cart-management.js,
we can get hold of that badge or that cartBadge element
by accessing document querySelector,
and then there, nav-items.
Whoops, class of nav-items.
And in there, the first item with a class of badge.
Keep in mind that query selector
selects the first matching item,
so this will give us the first badge
that's a child or descendant
of any element with a nav-items class,
and that will be that cart badge in that navigation
in our case.
So then it's this cartBadge element,
which I'll use here at the end of my function
to update its text content
and set that equal to the new total quantity.
This should do the trick.
Now we can test this,
and for this, we have to make sure
that the cart.management.js file
is executed on our pages,
to be precise, on the product detail page.
So in product.details.ejs,
at the end of the head section,
we want to add a script import
and import/scripts/cart-management.js
and defer this as always.
And whilst I'm here, I'll also update the title,
the page title,
and actually set this equal to product.title,
but that's just a side note,
more important is that we add that script.
And once that is done,
if we reload and open the dev tools
to see any potential errors,
if I do actually click addToCart,
I get an error.
And do you have an idea which error that could be?
Well it's related to CSRF.
Because when I do send a request here
from inside my JavaScript code,
still, we have to add the CSRF token
If it's not a GET request,
those are exempt by default.
This is a POST request,
so we have add the CSRF token,
hence we should add an _csrf field to our data
that we're sending with the request.
And now to get a villas token,
we can go to product-details.ejs
and there to the button,
besides adding the product ID,
we can also add the CSRF token
like this with locals.csrfToken.
And then data-csrf is my data attribute.
And with that added,
we can go here into our JavaScript code,
and there, not just extract the product ID from the dataset,
but also the csrfToken
by adding addToCartButtonElement.dataset.csrfToken.
Then we have the csrfToken here,
which we set as a value for _csrf in this request data.
And actually this here should be csrf not csrfToken.
Because on the dataset,
we have to access the property that we set in the template,
and in my case, that's data-csrf,
so that's the key we can access on the dataset.
And if we do that and save this,
if I click addToCart,
now this updates correctly.
Only once though, if I click this multiple times,
it does not update again.
And if I reload, it's even set back to zero.
So it looks like my cart data
is not really being saved in my cart,
some things still seems to not work correctly here.
The reason for dat can be found
in our cart middleware though, here.
When I am initializing a cart
based on data that was already stored in the cart,
it's not enough to just provide the existing items,
instead here in the constructor of the cart model,
we always expect to get the total quantity and total price.
The defaults here are only good
if we are first creating a cart, not thereafter.
So therefor of course,
we should all to provide that data here
when we are re initializing our cart.
For that, I'll add a little helper variable,
sessionCart, which is req.session.cart.
And then I can use my sessionCart here to access the items,
thereafter, also the total quantity,
and thereafter, where I want the price,
I access the total price, like this.
With that if we save this middleware,
now, the cart one value here survives.
And now I can increase this to higher numbers,
and if I reload, that's all the persisted.
So now we can add items to the cart,
next of course, we should add functionality to view the cart
and then all the managed to cart
from inside that cart specific page.