Websites with Angular and Less CSS tutorials
9. The recipe page component
av_timer5 minute read

So am I actually going to learn how to cook now or... ?

  1. Topics of discussion
  2. The recipe component page

1. Topics of discussion

In this tutorial, we will be creating a new component, for the recipe page this time. So, let's have some fun.

2. The recipe component page

Now that we have the authentication out of the way, let's move on to the main focus of this website, which is of course the recipe page. This is a very interesting page because we will learn to render html in Angular. What I mean by that is that we will take a string containing html element in it and render it in our page.

First thing's first though. Since the scope if this page is different than the previous two, a new module is required:

ng g m components/Content

I went for the name content because this module is reserved for any and all pages that refer to the content on this website. Before we move on, remember to import the module in your app.module.ts file.

Now, let's create the component:

ng g c components/content/Recipe -m=Content --export

Let's also create a content service:

ng g s components/content/Content

Remember to add it to your exports array in the content.module.ts file.

Now that we have this out of the way, let's create a route that will point to our recipe page component in the app-routing.module.ts file:

{
  path: 'recipes',
  component: RecipeComponent
}

And let's add a link to this route in our navigation menu:

<a class="navigation-item" mat-list-item routerLink="/recipes" routerLinkActive="active">
  <mat-icon class="material-icons">room_service</mat-icon><span class="navigation-item-label">Recipes</span>
</a>

Now that we can actually navigate to the recipes page, let's create a json file inside our assets folder. Right click on the assets folder, select New File and enter the name recipes.json. Inside it, place the following code:

[
  {
    "name": "Chocolate Cake",
    "slug": "chocolate-cake",
    "recipe": "<h2>Chocolate Cake Recipe</h2><ul><li>Ingredient 1</li><li>Ingredient 2</li></ul>"
  },
  {
    "name": "Chicken Soup",
    "slug": "chicken-soup",
    "recipe": "<h2>Chicken Soup Recipe</h2><ul><li>Ingredient 1</li><li>Ingredient 2</li></ul><p>P.S. Can be used against people wearing robots suits and using ice guns, with very good success.</p>"
  }
]

Now, inside the content.service.ts, place the following code:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ContentService {

  constructor(private httpClient: HttpClient) { }

  public loadRecipes(): Observable<any> {
    return this.httpClient.get('./assets/recipes.json');
  }
}

In the service, we import the HttpClient service from the rxjs Angular package. This service is used to perform http operations. More often than not though, you will be using GET and POST operations which are used to retrieve information from a file or server and to send information to a server respectively. In our case, we retrieve the information from the json file we created earlier. Finally, the return type of the method is Observable<any>, which tells us that we can subscribe to the result of that method. Usually, this type is used for dynamic data and this is the most common use case for it. The any part of it allows us to convert the result of the method to any type we want. This will become clear in a few minutes when we write the component code.

In order for your code to work correctly, your will need to import the HttpClientModule inside the content.module.ts file.

Before we move on to the component, right click on the recipe folder inside the content folder. Select New Folder and type model. Now, inside that folder, create a file called recipe-details.model.ts and in it, place the following code:

export class Recipe {
  name: string;
  slug: string;
  recipe: string;
}

This is a model class which we will use in our component. And speaking of the component, let's put the following code in recipe.component.ts:

import { Component, OnInit } from '@angular/core';
import { ContentService } from '../content.service';
import { Recipe } from '../model/recipe-details.model';
import { finalize } from 'rxjs/operators';

@Component({
  selector: 'app-recipe',
  templateUrl: './recipe.component.html',
  styleUrls: ['./recipe.component.less']
})
export class RecipeComponent implements OnInit {

  recipes: Recipe[];
  recipeContent: string;
  loading: boolean;
  constructor(private contentService: ContentService) { }

  getRecipe(item: Recipe): void {
    this.recipeContent = item.recipe;
  }

  ngOnInit() {
    this.loading = true;
    this.contentService
      .loadRecipes()
      .pipe(finalize(() => { this.loading = false; }))
      .subscribe(response => {
        this.recipes = response;
      })
  }

}

As you can see, we provide a ContentService object in the constructor for this component. Said object is used in the ngOnInit() method to retrieve the recipes from the recipes.json file. Before we subscribe to the method though, we use the pipe() method, inside of which we use the finalize operator. The pipe method can be used for various operations on the result of the method you are calling. In our case, we use it to set the loading component attribute to false so that regardless if the method fails or not, the state of the component reverts back to normal when the method is finished.

In the subscribe method, all we do is take the result and assign it to the recipes attribute. This does not throw an error because, if you remember, the return type defined in the service was Observable<any>. Observable means you can subscribe to it, but any refers to the actual return type of the result, once you get to process it that is.

Why is this approach necessary? After all, Javascript is mostly a synchronous programming language (I am talking about Javascript since Typescript gets compiled to Javascript). Well, here's the thing. When you are requesting information from a server, or from a file, that request is asynchronous. What that means is that Javascript makes the request to the server or file and then moves on to the next line of code, without waiting for that request to finish. That's why, in order to gain access to the results of the request, we need the subscribe approach.

The final thing worth mentioning with regards to the component is that we also have a getRecipe() method, which retrieves the recipe content, which some of you may have noticed, contains HTML elements. And all of this comes into play when we modify our component template, like this:

<div class="container container-fluid">
  <div class="row">
    <div class="col-3">
      <div class="d-flex justify-content-center" *ngIf="loading">
        <div class="spinner-border" role="status">
            <span class="sr-only">Loading...</span>
        </div>
      </div>
      <ul *ngIf="!loading" class="recipe-list">
        <li class="recipe-list-item" *ngFor="let item of recipes" (click)="getRecipe(item)">
          {{item.name}}
        </li>
      </ul>
    </div>
    <div class="col-9" [innerHtml]="recipeContent"></div>
  </div>
</div>

As you can see, in this template, we show or hide some html blocks depending on whether or not the loading attribute is true. Basically, we want to show the recipes only when the request to get them has finished. This is accomplished by using the *ngIf operator from Angular.

In the col-3 div we proceed to display the recipes from the file. We go through the recipes array using the *ngFor Angular operator and for each item in the array, we display the name using the {{}} markup.

In the col-9 div, you can see that we have the innerHtml attribute, to which we pass the recipeContent component attribute, which is set every time we click on a component. The click event is handled via the (click) attribute.

The final thing we are going to do is add a bit of style to our component:

.recipe-list {
  list-style-type: none;
  padding-left: 0;
  margin-bottom: 0;
  border-right: 1px solid #062739;

  .recipe-list-item {
    height: 48px;
    cursor: pointer;
    padding-left: 10px;
    padding-top: 10px;
  }
}

:host ::ng-deep ul {
  margin-bottom: 0;
}

The second part of this code is used to style the code rendered via the innerHtml attribute.

And that about covers it for this tutorial. In the next one, we will be busy creating an add recipe component. See you then.

COMMENTS
No comments yet...