Jonas: Welcome back.
So now that you know everything
about the model-view-controller architecture
and also how it's gonna be implemented in our project,
let's now actually go do that in practice.
And this is gonna be a long lecture,
so let's get started.
And let's start by creating the necessary new files
so that we can then split up our code between them.
So let's start by closing all of the folders here.
And then inside of src
and JavaScript, let's create a new file,
which will be called model.js.
And so this is basically going
to be the module in which we write our entire model.
Then we also need the view,
or actually, the views
because we will have multiple views.
Basically one for each feature.
So now we're working on displaying the recipe
and so this one is going to be called recipeView.
But actually, let's create a new folder here
for the views in order not to clutter our JavaScript folder.
All right?
And so then in here, let's create recipeView.js.
So we will have one big module here
for all the controllers.
Then one big file for the entire model.
So for all the models basically.
So for the recipe, for search, for bookmarks
and all of that but for the views,
we will have one module for each of the different views.
And you'll understand why that makes sense
as we keep developing our application.
And probably the first reason that comes to mind
is that the views are simply much bigger.
And so we don't want a file, which like 500 lines of code.
We could, of course, also split up the model
and the controller and probably many people would do that
but I don't want to have to mess with a lot
of files here in this project.
That would just make it very confusing for you to follow.
So let's actually take a look again here
at this architecture
that I showed you in the last video.
And here, we will actually start with the model.
So again, we're going to have a big state object,
which then inside will contain an object for recipe,
search and bookmarks.
And now we will just start with the recipe
and then there will be a function
for loading the recipe, right?
And so then that function will then be called
by controlRecipes, which kind of sits
between loading the recipe
and then rendering it using the view.
So remember that the controller kind of sits
between model and the view
so it acts like a bridge.
All right, and let's actually keep it open here
and close all of these others.
And so here in our model,
let's simply create that state object
and then in there we have the recipe,
which in turn, is also an empty object.
At least at the beginning.
And then we will export this state
so that we can then use it in the controller.
And let's put them here just like in the diagram.
Model, controller and view.
Okay?
So we're exporting the state from here
and now let's also create that loadRecipe function.
And of course, we also export it
so that we can use it in the controller.
So loadRecipe.
And this function will be the one responsible
for actually fetching the recipe data
from our Forkify API.
And so this is going to be an async function.
All right?
And so now we can actually start grabbing some
of the code from here.
Okay?
So as I said, basically the goal of this lecture
is to refactor all the code here
into the different parts of the architecture.
Okay?
So what here exactly is concerned
with basically getting the data?
So it starts here
when we fetch the recipe from the API
and then basically, it's all of this here.
So all of this.
So let's copy it.
So rendering the spinner, of course,
has nothing to do with the business logic
but really, it is only presentation logic
and so this one will eventually go
into the view.
And then here getting the ID,
this is also not business logic.
This is actually more about the application logic itself.
So about making the application work.
And so let's keep that part here.
So again, I will just copy this
or actually cut and put it here.
And now we start getting these errors but nevermind.
Probably, we should actually terminate our process here
with Control + C
so that we don't get errors all the time.
Okay?
Now we need to, of course, adjust this here a little bit.
So this first part here makes sense, right?
Now, we just need to get this ID from anywhere.
So right now, we have no idea
which ID we should actually fetch.
And so it's probably a good idea
to pass that ID into this function.
So the controller is the one
that will actually get that ID right here
and so then when it calls the model,
so the loadRecipe function,
it can pass that ID into it.
Then this part here can all stay the same
and this as well.
And this actually should now
not be the recipe but state.recipe.
And here we can just create a const from this.
And here let's also log state.recipe.
All right.
So this function here is not going to return anything.
All it will do is to change our state object.
So this big state object,
which will then contain the recipe
and into which the controller
will then grab and take the recipe out of there.
And this is going to work
because there is a live connection
between the exports and the imports.
Remember that?
So when this state object here
is going to get updated by loadRecipe,
then that state is also updated in the controller,
which imports the state.
So let's now actually import all of that into here.
So doing it here at the very top.
So import and then from the model,
we simply want to import everything.
So we have a couple of named exports here.
So again, there are named exports
and default exports
and these ones are named exports
because we explicitly gave them a name
and so here we can simply import everything
and then call it the model.
And so here in the controller,
we will then end up with model.state
and model.loadRecipe.
Remember that?
And then here, the current folder, model.js.
Okay.
So let's go back here to loading the recipe
and the spinner should probably be outside of here.
And so here, now let's actually call the function.
So let's call model.loadRecipe
and then pass in the ID.
And this loadRecipe function
is an async function and therefore,
it is going to return a promise.
Remember?
And so therefore, here we have to avoid that promise
before we can move on in the next step here
in the execution of this async function.
And so you see, this is exactly the situation
of one async function calling another async function
that we already studied in one of the previous sections.
Remember that?
And so again, it is really, really important
to keep in mind that an async function
will return a promise
that we then need to handle
whenever we call that async function.
At least, if we want to get some result out of it
or if we kind of want to stop the execution
in the function that is calling the other async function.
All right?
So again, really important to understand the mechanics
of what is happening here.
Now, here we should probably also have some error handling.
So I forgot that part.
So try
and all the way down here,
and then catch the error
and then alert the error as well.
All right.
So remember that this loadRecipe function here
does not return anything.
And so therefore, we are not storing any result
into any new variable.
Instead, here we will get access
to state.recipe.
So to that state.recipe
that is going to be manipulated right here.
So you see that this loadRecipe function here clearly
is not a pure function.
So it has the side effect
of manipulating this variable
that is outside of it.
And there would be different ways
of avoiding that but that is just a lot of work
and it's just not worth it here in this case.
We are still trying to learn here, remember.
But anyway, we now here have access
to model.state.recipe.
And so just in order to see
if everything still works,
let's store that recipe into the recipe variable
so that we can then render it,
using this code that we already have here.
So just temporarily, recipe
and then destructure it.
So let's give it a save
and let's restart our application here.
All right.
Let's reload here
and you see that it's working.
Right?
So everything we implemented so far was correct
and that's pretty important to keep checking
when we are refactoring code like this.
Okay?
So that was actually the easy part,
now comes the recipeView,
which will then contain basically all the rest
of the code that we have here.
So let's go here and set the view up.
So the view is going to be a class
and in this case called RecipeView.
And we do this because later,
we will also have a parent class called View,
which will contain a couple of methods
that all the views should inherit.
And so using classes makes all
of this very simple to implement.
Also, we want each view to have a couple of private methods
and properties and so classes makes this really easy
to implement again.
And just in general, I feel that classes
are really the right way to go here.
And again, it will make even more sense
once we start to add the parent class of view.
But anyway, I mentioned that I want each
of these classes to have a couple of private properties
and one of them is going to be the parentElement.
So parentElement, and let's right away set the parentElement
to the recipe container.
So basically, to this element, all right?
Because this will then make it really easy
to render the spinner
and to render success or error messages
or to render, of course, the recipe itself.
So if each of the views has this parentElement property,
then it will be really easy to do all of those tasks
and again, that will become really clear
once we start adding more and more views.
But now the next thing I want to do
is to actually export something from this module.
So of course, we have to export something
from this view so that the controller
can then use it.
Now, what are we going to export from this view?
So we might export, of course,
the entire class like this
and so then here in the controller,
we would have to import that class
and create a new object out of that class.
So basically, create a new RecipeView object, right?
However, in that situation,
it might be possible
to create more that one view
and we would never want that.
Also, that would add unnecessary work
to the controller, which we basically want
to keep as simple as possible.
And so in order to avoid all that,
we will already create the object here
and then export that object.
And so like this, no one from the outside
of this class here will have access to anything,
except for the object.
So what we will do is to export default
and then a new RecipeView.
All right?
And that's actually it.
So we don't pass any data in
and so therefore, we don't need any constructor even.
Okay?
And then here all we do is import
and here we then can give it any name.
So I'm calling it recipeView
because I think that's what makes most sense.
And then views and then recipeView.js.
Now, how do we then actually pass any data
into the recipeView?
Because if we're not creating the new object ourselves,
then we cannot pass any data
in like for the constructor, right?
So we cannot pass any data into this RecipeView,
well, because we're creating that object right here
in the RecipeView module already.
But actually, I did that on purpose
so that we can create a very nice method called render.
So what I will want to do here,
and let me already write it here
even before having the method.
So what I will want to do is to call recipeView.render.
And then here all we do is to pass in the data,
which will be model.state.recipe.
And this method here called render
is a very common name for methods,
for example, in React, it is also called render.
And so I think this is a nice name.
It's really descriptive of what is going to happen.
All right?
So if we did export the entire class,
then here we would have to do like const recipeView
equal new RecipeView or something like that.
And then here, we would have to pass in the data like this
when we create the new recipe.
And of course, this would also be possible
but honestly, I like it better like this.
So having this nice render method
is a lot more descriptive
and a lot cleaner and also for the reasons
that I explained previously already.
Now, anyway, this render method here
will now accept this data.
It will then store it into the object.
So let's add the public render method.
So this is, of course, part of the public API.
So this receives data
and will then set this.#data
to the data it just received.
So we need to declare that up here.
And so this render method here
and these two properties
are something that all the views
are going to have in common.
And this will be really beautiful to work with
as you will see.
But now, let's actually do something with this data,
which is to actually render it.
So basically what we want now
is to take all of this code
and cut it from here.
And put it into our view.
Okay?
Now, we will actually not put it into the render method
because again, this method will later be common
to all the views.
So to all the classes.
However, each view will, of course, render different HTML.
And so we will simply have a method
that generates that HTML
so that the render method can then render that HTML.
So if that sounds confusing, let me write it in code.
So generateMarkup.
And so this is going to be a private method
and since we're using Babel here,
I can already use this syntax here.
So pasting the code here
and so this is going to generate this Markup variable
and we can actually immediately return that from here.
Okay?
Now, this is not going to do anything yet
because what is recipe here in this case?
It's nothing, right?
It's not defined at all.
Now, in this case, where is this data actually?
Well, it is in this.#data, right?
So why's that?
So let's take a look at our controller
and let's start from the beginning.
So the recipe is loaded here
and then that will store it into the state object, right?
Here we don't need that anymore.
Then here we take model.state.recipe,
which is that data that we just received here in step one
and then that data is passed into the render method, right?
And so render method then takes that data
and stores it inside of this.#data.
And so this is so that we can then use
that data all over the place
inside of this object.
And by the way, this is exactly what we have here
in this architecture diagram.
So inside of controlRecipes,
the loadRecipe function is called,
then the recipe data foes into the state
and then that data passes
right through the controller basically
and goes into the render method,
which then in turn, calls generateMarkup.
So you see, we already have many things
here implemented here already,
like data and parentElement,
we're just missing some small parts
but we are going to get there.
But now what we were doing here
is to discuss where this recipe data
is actually located right now.
And we came to the conclusion that it is in this.#data.
So I will copy it
and then we need to replace all the recipe. here with that.
So I'm hitting Command + D.
So for you, that should be Control + D.
And then let me paste that dot
and then that's it.
All right?
Then down here, we have this code,
which is not supposed to be here
because all this function does
is to return an HTML string.
And it's going to be the render method
that will be responsible for then actually putting
that HTML onto the page.
Now, okay.
So here, let's say that the markup
is this.#generateMarkup.
Well, this is probably something that's coming
from Babel here.
Not really sure why.
But apparently, VS Code doesn't like this yet.
Ah, we're still missing the dot here, of course.
Now it appears to be working.
And of course, we've still got some errors here
but nevermind about that.
We are still refactoring.
So we have our markup.
Now we need to put it on the page.
So again, we need to start by clearing the parentElement
and actually, let's create another small method for that.
So just to get in the habit of abstracting some code
and so now this is where our parentElement
will come into play
'cause now we can say
this.#parentElement.innerHTML
and set it to empty.
Right?
And so this method here
will be usable for all the views
as long as all the views
have a parentElement property like this one.
Right?
So all of this will become really reusable
as you will in one of the future lectures.
So here we now can then call that.
And then to finish,
we can finally render that HTML to the page.
And so here, once again,
we can now use this.#parentElement
and then insert that HTML there.
Okay.
So this might already be working actually.
So all of this here looks good to me.
Let's take a look at the controller.
And you see that this is now a really nice
and small function here already, right?
Now, this one, we actually want to call it controlRecipes.
So let's replace it everywhere.
controlRecipes.
So that's the name that I have here in the diagram.
Okay?
And so now what we're missing
is only to export the renderSpinner also into the view.
So this one, of course,
also has nothing to do with the controller
and so it belongs in the view.
So of course, not a function
but a method and this one will also be a public method
so that the controller can then call this method here
as it starts fetching the data.
Okay?
And now here again,
we have the beauty of the parentElement already being
inside of the object.
So this one we can simply delete
and here we can replace it with this.#parentElement.
Now, right.
Next up, I'm seeing that this icons variable
is not going to be defined anywhere in this module.
So we will need to grab that from here.
So you'll see that here we have the icons
and we don't need them here anymore at least.
So now we need them right here.
And now what we need to do
is to then call this renderSpinner method right here.
So that's going to be recipeView.renderSpinner
and here again, we no longer need to pass
in the parentElement.
And just watch how beautiful this is.
So we have recipeView and then renderSpinner
and we already know
that this will then automatically render the spinner
on the recipeView.
And the same is later going to happen with other views.
For example, with the bookmarks view.
We will also have a renderSpinner on the bookmarks view
and it's going to work the exact same way.
And the same with render.
So all we have to do is call these same methods then
on all the views
and they will then simply act on whatever view
that we are calling them, right?
And that works so nice
because we have this parentElement here
and also this data property.
So I think that this is a really nice architecture
and so let's now actually see if this works.
So let's reload this here.
And here we have some errors.
So apparently, if there is a really big error,
then Parcel will let us know about that.
And in this case, the problem is that it failed
to resolve this URL.
And that actually makes sense
because this view here is in a different folder.
Right?
So here we're only going one folder up
and going up one folder from here
will end up in the JavaScript folder.
However, we want to go into images.
So we need to go up one more level.
So like this.
Okay?
So here we have apparently some bug.
Let's just see if anything happens still on this event
and it does.
There's still some problem here maybe
that has to do with the fact
that we are using these private methods here.
So let me change this here
to this one.
So right here, yeah.
And let's also comment out this one
and then we're not using any private methods anymore.
However, that is apparently still not working.
All right.
So let's take a look here at our this.#data.
So this.#data just to see what we get here.
And so we see that apparently
that method is not even being called.
Ah, and I see the problem.
I actually did not call it.
So I didn't use the parenthesis, right?
So what a stupid mistake.
So let's roll back on all the changes I did,
except, of course, for this one
but here, probably all I need to do
is to call these two methods.
Ah, beautiful.
So we're back.
It's working now again.
And it's working really nice,
including the spinner.
Okay?
And so I think with this,
we successfully refactored our entire code
to this new architecture.
Well, not the entire code actually.
We're still missing this handler here
and all of this part that is here.
But don't worry about that for now, okay?
We will have a separate lecture,
which will talk about how we can handle events
in this particular architecture.
For now what matters
is that this works just nicely.
There's just one thing that I actually want to change
but that has nothing to do with the architecture.
So what I want to change is these numbers.
So do you see here we have 0.5
but in the real world, you will see this more like this.
So like 1/2 tablespoon of brown sugar
instead of 0.5.
So this looks a lot nicer or this, for example,
1 1/2 is a lot nicer than 1.5.
And so for that,
we are actually going to use an external library,
which will very easily do this for us.
Okay?
And so I included this part so I could show you
just how to do this.
So let me just google it here.
Npm fractional
just so we can see what this package looks like.
So if you google npm,
then you will usually end up on npmjs.com
and this will then contain like one page
for each package that is on npm, all right?
And so here what you see
is that you can basically create new fractions based
on numbers and then you can do all kinds of multiplications
with them, like multiplying, dividing
and in the end, converting them to a string.
And so that's what we will do now.
So we will convert each number to a fraction
and then immediately convert them to a string.
So this is how we install it.
Let's just grab that.
And then we need our terminal back.
Let's add another one.
So that's the plus sign.
And this should be pretty fast hopefully.
And so we need that here
and so let's import
fractional from
and then fractional.
And so here again you see
that any libraries or any packages
that we import from npm,
we don't even have to specify any path.
All we have to do is to write their names here
and then here, we need also what they export.
And let me see what they actually export.
So it should be called fraction, okay?
So this library actually exports something called fraction.
And we can see that because here,
it is importing the fractional library using require.
And that is simply the old CommonJS way of importing,
which I showed you earlier.
And so that is one of the reasons
why I show that to you
because yeah, again you will see this all the time
in the wild still
because so many packages on npm
are still using CommonJS.
So let's import Fraction here.
Actually like this.
Okay?
And let's just see if this actually does exist now.
And yeah, it does.
So actually, that's even Fraction inside of Fraction
but anyway, let's now go ahead and use this actually
so that it's in here.
So here we now no longer simply want the quantity
but we want the quantity converted
to a fraction string.
So according to the documentation,
we have to say new Fraction
and in this case, it's actually Fraction.Fraction.
So again that's because here inside of Fraction
is where the Fraction function is actually located.
Okay?
And here you could see that we have to then use new.
Now, actually to make this a bit nicer,
we can also use destructuring here right away.
So we can do this.
And if we now take a look,
well, now we have this bug here.
But nevermind.
So let's just do this.
And then call toString on that
just like it said in the documentation.
So you see that now as I destructured that fraction,
it became the actual function.
And yeah, here you see beautifully that it is now
in this size or in this format of the fraction.
And if I take a look at the other one,
then the same as well.
Now, this one doesn't look very beautiful but nevermind.
So we cannot fix all of it
but at least, this looks really cool now, right?
And so this is when we reach for packages on npm.
So when we need some functionality
that we don't want to implement ourselves.
I'm just going back here
because I noticed some problem here,
which was this one.
And that was because previously here we had null.
So there was no value for this cilantro.
And so we should probably check if the number does exist.
So we can say ing.quantity.
If it exists, then do this.
And if not, then simply put an empty string there
and of course, now here.
So that must be out here.
Yep, now it's gone and it looks a lot nicer.
All right.
Now just to finish,
I actually want to refactor this function here.
So let me grab this
and put it down here and call it generateMarkupIngredients
or just Ingredient.
Okay.
And this will then receive the ingredient.
Okay.
And then here inside of the map,
we can then simply call this.
And let's copy it because these private methods,
for some reason, VS Code doesn't really understand them yet.
So right here.
And so successfully refactored this part
and so we are now done in this lecture
where we also successfully refactored the entire code
that we already had
to the model-view-controller architecture.
All right?
So we're not done 100%
but we've done a lot of work in this lecture
and I really hope that you understood everything
that we did here.
So this was a really, really, really important lecture.
So make sure that you understand everything that we did.
So make sure to review all the code
and also compare it here
with this diagram one more time
so that you see which functions we call when
and then also, how exactly the data flows
between these different functions.
And especially here in this view
because I know that this can get a little bit confusing
and so reviewing this
is gonna be a very good idea,
especially because we will use this so many times also
in the other features.
And so by then, you really must understand
how all of this fits together
because otherwise, it's gonna be very hard
to follow the rest of the project.
Okay, so take your time
and only then move on to the next video.