Jonas: Let's now continue working
on the recipe uploading feature.
And this time we will actually work on the model
which will be responsible for sending the recipe data
to the Forkify API.
So let's go to our model.
And then down here after the bookmarks
let's create a new function and export it,
called upload recipe, okay.
And this one will eventually make a request to the API.
And so therefore it's going to be an async function
and it will receive the data for a new recipe.
Now, the first task of this function
here will be to take the raw input data
and transform it into the same format as the data
that we also get out of the API.
So let's take a look at that.
Now, it reloaded the page, of course.
So basically the format of the data that we receive
from the API looks a little bit like this.
Now, it has some different property names here
but what really matters
here is that the ingredients are nicely stored
in this array.
So there is an ingredients array
which contains a bunch of objects
where each contains quantity, unit, and description.
However, the data that we get into the function
right now simply has these three
or actually these six ingredient properties.
So ingredient one, two, three, four, five, and six.
And also it has each of these ingredients
here separated with these commas, all in one string.
And so now we need to basically put all of this data
here into one array and also separate each of these strings
into the quantity, unit, and description.
Now, of course, here in the form I could have also created
three different fields for each ingredient.
So one input field for the quantity,
the unit, and to description.
But that was a little bit too much work
for this learning project
and so I decided that I will simply give this hint
that the format needs to be quantity, unit, description.
And so that's what we have here.
So half a kilo, so kilo is to unit,
and then rice is to description.
Then here we need one avocado but there is no unit
and so we leave that one empty.
And then here we have just salt,
so without any quantity and any unit.
And so let's now take this data.
So these six ingredients and then take the data
out of there and put them into an object.
So this is how we will do it.
So we want to create an array of ingredients.
And so for that,
a good idea is to use the map method, right?
So map is always good to create new arrays
based on some existing data.
So in order to use map, we need an array.
And so let's convert the object
that we get here back to an array.
So object.entries,
and as I mentioned, this is basically the opposite
of object.fromEntries,
but it still makes sense to convert it to an object
here because we will also need the rest of the data
here as an object.
So new recipe,
and then we want to filter this array
because remember we only want the properties
that are called ingredient one, two,
three, all the way until six.
So in this array that we have right now,
each element is an entry, right?
And once again, let me actually show that to you.
So without seeing the data
I think that it becomes very difficult to follow this.
So I will console.log this data
here and now of course, we need to call this function,
upload recipe in the controller.
So let's not do this,
but let's actually call model.uploadRecipe
with the new recipe.
Now, okay, let's click here.
And so again, that is our array that we will work with now.
And so remember right now we are filtering this array.
And so what we want is basically only the entries
where the first element of the array starts with ingredient
like this, right?
And then also the second part should not be empty.
So in this case, we are of course not interested
in four, five, six.
Okay, so here add this condition
that we need for the filter.
We want to say, just as I explained just a second ago.
So the first element of the current entry should start
with ingredient and the second one should exist,
so it should not be an empty string.
So entry one,
so the second element should not be an empty string, okay.
And so this should now filter out only the data
that we are interested in.
So let's log at to the console and not this anymore.
And yeah, so that is what we were looking for.
And now all we need to do is to basically take the data
out of the string here and then put that into an object.
And so for that, we now finally use the map method.
And so here we already have some ingredients.
And so now we will take each ingredient
and split it by the comma,
so that's the separator that I specified.
And before we do that,
let's actually make sure that we remove
any white space here.
So we can simply do .replace and I believe that right now
the replace all method is actually already available.
So remember in the video where we talked about strings
I mentioned that replace all
will soon be available in browsers.
And I believe that right now it already is.
And so let's use that here.
So let's replace all the basically spaces
with an empty string.
And so this should then return an array of three elements,
and so let's not destruct that array.
So the first element is the quantity,
the second one is the unit,
and the third one is the description.
And now, let's return an object with this.
So with quantity, unit, and description.
We will still have to do some things
here but for now let's take a look at what we get.
And ingredient.replaceAll is not a function.
So that's because each ingredient
here is actually still an array,
so just like we had it here before in the entry.
So the first element of entry here remember was the key
and the second one was the value itself,
so here we need to use this on ingredient one.
So the second element of the entry.
So let's see now, and here we go.
So we have quantity, unit, and description
nicely inside of one array.
And so that's now kind of looks like this, great.
Now, there is just one thing that we need to notice here,
which is that when the quantity does not exist
then we want it to be no.
And otherwise it's actually a number.
So currently we still have strings
here but we want us to be a number now,
and so let's go ahead and convert that.
So let's come here.
So the quantity should be, so if there is a quantity,
so let's use the ternary operator.
So if there is a quantity, then convert that to a number.
All right, and if not, which is the situation
in which it is an empty string, then we want to return null.
So that should fix that, and indeed it does.
But now, let's see what happens
when we simply put something else here,
let's say just a number.
So let's see.
Well, then here we get a bunch of undefined
and so that we don't want,
so our format
here should always contain quantity, unit, description.
So in this case, what we would need to do is this.
So this is what we would expect.
That's so then we get here the empty strings as expected.
So basically what this means is that when we split
this string here, we wanted to have three parts, all right?
And so let's actually convert that to a code.
So here we want to now put
this here into a separate variable.
So let's call it ingredients array,
and then that is the array that we destructure.
But before we do that we can test
if this array actually has the length of three.
So if ingArray.length is different from three
then we want to create a new error.
So throw new error,
and then let's say wrong ingredient format,
please use the correct format.
And so then at this point here,
the function will immediately exit.
Now, then what do we want to actually happen
with this error?
Well, we will want to render an error message to this view.
So to the add recipe view, right?
And that is very simple.
So here we can use all try catch
just like we have been doing before.
So try and so if some error occurs in this function here,
then it goes to the catch block as always.
So here we get the error and then let's log that error
to the console again, with some emojis here to make it easy,
to understand that it is our own error.
And then we can simply say, addRecipeView.renderError.
And now we can actually pass in the error message
ourselves because here in the view
we accounted for that ability.
So by default the error message
is coming from this .errorMessage
but we can also specify our own.
And so that is what we will do now
because this way we are able to create the error message
basically here where the error actually occurred.
Okay, so error.message, and so now let's see what happens.
So for now, let's use the correct format
just to see if everything keeps working,
and it does.
But now, let's do it wrong
and we get wrong ingredient format
but it is still coming from the model.
So here we got uncaught in promise
and that actually makes sense,
because remember that here we are in
an async function already.
So this returns a promise
and so basically this then rejects the promise.
And so therefore we should handle that already right here.
So try like this and then catch the error
and simply throw that error again.
Okay, let's try that again.
Okay, and that still doesn't look
the way we want it to look.
So we wanted here to be an error message,
and so that should come from the controller.
And so that means that there is something wrong
in the controller.
And I guess I know why that happens
because remember that this is an async function right here.
So it returns a promise but then right
here we are actually not awaiting that promise.
So we need to use await
here and then also make this an async function.
And so only with this,
we are now actually handling this function
here as a function that returns a promise
so that the rejected promise can then actually get caught
here in this catch block.
So let's try once again here with some number.
Ah, and now it works.
So now, we get the actual error with these emoji mark
and also the error that we want here, okay, great.
So we have that part working of the ingredients.
Now, it's time to actually create the object
that is ready to be uploaded.
So let's create a recipe object here.
And so basically this object will be the opposite
of this one here.
So here we take in the source URL formatted as this,
and we want to send the data where the URL looks like this.
So where the property has this format,
it's the same here for the URL and to cooking time actually,
because that is exactly the format
that the API is ready to receive.
So it has to be exactly the way that we received it.
Okay, so let's write that object very quick.
So that is at newRecipe.title
then the source URL is that newRecipe.sourceUrl, like this.
Then the image URL is newRecipe.image, right?
Then the publisher is actually the same,
newRecipe.publisher.
The cooking time, newRecipe.cookingTime.
So fortunately, VS Code is giving us
already the correct suggestions, .servings.
And so that's all I think.
And so all we have to do now is to add the ingredients
array, and that's it.
And then we also have two numbers
here which we should convert to numbers
using this plus trick, all right.
And so now, let's actually log this one to the console
and not just the ingredients.
Oh, and of course, all this should be happening
inside of this try block, right.
So let's see, okay.
That looks great.
And it looks ready to be actually sent to our API, okay.
So let's do that.
So remember we already have a method for getting JSON.
So that's this one and now let's create a method
for sending JSON here in our helper.
And actually it's going to be very similar.
So for now, let's just copy it,
but then a bit later we will simply refactor this.
So here let's call it send JSON, all right.
And so now, let's learn how we can actually send data
using the fetch function.
So up until this point,
all we ever did was to simply pass in a URL
into the fetch function
and that would then automatically create a get request.
However, to send data,
remember how we discussed
before that that needs to be a post request.
And so here besides passing in the URL
we also need to pass in an object of some options, okay.
And the first option is going to be the method.
And so that method is now post.
Then we also need to pass in an object of headers,
and headers are basically some snippets of text,
which are like information about the request itself.
And many of them are like standard headers.
And one that we need to define is the Content Type.
So Content Type with an uppercase C and a T,
so make sure you get that right.
And then here application/JSON,
and so with this we tell the API.
So we specify in the request
that the data that we're gonna send
is going to be in the JSON format.
And so only then our API can correctly accept that data
and create a new recipe in the database.
And now, finally the payload of the request.
So basically the data that we want to send,
which is called the body.
So the body should be in JSON.
Remember as I just explained,
and so we can once again use JSON.stringify
and then convert the data that we want to send.
Now, and here we actually need to accept that data
here into the function, of course, as a second parameter.
So let's call that upload data.
And so that is the data that we then convert to JSON
so that it can be sent, and that's actually it.
So that's all we need to do.
So we still pass in a URL
which we will need to specify next.
And then we take that URL and the upload data,
and then simply create this fetch request like this, okay.
And then everything else is actually going to be the same.
So we will still have this request a racing about a timeout
function so that it can't run forever.
And we will also await any data
coming back because this API that we're gonna use
so the Forkify API will actually return the data
back that we just sent.
And you will see in a moment
why that is very important, okay.
So again, everything here is going to be the same.
And so a bit later we will then refactor these two
here into the same function, but let's do that later.
So here we now need to import the send JSON function.
And so now, let's use our newly created function
to create the Ajax request.
So send JSON and now the URL of the API,
and actually we have that here stored somewhere,
so that's this URL that we have in the config, right?
So slash recipes,
but that's actually not all as we will see in a second.
All right, let's get rid of this.
And now, let's go to the Forkify documentation page.
So at this URL, because now we actually need the API key.
So please click here now on this button,
which will then generate your unique developer key.
Now, if you got some kind of error
here while generating your key
then maybe it is because you requested more than one
API keys per hour.
So remember that you should only ever request one API key.
That's why I have this limit here in place
so that no one can abuse my API basically.
Now, if you did actually only try it once
and you still got an error,
then you can always contact me on Twitter, okay.
So here's the link to my Twitter page
and then just tweet the error at me
and I will try to help you.
Now, my key disappeared here,
so let's click again and now I will copy
it so that I can use it in my code.
So let's go down here or actually it's right here.
So you'll see the API path is actually exactly the same
as for getting all the recipes.
Now, the only difference is that of course,
we will not a search for anything now
but we will have to add our key like this,
so with this key parameter, okay.
And so yeah, we are doing a post request now, right.
And so that creates a new recipe
then when we hit this end point,
so this path with a post request and some data, okay.
Now, let's store actually this key in the config file.
So as a string, okay.
And then let's use that key here.
So again, the question mark to specify a list of parameters
and then key equals,
and then we have to import it up here
and then the key, okay.
And remember that this will actually send the recipe
then back to us.
And so let's store that as data and also await it.
And then finally, let's log that data to the console.
So console.logData, and now let's see what happens.
So we got a lot of errors here,
so it seems like the data is completely wrong.
And that's simply because I just forgot
to actually put that data here.
So the recipe, so of course send JSON has two parameters.
So not only the URL but also the data.
Let's try that again.
And it kind of worked
already because this error that we got back
here is actually already an error coming from our API.
So tells us that the input data is invalid
because the source URL
needs to be at least five characters long.
So let's try that again.
And actually we need to reload for now.
So the URL, let's just add something here
and probably all of them need this.
So let's do that quickly here in the HTML.
So I say, default value kind of,
so you see now we have test 23.
And as we upload it now then that worked.
So we get a status success back from our server
and then our data, right.
So that data contains everything that we just send
it plus the created at and also an ID.
And of course also your key
and that key will be very important in the next video
when we then want to mark our own recipes,
basically as our own.
Now, it will make sense to actually render
this newly created recipe also then to the user interface
after closing this display here,
so after closing this window, right.
And so therefore we need to convert
that to the format that we use in our application, right.
So basically in our application
we are using these different property names, right.
So we are in the same situation
as we are here in load recipe
where we get the data in the original format.
So with this kind of property names,
and we now need to convert it back to this one
so that all application understands the data.
All right, now, instead of writing this entire code
here twice, we can simply grab
this and put it into a new function.
So let's do that right here and call that one,
create recipe object,
and it simply receives some data.
And then it returns an object like this.
So return this soda then we can do whatever we want with it.
And in this case,
that's going to be to put that in the state.
So state.recipe will be create recipe object
with the data that we receive.
So basically just some nice refactoring.
So this now does exactly what we had before,
but now we can use that down here as well.
So we get the data
and now we want to then store that into the state.
So again, state.recipe equals create recipe object
with the data that we just received.
And now, let's actually come here to the controller
and log that stated recipe to the console,
so that's model.state.recipe.
Okay, so let's see.
And yeah, it now looks exactly the same as this one, right?
So beautiful.
There's just one thing missing,
which is this bookmarked here and also the key
is missing as well.
So let's take care of the bookmarking
and also of preserving our API key in this object, okay.
And a quick look at our flow chart
here will reveal that all we want to do is basically store
the bookmarks to local storage, right.
And also mark, of course, that recipe as a bookmark.
Okay, so we already implemented most of this.
So this and this in the last lecture,
then this part and this part in this lecture,
but let's now also take care of bookmarking it,
but that is very easy
because we already have the function for that.
So that is of course, add bookmark.
So we already have our recipe.
And so we can simply call add bookmark with our recipe
and that will then take care of everything.
So add bookmark with state.recipe.
And finally, we also need to add our key to the object,
so to state.recipe.
And we could do that here manually,
but let's actually use a nice trick
to do that down here or up here
where the object is actually created, so right here.
Now, in the case of a load recipe,
not always there will be a key.
So most of the recipes of course, don't have any key.
And so we cannot simply do this.
So key recipe.key because again,
not all of them will have one.
So we only want to add the key if there actually exists one.
So let's use a nice trick to do that.
So we can use the end operator, which does short-circuiting.
So we can do a recipe.key and then this object, all right.
I will explain to you in a second what this does that.
And then we can spread this.
So that looks confusing.
So let me explain what is happening here.
So remember that the end operator short-circuits.
So if recipe.key is a faulty value,
so if it doesn't exist
well, then nothing happens here, right.
And so then destructuring here, well does basically nothing.
Now, if this actually is some value,
then the second part of the operator
is executed and returned.
And so in that case, it is this object
here basically that is going to be returned.
And so then this whole expression will become that object.
And so then we can spread that object
to basically put the values here.
And so that will then be the same
as if the values would be out here like this,
key recipe.key.
But again, only in case that the key actually does exist.
And so this is a very nice trick
to conditionally add properties to an object.
So keep this one in mind,
now it is a very handy trick sometimes.
Now, okay, and I think we should actually be done here now.
So you see uploading data is a lot of work,
but I think this is still a nice exercise and let's see.
Here is our key now.
And also the recipe is now bookmarked, great.
So just to finish this lecture very quickly,
let's go back to the controller
and now actually render this recipe in the recipe view.
So render recipe, which will be recipeView.render,
and then as always model.state.recipe.
But now with this,
we would not really be able to see that rendered recipe yet
because we still have that form open.
So let's now close that form.
So close form window.
And we can do that because there is already a method
for that here.
So it's this public toggle window method.
And so let's now use that, okay.
So and actually let's not do that immediately,
but only after some time so that we can first display
a nice success message.
So we can use a set timeout and an insight of that
callback function.
We can then call, addRecipeView.toggleWindow,
and here let's specify some time,
let's say two and a half seconds.
However, as you already know,
we should never use a magic number like this.
So let's create one more configuration variable here,
let's call this the modal close,
and then let's say seconds so that we know that this value
is actually a value in seconds.
So let's use two and a half.
And now, we need to actually import
this because I believe that right now
we are not yet importing this into the controller.
And indeed we're not.
So let's say that the named import from config.js.
And so then down here let's use that modal close seconds
and then times a thousand
to convert it to milliseconds, okay.
And before we do that let's also display a success message.
And for that we already have in our view,
the render message method, right.
Which will take this.message,
which in this case we don't have one yet,
but let's actually add one.
Recipe was successfully uploaded.
And so now it's very, very easy to do this.
All we have to do is addRecipeView.renderMessage.
And so once again,
now that we have all of these nice methods in place,
it's very easy to take data
and then render all kinds of things on the user interface.
For example, like this error, or the success message,
or of course the data itself.
So let's try it one more time.
And there is our success message and it also disappeared.
And great, here is our recipe.
And also with our nicely formatted ingredients.
So remember we put 0.5 kilos of rice.
And so of course, that was successfully converted to an half
And you see it's also already a bookmark.
So you see that it's right here.
Of course, there is no image
because that path that I specified
there is of course not a correct path
but that doesn't matter at all here.
And now, to really finish
let's now add just one final thing
which is to render a loading spinner
in the add recipe view,
before we actually start to upload the data.
So this will then show the user that something is happening.
And so one more time that is now really easy to do
because we already have all these methods in place.
Okay, let's slow down our network a little bit to slow 3D
so that we can actually see it happening.
Well, that didn't display the spinner for some reason there.
So let's see.
AddRecipeView.renderSpinner, looks pretty correct to me.
So let's just take out this piece
here so that we can actually inspect the code.
Now it's taking more time because we are,
so let's put it back online, add recipe.
Oh, and now, you see it actually showed the loading spinner.
So not sure what happened to error but now it is all good.
Okay, great.
So all we have left to do now is to then display this recipe
as our own,
basically by using the key
that we have now here in our object
and also do the same thing in the search.
So let's take maybe a quick break
and then come back to finally finish this application.