So, how should view and edit work
for the product?
Well, if I click it,
in the end I wanna load this form with all the details
for the chosen product filled in.
And then I want to be able to save this,
to save the updated product
that is actually all I would like to do here.
Now, to make this work, we need a new view of course,
we need a view for updating,
so I will copy the new-product.ejs file
and rename it to update-product.ejs file.
Now, of course in both ejs files,
we now have basically the same form.
And whilst we could have this duplication,
it's a bit more elegant to add a new include,
which we could name product-form.ejs
and we then grab that form from new-product
this entire form with all the elements in there
and we cut that and add that into product-form.
Here we now just need to make sure that the submitPath
actually is flexible,
because we'll have different submitPaths
for adding a new product or updating an existing product.
So, actually here I'll replace that with an ejs placeholder,
where I expect to get the submitPath let's say.
But now with that,
that form lifts in the product-form.ejs file.
And back in new-product, we can now include it,
we can include it by reaching out to includes
and then product-form.
And now we just have to pass in that second parameter value
where we set that submitPath,
which we just added to the template.
In case of adding a new product to admin/products
that's the path I had before.
But now we can copy that included element here
that included snippet and go to update-product.
This view we added a second ago
and there we now replaced this form with that include,
but the submiPath will be a different one
we'll change it in the second.
Add new product is also wrong,
here instead I wanna say Update Product
and also say Update product up here.
And then here for the submitPath of the form
if we are editing an existing product,
I would say that the path should be /admin/products/
and then the id of the product that's being edited.
So, therefore here I'll then concatenate
a new value to the string with a +,
so add something to that string
and that could be /product.id, the id of the product
which we will soon pass as a value into this template.
We're not doing that yet, but we'll soon do that.
And speaking of that that is something we should do now,
we should make sure that in our product model,
we got a method for finding a model by id,
a product by id I mean.
And that in the admin.controller,
we then use that method to find a specific product.
For that let's actually start in the routes files though
in the admin.routes, because there I now want a new route
that allows us to load this update page we just added.
So, this template we just worked on
and then a route that allows us
to handle the submission of the product data.
For this we need a get route for a loading that page
and the path here could be /products/ and then the id.
This should be a dynamic route,
because the id will be different for different products.
And you learned that you can set up dynamic routes
with the route parameters by adding colon
and then a placeholder name of your choice, for example id
or productId whatever you want, I'll go for id.
So, this will then be a different value
every time this route is visited.
It's the value that will be entered in the URL,
this here is just a placeholder and we will be able
to get the concrete value that was entered in the URL
with help of that placeholder in the controller action
that will be executed.
This get route isn't everything though,
I also want a post route for that same path,
because that is the route that will be visited
whenever that form is submitted that update form.
Now, we need the two controller actions for these new routes
and therefore, in admin.controller,
let's say at the bottom of the file,
though the position is up to you.
I'll add a getUpdateProduct function
and then also another function
which I'll name updateProduct that's the function
that will be triggered when the post request arrives.
Now, we should expose these two functions,
simply by adding them to this exported object.
And now before we add any logic to these functions,
we of course should go to the admin.routes file again
and point at these newly added actions.
So here for get,
I point at adminController.getUpdateProduct
and for the post route
it's adminController.updateProduct like this.
Now, we will get that id and therefore in our model,
in our product.model we can add a new method,
a static method actually, a static async method,
findById could be a fitting name,
because there I expect to get the productId
as a parameter value.
And then I'm wanna find a matching product
in the database by that id.
So, now in here I wanna use my database
and there of course, the products collection
and here we can find a specific entry with help of
the findOne method.
Now, findOne takes a parameter value,
which is an object that allows us to describe
the filtering criteria.
And here I wanna look for the product
where the _id in the database,
so the values stored under _id in the database
matches the id I'm getting here.
Now, the id I'm getting here will be a string
and then the database we have these ObjectId things.
So, to match with ObjectId here,
what we can do is we can actually import mongodb
by requiring mongodb here in the product.model.
And we can manually construct such an ObjectId, id
based on our string id, for this I'll get my prodId here
by calling new mongodb.ObjectId,
which is a constructor function
provided by the mongodb package.
We can pass our productId as a string to ObjectId
and we'll get back an ObjectId, id thereafter.
And then we can use this ObjectId here for this search
for finding one document.
And I will await this and eventually I will hopefully
get my product, which I then of course return.
Of course though that might fail,
I might not actually find a product with that id.
In that case I wanna handle this appropriately
and what I'll do in that case
is here I will check if (!product),
so if I didn't find a product
in that case I will actually not return it,
but I'll throw a custom error instead.
You can generate your own errors and we can then handle that
in the admin.controller.
What I'll do here is I'll create a new error object
with the built in error class
that's provided by JavaScript out of the box
and we can pass an error message in here,
like, "Could not find product with provided id."
And we can then throw this error like that.
We can even add some extra data to the error object,
just as we can add data to any object in JavaScript
by using the dot notation and adding a property
that doesn't exist yet.
For example, a code property which I add,
which I set to 404, because that's the default status code,
the default HTTP status code for not found.
You don't have to use it here,
because we're not sending back a HTTP response here.
Instead, I just set it here,
because I will then later use it
to send back a 404 response if we have this specific error
and to identify this specific error later,
I'll give it to this code.
So, we throw this error if we don't find a product
for the given id, otherwise we return product.
It's also worth mentioning that generating
this ObjectId can fail,
if we provide an invalid productId here.
And therefore actually what I do here
is I will also wrap this with try/catch here,
the generation of this productId and I'll catch this error
that I might get here.
And I convert productId or prodId here into a variable,
which can then be set in try, we have to do it like this,
because of scoping if I would create a constant here in try
it would only be available there,
doing it like this it's available outside of try as well.
And if we make it into this catch block,
which means generating the ObjectId failed,
then I will also throw this error again,
so that we can handle it into controller,
but I am handling it here at first time
so that I can also add my code property
to this error object here.
Because no matter if I didn't find a product
or if generating that id already failed,
it means that we are dealing with an id
that doesn't lead to data in the database.
And in that case,
I wanna set my 404 error so that I can later show
the appropriate error message.
With all of that we built
a little bit more complex findById method,
which we can now use to find a product by id.
And as long as it's a valid id for an existing product,
we also won't run into the errors
which we are generating in here.
So, now back in admin.controller in getUpdateProduct,
we get the req and the res and we can now add async here,
because we'll be dealing with promises.
Because I will use my Product
and then use the newly added findById method,
which is async, await and therefore returns a promise
to look for the product with req.params.id as an id.
And this is how we can extract the value
that was entered in the URL as a concrete value
for that placeholder we added in our route.
So, in the route we have :id here and that means
that we can get the value that
is then actually entered in the URL with that identifier
we chose here id in my case on req.params.
That's how express exposes to concrete value provided
for this dynamic parameter for this placeholder.
Well, here I get my product and of course
we have to await this and it could fail,
because connecting to the database failed
or because we have this custom error
on which we worked a couple of seconds ago, this 404 error.
Either way, we wanna catch this error
and then let express handle it by manually forwarding it
with next and returning thereafter.
Actually,
we don't need to return if there is no other code
after try/catch that could execute.
Because now I wanna continue here inside of this try block
if we did succeed here, if we did not face any errors,
then of course I wanna render this template
we added a couple of minutes ago.
This update-product template,
which is in admin/products/update-product.
This template should be rendered
and of course I'll pass my product
as a value into this template.
So, this product here is passed into this template
and it's passed into this template
under a key named product.
That means that now in that update-product.ejs file,
product will be available as a key, as a variable.
And we now got the full product data available
in there actually.
So, if we wanna pre-populate product-form with some data,
we can now do that.
We can now go to product-form and prefill all those inputs
with the data that was stored in the database before.
We do that with help of the value attribute
which we solved before
and then we can set the title
with product.title for example an ejs.
We don't set the image,
because I don't wanna pre-populate that we can't do that
it's a file on the server.
We can't pass that into the image picker,
which is served to our users that doesn't work,
so we got no default here.
But I will add it here to this summary
with product.summary.
And I will of course also do it for the price,
with product.price and also here for the description then
with product.description.
So, now all these fields will be pre-populated
with data that's coming from the database,
assuming that in update-product.ejs we do provide
this product into this product-form included snippet.
So, therefore in this object here,
which we pass to include where we set up the data
that's exposed to that included snippet.
I add a second key value up here,
which is product and which points
at the product I'm getting in this template in general.
We also have to do this in new-product.ejs,
because there I'm also including the product-form.
So therefore here, I also wanna set a product field
for this included snippet.
And here of course, if it's a new product,
we don't have any titles, summary and so on yet.
So, therefore here I will actually create a new object
and split this across multiple lines
to make it a bit easier to read.
Where I set title to an empty string,
summary to an empty string, price to an empty string
and the description to an empty string.
Because if it's a new product,
we of course have no starting values as a default.
With that if I go to add a product it's all empty,
but if I go to view and edit it's pre-populated.
Now, the description is not pre-populated,
because I made a tiny mistake there.
The description of course is a text area
and for a text area you don't set the value.
Instead you pass that default value
between the opening and closing tags.
Very important though,
you set this without any line breaks
that you might wanna add for readability,
because those line breaks would be included
in what you see on the screen.
The text area is a bit special when it comes to this,
it does not ignore line breaks and white space
as all the other elements do, instead it includes it.
So, here we don't wanna have
the extra line break therefore and if I don't do that,
now if I reload, I can see my data here as well.
So, that is working, but of course clicking save here
wouldn't do the trick yet.
We're not handling the submission
of updated product data yet.