Creating a New Feature in a Full-Stack Recipe Sharing App
Adding the infrastructure necessary to create a new page to view recipes and browse a list of recipes. Potluck dev diaries, part 2
This is an ongoing tutorial series and development diary about full-stack software development
Last time, I left off after building a little bit of the frontend client infrastructure in Potluck. So far, we have a home page and a couple of modules, but not really much in them.
As always, the code for this project can be found in the GitHub repo.
Today, I want to go ahead and start building out a new feature. This feature will allow users to scroll through recipes and view a single recipe. Here’s what we’ll be setting up today:
Building a new module and registering it in the App Module
Routing to our new feature and setting up routing between components
Creating a Recipe Model that will serve as a data interface
Setting up a Recipe Service, filling in some dummy data and setting up a container-presentation pattern of design
Getting the data in the new components
Like building a house, you need a foundation. Our foundation in this case are a new module, a service, and routing. Having these in place sooner than later will save us a lot of pain in the future.
Building the Recipe Module
The Module
We’re gonna do this just like we did last week.
ng generate module recipes --routing // or just ng g m
This gives us a new module, and a routing module with it. Let’s go ahead and build the two components so we have something to route to.
ng generate component recipes/recipe-list -m=recipes
ng generate component recipes/view-recipe -m=recipes
Routing
Now that I have the two components, I route to them and register them in the App Routing Component.
//app-routing.module.ts
...
{
path: 'recipes',
loadChildren: () => import('./recipes/recipes.module')
.then(m => m.RecipesModule)
}
...
//recipes-routing.module.ts
const routes: Routes = [
{
path: '',
component: RecipeListComponent
},
{
path: ':recipe-url',
component: ViewRecipeComponent
}
];
Let’s break this down.
In the app routing module, I make the path ‘recipes’. This means that if I go to to localhost:4200/recipes
, it will load the Recipe Module. The first path I have listed is just an empty string. Essentially, you can think of the path being ‘/’ + ‘recipe’ + ''
. When you get to that route, it leads you straight to the Recipe List Component, as we see here:
Let’s break it down further.
{
path: ':recipe-url',
component: ViewRecipeComponent
}
The :recipe-url
portion is a unique identifier for each recipe. When the route goes there, it will look at our existing recipes and load the recipe with that url. The View Recipe Component will open up, and it will show the user the recipe associated with that unique identifier. Hopefully this will make more sense a lot later in this post.
Eventually, our API will take in the recipe url as a parameter, and serve that recipe back to the user.
Even though there are no recipes associated with any urls, you can type recipes/anything
and it will display the View Recipe Component.
A Recipe Model
Our model is going to be a simple one at first. We will be adding things to it as they come to me, but the good part is that as time goes on, that won’t be an issue. While it’s true it should try to match what’s going into our database (and that I should probably create the data infrastructure first, yet here we are), we should keep it flexible enough that in case the shape of the data changes, there won’t be an issue.
A recipe needs these things for our app:
Title - I hope this is self-explanatory
Description - A short quip to get people to click on the recipe
Ingredients - What the recipe needs in order to be made
Steps - The steps the cook should take
Tags - A list of tags that will help users search for a new recipe
Photo URL - We’ll start by getting these from Unsplash, but eventually the user will be able to upload these
I’ll begin with an interface instead to keep things lighter on the frontend and not have to worry about instantiation and such. Eventually, I want to add comments, ratings and more, but that’s so much farther down the line than I want to think about.
I went ahead and created an ingredient model too and put them in a new folder.
The recipes and ingredients share what’s known as a many-to-one (n-1) relationship. On the front end, we don’t really care about the database’s implementation, and we know nothing about the shape of the data tables here. All we know is that we want to get the information from the server which looks exactly like this, even if there are more columns in the database table than just these.
export interface Recipe {
RecipeId: number
Title: string;
Description: string;
Intro: string;
Ingredients: Ingredient[];
Steps: string[];
Tags: string[];
RecipeUrl: string;
PhotoUrl?: string | null;
}
---
export interface Ingredient {
IngredientId: number;
Name: string;
Quantity: number;
Unit: string;
}
Each ingredient and recipe comes with a unique ID. Note the question mark and “null” pipe on the PhotoUrl - that’s because uploading a photo is optional.
Recipe Service
In an Angular application, a service can serve as a single source of truth. With nested components, I like to pass the information from the parent to the child and send events up to the parent, in turn only updating the service from the parent. This pattern of design is known as the Container-Presentation pattern.
In the diagram above, the parent component makes all the service calls, and distributes that data accordingly when anything is updated.
Child components should be simple. They should have no idea what the implementation is. Their purpose is to display data and raise events. It is a hallmark of the separation of concerns.
Creating the Service
Like generating a module or component, you can build a service from the Angular CLI.
ng generate service recipes/recipe
We’re left with a blank service and a test file (don’t worry, I’m going to go over unit testing soon). I want to create a stub file for some mock data. It’ll help us fill out the components. The recipes folder now looks like this.
The stub file contains an array of recipes and arrays of ingredients for those recipes.
export const CHANA_MASALA_INGREDIENTS: Ingredient[] = [];
export const CEREAL_INGREDIENTS: Ingredient[] = [];
export const SMOOTHIE_INGREDIENTS: Ingredient[] = [];
export const RECIPES: Recipe[] = [];
I fill these four constants out accordingly, and we can move on to the service. I encourage you to view the data in the full GitHub repo if you want to see what I filled these out with.
Adding Methods
GetRecipes
I can add a getRecipes
method to the service. I’m going to be using RxJS Observables and faking a time delay from the server. The time delay isn’t necessary, but it kind of demonstrates how the application would behave in real life.
// recipe.service.ts
public getRecipes(): Observable<Recipe[]> {
return of(RECIPES).pipe(
delay(2000)
)
}
Why are we returning an Observable?
An Observable is essentially a datatype which promises the consuming component that data is coming. We Subscribe to Observables to get data from them. To control the flow of information in our app, we use Observables and Subscribers to make sure all of the components are up-to-date with the latest data every time something is updated.
Imagine a local newspaper delivery route. If the newspaper company is the Observable, then you are the Subscriber. If you subscribe to the newspaper, you’ll get a paper at your doorstep every week.
return of(RECIPES).pipe(delay(2000))
Here, ‘of’ is a shortening of Observable. In this case, we want the Observable to broadcast the RECIPES
constant created earlier. The pipe()
operator is something that sits between an Observable and Subscriber. It can transform the data before reaching the subscription. In this case, I added a 2000-ms delay between the page loading and the data displaying.
GetRecipeByUrl
What if a user wants to view a single recipe in detail?
public getRecipeByUrl(url: string): Observable<Recipe | null> {
const recipe: Recipe | undefined = RECIPES.find(r => r.RecipeUrl === url);
return of(recipe || null).pipe(delay(2000));
}
Eventually, the server is going to do the finding for us. In the mean time, we have to simulate it on the fronend. The Array.prototype.find
method doesn’t always return a value if it doesn’t exist. In that case, we need to return null instead. We can handle that here by displaying a message to the user that the recipe doesn’t exist.
Coupling the Service and the Component
Finally, let’s get this data in the components!
We’re going to worry about layout and styling later. For now, I want to keep it short and sweet. I’ll be using Bootstrap components for now.
Recipe List
The first step is to fetch the recipes from the service. We’ll need to inject the recipe service in the constructor, call the getRecipes method, and set the recipes variable in the component equal to what comes back from the service. We also need to unsubscribe when we leave the page to prevent memory leaks.
public recipes: Recipe[] = [];
public doneLoading: boolean = false;
private allRecipesSubscription$: Subscription = new Subscription();
constructor(private recipeService: RecipeService) { }
ngOnInit(): void {
this.allRecipesSubscription$ = this.recipeService.getRecipes()
.pipe(finalize(() => this.doneLoading = true))
.subscribe({
next: recipes => this.recipes = recipes,
error: () => console.error("There was an error fetching the recipes")
})
}
ngOnDestroy(): void {
this.allRecipesSubscription$.unsubscribe();
}
The next
call in the Subscription is what comes back if the call was successful. This happens as soon as the page loads. error
will fire if there was an issue. For now, I’ve posted an error to the development console. I’ll be adding more to this as time goes on.
The finalize
pipe sets the loading state to done when we’ve received all the data.
The allRecipesSubscription$
variable stores our subscription and prevents memory leaks when the user leaves the page. This is an important step!
In the HTML template, this is what I have. I’m using *ngFor
to loop through the recipes and string interpolation to insert the recipe details where applicable.
...
<div *ngIf="doneLoading">
<div class="card mb-3 mx-auto"
style="width: 28rem;"
*ngFor="let recipe of recipes">
<img [routerLink]="recipe.RecipeUrl"
[src]="recipe.PhotoUrl"
class="card-img-top img-max-height"
[alt]="recipe.Title">
<div class="card-body">
<h4 class="card-title">{{recipe.Title}}</h4>
<p class="card-text">{{recipe.Description}}</p>
<a [routerLink]="recipe.RecipeUrl"
class="btn btn-light">View Recipe</a>
</div>
</div>
</div>
...
And, if I do say so myself, this looks beautiful.
I’ve also added in the loading state placeholder, which is visible while the delay
is in progress.
Recipe Details
One last thing I want to add is the recipe details. To load up a specific recipe, we’re going to get the information from the URL bar. Here’s the the controller looks like, with a blank recipe variable I created and put in the stub file.
// view-recipe.component.ts
public recipe: Recipe = {...DEFAULT_RECIPE};
private recipeSubscription$: Subscription = new Subscription();
constructor(
private route: ActivatedRoute,
private recipeService: RecipeService
) { }
ngOnInit(): void {
const url: string = this.route.snapshot.params['recipe-url'];
this.recipeSubscription$ = this.recipeService.getRecipeByUrl(url)
.subscribe({
next: r => {
if(r) this.recipe = r;
},
error: () => console.error("There was an error loading the recipe")
})
}
ngOnDestroy(): void {
this.recipeSubscription$.unsubscribe();
}
In regards to the ‘:recipe-url’
part in the router, the line this.route.snapshot.params['recipe-url']
takes the string from the url and uses that to find the recipe you want. The page then gets filled out like this (this is not the full template - you can find that in the GitHub repo).
<img class="img-header" [src]="recipe.PhotoUrl" [alt]="recipe.Title">
<h4 class="recipe-header">{{recipe.Title}}</h4>
<h5>{{recipe.Description}}</h5>
<p>{{recipe.Intro}}</p>
<h5 class="recipe-titles">Ingredients</h5>
<ul>
<li *ngFor="let ingredient of recipe.Ingredients">
{{ingredient.Name}} - {{ingredient.Quantity}} {{ingredient.Unit}}
</li>
</ul>
<div *ngFor="let step of recipe.Steps; let i = index;">
<h5 class="recipe-titles">Step {{i + 1}}</h5>
<p>{{step}}</p>
</div>
And here’s what the finished detail page looks like on desktop:
I have not added in the tags or rating yet. I want to add that in when we start to fill in the search results and more.
Summary and Recap
We went over a lot today. If you’ve made it this far, I salute you. There are a lot of places where I didn’t fill out the details, so please fill in the blanks by visiting the GitHub Repo I’ve beaten into you so many times.
The first thing I do when I create a new feature is set up a new module. I’ll also set up routing, which is likely to be added to later down the road. Next, I’ll add a model and a service. The service handles HTTP requests and anything else where there’s heavier business logic, or there’s a method which I want to be used by multiple components. The service and components are coupled using the Observer Pattern of software design. Finally, I set up the components and got the views out. Beautiful! All in a night’s work.
Read the next article in this series:
Subscribe to read these as they come out. I’ll be writing these up as I have time. I hope you learn something from them. Cheers!