So let's move to the frontend
and make sure that we send such a JavaScript-driven request,
such a AJAX request,
to this route from the frontend.
And for this, of course,
it's again time for frontend JavaScript.
I'll add a new file in the public scripts folder for that,
and I'll name it product-management.js.
Filename, of course, is up to you,
but it will help us with managing products.
It will help us with deleting products specifically.
Now, in this product-management.js file,
I wanna listen to clicks on the Delete buttons.
And, of course, we have multiple Delete buttons
if we have multiple products.
I wanna listen to clicks on all of them.
And then, of course, I need to find out
which Delete button for which product was clicked
so that we send the correct request
with the correct ID to the backend.
And then once we did all of that,
we, of course, do want to send that request
with that ID of the clicked item
so that this delete is initiated.
And then once that delete succeeded
and we got back a response,
I wanna update the DOM with browser-side JavaScript
to manually remove the product item
for which the Delete button was clicked.
So plenty of things to do.
Let's start by adding event listeners
to all these Delete buttons.
And for that, of course, we need to get access
to those Delete buttons.
Now, for that,
if we have a look at the product-item.ejs file,
we can see that all these buttons
have, of course, btn classes,
and I don't wanna select by that.
I would also select wrong things if I did that.
Instead, I will select with help of that product-item class,
which I have on this article,
and then select the first and in this case only button
that I have inside of these product-items.
So in the product-management.js file,
this frontend JavaScript file,
I will get my deletion or my deleteProductButtonElements
by using document.querySelectorAll,
and that's important.
We need querySelectorAll here since that will then give us
an array of all matched elements
instead of just the first matching element.
And I will select all the buttons
inside of elements that have the product-item class,
and this is the appropriate CSS selector for that.
So with that, we got all these button elements.
And now, of course, I wanna add event listeners
to all of them.
And to achieve this,
we can loop through them, for example, with a for-of loop
to get the deleteProductButtonElement
whilst looping through the deleteProductButtonElements.
And yes, these are long names,
but I wanna use explicit, clear names here
so that it's obvious what's stored inside of them.
And then for the single elements here,
I want to add event listeners and listen to click events.
And then, of course, I want to trigger a function
when such a button is clicked,
a function that should actually have a name,
and the name could be deleteProduct
because that's what we'll do.
So here we wanna point at deleteProduct at this function.
Now we got the click listener.
Of course, now we need a way of finding out
for which product the Delete button was clicked.
And there will be various ways of doing that.
One great way, which I also showed you before in the course,
is to utilize data attributes.
We can add the special data dash attribute to our elements.
And when we do so,
we can add arbitrary data pieces, simple data pieces,
to our elements.
For example, here we could add
a product id data piece to this button
with data-productid.
The part after the dash is up to you.
And then here, use EJS to inject the concrete value
which will be different for every product item.
And, of course, it should be product.id.
Now, inside of this deleteProduct function here,
we can get access to that by accessing the dataset property
of the button for which this event occurred.
Now, the only remaining problem here, of course,
is that we got multiple buttons.
How do we know which button was clicked
inside of deleteProduct?
For this, we can utilize the default event object
which we get here.
You might remember that we have these event parameter values
which we get automatically
for all the functions that are triggered upon events
so that we use with event listener,
addEventListener in the end.
We get a event object that describes the event
that occurred automatically by the browser.
And on this event object, we have a target property.
This target is the element which caused the event
or on which the event occurred.
And in our case, we know that this will be the button,
the buttonElement on which the user clicked,
so the concrete button for a concrete product
on which the user clicked.
And then the productId, of course, can be extracted
by using this buttonElement and there the dataset property
because that's how we get access
to these data dash attribute values.
You get access with help of the dataset property.
And on the dataset property,
the parts after the dash will be properties.
So in this case I'll have a productid property
written like this on this dataset object
which it turns out to be for the button
on which the click occurred.
And that is then the product ID
which I wanna send to my backend
when I send this AJAX request.
And speaking of that, that's the next thing I wanna do.
I wanna send this request to the backend.
And before doing that, we got various options.
We can use third-party packages like the axios package,
which is a super popular package for sending HTTP requests
from inside your JavaScript code.
It's great, and if you use that, that's absolutely fine.
There's nothing wrong with it.
Here, however, I will use
an approach that's built into the browser.
I'll use the fetch function,
which is available out of the box in browsers.
The fetch function sounds like it's only used
for fetching data, for getting data,
but actually we can also use it for sending POST
or, in our case, also DELETE requests.
Now, fetch, first of all, wants a URL
to which the request should be sent.
And in our case,
that should be the domain on which we already are,
so we don't need to repeat that here,
slash and then the path on that domain
where we wanna send the request to.
In our case, that would be /admin/products and then the id.
So here I'll then concatenate the productId.
You could also repeat your domain name, by the way.
And you would need to do that
if you send a request to a server
that's not the server that's hosting this website.
If you're sending the request to a different server
with a different domain,
you need to include the domain here.
If you're sending a request
to the very server that served this page, though,
then you don't need to repeat the domain.
You just need an absolute path that starts with slash,
and then it will automatically be appended
to the domain on which you currently are.
So this will send a request
to /admin/products/ the id of that product,
but now we also need to configure this request,
which we do by passing a second-parameter value
to the fetch function
which is an object full of key-value pairs
that we can use for configuring.
And we need to configure here
because by default we would send a GET request,
and that's not what I wanna do here.
I wanna send a DELETE request instead.
Now, we can change the method, the HTTP method,
that's being sent or that's being used
by setting the method property on this configuration object.
And the default is GET, we could set it to POST,
but here we'll set it to DELETE.
And that's the first time we use such a special HTTP method
which actually isn't that special.
The only special thing is that we can't send it
when we just work with links and forms
without any frontend JavaScript,
but that, instead, we need JavaScript to send requests
that have this kind of method.
But then it's absolutely normal to use this feature.
And here, sending a DELETE request simply makes more sense
than sending a POST request with the form,
though we could also do that.
That would not be bad. It wouldn't cause problems.
But if we have the opportunity
of sending a more specific DELETE request instead,
we might simply do that.
Now, there is one thing we should also do for this request,
and that involves our CSRF token.
We need to send the CSRF token with that request as well
because otherwise the request will be blocked by our server,
because the CSRF protection we added on the server
also affects the request we send with frontend JavaScript,
not just requests that are sent
because some form is submitted.
And that's a good thing.
We want that protection against all requests
no matter how they are sent.
Now, to send the CSRF token along,
we could add it in the request body.
But it turns out that for DELETE requests,
you typically don't have a request body.
Instead, we simply add the CSRF token in the URL
as a query parameter
by adding the _csrf query parameter.
You add query parameters by adding a question mark
and then your key-value pairs.
And then we need to get access to that CSRF token
to add it into this URL.
Now, we can get access
just as we get access to the productid.
Back in the template, besides adding data-productid,
we could add data-csrf to also use EJS here
to access locals.csrfToken
and add that CSRF token that we need on the data
or with a data attribute
to this Delete button as well.
And once we did that, in product-management,
we can, of course, get our csrfToken here
by accessing buttonElement.dataset.csrf, oops, csrf
because I chose csrf as an identifier after data dash.
So now we can add that csrfToken here,
and that should then send a valid request.
Now, that's actually all we need to configure here
since we're not adding any body, any request body,
to that request.
We also don't need to set any special headers or metadata,
and hence sending the fetch request like this is enough.
Now we, of course, wanna wait until we have a response
and then update what we see on the screen.
We wanna remove the item on the screen which was deleted
without reloading the screen,
which we could, of course, also do.
But I want to update the DOM
from inside frontend JavaScript code
to delete the item manually with JavaScript.
Now, fetch returns a promise,
so I wanna wait until I got that response,
so once that promise yielded that response,
and hence we can use async await here as well.
That works in the browser as well,
so we can run async await here and await the result of this
which is the response like this.
And then we can check if the response is okay
or if we got back an error
by checking if not response okay,
so if we don't have an okay response,
if the status code of the response is not 200
or any other 200-ish number,
in that case, I wanna show an error message to the user,
and I'll use the built-in alert for that and just say
Something went wrong.
Of course, you could alternatively also write code here
that shows the error message right in the DOM
so that you add a modal or some error box
or anything like that.
I'll go for this alert,
and then return so that no other code executes thereafter,
then we can move on.
Now we know that the response was okay,
and that's now when I wanna update the DOM.
And for this, of course,
I again wanna remove this product-item
for which the Delete button was clicked.
Now, the good thing is we know which button was clicked.
We got access to that inside of deleteProduct.
So if we now wanna find the product item that belongs to it,
we could, for example, do some DOM traversal
to move up to this article.
The button is inside that div, which is inside that div,
which is inside that article.
So we could say from the button's perspective,
since we got access to the button in our function,
we can access the parent element,
and then the parent of that, and then the parent of that,
and that would be the product item
which we wanna remove from the DOM.
Of course, the downside of using this
is that whenever we move that button
into another level of nesting,
we have to update the JavaScript code.
But to practice DOM traversal and see how that works,
and since I don't plan on changing the HTML structure here,
I will use that approach.
That means that here,
where I do know that I wanna remove the item from the DOM,
I'll use my buttonElement and access the parentElement
and then access the parentElement of that
and then access the parent element of that
since accessing that first parentElement gives us
that first div.
Accessing the second parentElement here in the middle
gives us that second div.
And then accessing the parentElement here at the end,
that gives us the article,
and it's this article which I wanna remove.
Actually, if we are precise, it's not the article.
Instead, in all products, it's that list item.
So we should actually go up one level more
and access .parentElement again to access that list item.
And, yeah, that can be a bit hard to read,
but, again, I wanna practice this DOM traversal thing.
And with that all done, we now got access to the list item
that holds the article that we wanna remove,
therefore we can now call remove on that.
That's a built-in method that exists on DOM elements,
and it will remove that element from the DOM
when we call that method.
Okay, so that should actually do the trick.
Now we need to make sure
that we actually do include that product-management.js file
on this all-products page.
So here in all-products.ejs,
below this link I'll add a script import
where my source is /scripts/product-management.js,
and, as always, we defer this
so that this only becomes active
after the entire page content has been parsed.
Now with that, if we reload this page,
and we open the Dev Tools
to see the JavaScript Console there
to see any possible errors,
if I click on this Keyboard item and the Delete button,
I get an error.
I get an error from the server, a 500 error,
and hence my something went wrong pop up here.
So let's see what's wrong by going to the server side
where we see the server-side error message.
It's invalid csrf token error.
Now, if we reload, we see that it actually was deleted,
so that actually worked,
but, still, we got that error.
But, actually, that error has a reason.
If we click Delete on the Magic Trackpad here,
and we have a look at the Network tab
of the browser Dev Tools
and clear that here before clicking Delete,
we see that actually two requests are sent here.
The first request is the correct one.
It sends a request to that URL
that includes the ID and the CSRF token,
and it's this DELETE request.
Then we got a redirect, though,
because, of course, in my controller
here on the backend, in the admin-controller,
I am redirecting.
Now, that's the problem.
I am redirecting
even though I'm sending a frontend-based JavaScript request.
And the idea behind those JavaScript-driven requests
is that we don't load a new page,
but we stay on the existing page.
Hence, by default, it doesn't support a redirect response,
and that is in the end what causes this error here
even though it's a bit cryptic.
The proper thing to do here on the backend
is not to redirect.
But since we will target and trigger this action
with help of an AJAX request,
we wanna send back a response in JSON format.
This can be done by calling the json method on response,
and there, we can now pass object
which will be transformed to JSON by Express
where we, for example, just send a message
like Deleted product
since I don't really need any other response data.
But now this is not a redirect
but instead a different kind of response
where we just respond with some data.
And if you recall the section where I introduced you
to AJAX and these frontend JavaScript-driven requests,
you might recall that often the idea behind using AJAX
is to just exchange data.
That's what it's meant to be used for.
So here we wanna send back such a data response
using the commonly used JSON format
which is supported out of the box by Express
with help of that json method.
And now with that, if we reload again,
and we add a brand new product here quickly
for testing this,
just take any image and enter some dummy data,
if I do that and I click Delete here,
now it succeeds without an error message.
And if I reload, this still works.
And one last time, if I try this again
and check if I got any error messages here in the Console,
if I click Delete,
no, that looks good.
And that is how we can send such a deletion request
with frontend JavaScript code
and by using this AJAX request functionality.