Skip to main content

How to implement an article detail page

January 30, 2024About 5 min

How to implement an article detail page

  • At the moment we only show an overview of all the articles. These articles are showing the full content.
  • We only want to show a brief preview of the article and the possibility to show the full article
  • The goals for this module:
    • Update our ArticleService with a new getArticleById method
    • Add a reusable ArticleDetailComponent
    • Navigate from the ArticleComponent to the ArticleDetailComponent
      • Adding a route with a parameter
      • Using a button with a click event
    • Show a full version of the article
    • Creating a pipe to limit the content of the article to a chosen amount of characters

Update the ArticleService

  • Let's improve the ArticleService by
    • moving our Article array to a private class variable
    • filling this array in the constructor
    • returning this array in the getArticles() method
    • creating a new getArticleById method
import { Injectable } from "@angular/core";
import { Article } from "./article";

@Injectable({
  providedIn: "root",
})
export class ArticleService {
  private articles: Article[] = [];

  constructor() {
    let article1: Article = {
      id: 1,
      title: "Title article",
      subtitle: "Subtitle article",
      imageUrl:
        "https://images.pexels.com/photos/1202723/pexels-photo-1202723.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=100&w=134",
      imageCaption: "caption image",
      content: `Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur voluptas sequi voluptatum pariatur! Quae cumque
      quidem dolor maxime enim debitis omnis nemo facilis sequi autem? Quae tenetur, repellat vero deleniti vitae
      dolores? Cum tempore, mollitia provident placeat fugit earum, sint, quae iusto optio ea officiis consectetur sit
      necessitatibus itaque explicabo?`,
      author: "Michaƫl Cloots",
      publishDate: "28/11/2020",
    };

    let article2: Article = {
      id: 2,
      title: "Title article 2",
      subtitle: "Subtitle article 2",
      imageUrl:
        "https://images.pexels.com/photos/3422964/pexels-photo-3422964.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=100&w=134",
      imageCaption: "caption image 2",
      content: `2 Lorem ipsum dolor sit amet consectetur adipisicing elit. Tenetur voluptas sequi voluptatum pariatur! Quae cumque
      quidem dolor maxime enim debitis omnis nemo facilis sequi autem? Quae tenetur, repellat vero deleniti vitae
      dolores? Cum tempore, mollitia provident placeat fugit earum, sint, quae iusto optio ea officiis consectetur sit
      necessitatibus itaque explicabo?`,
      author: "Florian Smeyers",
      publishDate: "30/11/2020",
    };

    this.articles.push(article1);
    this.articles.push(article2);
  }

  getArticles(): Article[] {
    return this.articles;
  }

  getArticleById(id: number): Article | null {
    return this.articles.find((a) => a.id === id) ?? null; //find = JavaScript method on arrays!
  }
}
codedescription
getArticleById(id: number) ... search for an article based on the id, if no article found return null

Hardcoded vs API

  • Remember: for now we work with a hardcoded array, but later on this service will call an API to get the articles

Create the ArticleDetailComponent

  • Create the ArticleDetailComponent by executing the following command:
ng g c article-detail-component
  • article-detail-component.ts
import { Component, inject, OnInit } from "@angular/core";
import { ArticleComponent } from "../article-component/article-component";
import { Article } from "../article";
import { ArticleService } from "../article-service";
import { ActivatedRoute } from "@angular/router";

@Component({
  selector: "app-article-detail-component",
  imports: [ArticleComponent],
  templateUrl: "./article-detail-component.html",
  styleUrl: "./article-detail-component.css",
})
export class ArticleDetailComponent implements OnInit {
  article!: Article;

  private readonly articleService = inject(ArticleService);
  private readonly route = inject(ActivatedRoute);

  ngOnInit(): void {
    const articleId = this.route.snapshot.paramMap.get("id");
    if (articleId != null) {
      let articleTemp = this.articleService.getArticleById(+articleId) ?? null;
      if (articleTemp != null) {
        this.article = articleTemp;
      }
    }
  }
}
codedescription
ActivatedRoutewe inject the ActivatedRoute so we can access the parameters. ActivatedRoute is part of the @angular/router module, so we import it
this.route.snapshot.paramMap.get('id')get the id route parameter. We define this parameter in the route in the AppRoutingModule
  • article-detail.component.html
@if(article) {
<app-article-component
  [article]="article"
  [isDetail]="true"
></app-article-component>
}
  • Again: because we use the ArticleComponent in our article-detail.component.html, we have to import it in our component!
    • imports: [CommonModule, ArticleComponent],
  • We want to show the full article
  • We could copy/paste the content of the ArticleComponent to this template and show the full article OR we could make our ArticleComponent smarter by adding an isDetail input parameter
  • The second option is the best option, because we want as little duplicate code as possible.
  • But before we update the ArticleComponent, let's configure the routing
  • Add a new route with id as a parameter in the app.routes.ts file
import { Routes } from '@angular/router';
import { ArticleComponent } from './article-component/article-component';
import { HomeComponent } from './home-component/home-component';
import { ArticleDetailComponent } from './article-detail-component/article-detail-component';

export const routes: Routes = [
    { path: '', component: HomeComponent },
    { path: 'article', component: ArticleComponent },
    { path: 'article/:id', component: ArticleDetailComponent },
];



 




 

Route parameters

  • :id will assume that the first number in your route is the id, like http://localhost:5878/article/1 - Get it by using this.route.snapshot.paramMap.get('id') where route is the ActivatedRoute

Update the ArticleComponent

  • Let's update the ArticleComponent:
import { Component, inject, Input, OnInit } from '@angular/core';
import { Article } from '../article';
import { Router } from '@angular/router';

@Component({
  selector: 'app-article-component',
  imports: [],
  templateUrl: './article-component.html',
  styleUrl: './article-component.css'
})
export class ArticleComponent implements OnInit {
  @Input() article!: Article;
  @Input() isDetail: boolean = false;

  private router = inject(Router);

  ngOnInit(): void {
  }

  detail(id: number) {
    this.router.navigate(['/article', id]);
  }
}


 









 

 




 
 
 

codedescription
@Input() isDetail: boolean = false;new input() parameter
detail(id: number) { ... }method that we can choose to be executed when a certain user interaction takes place in the template
this.router.navigate(['/article', id]);use the router to navigate to the article/:id-path (which will load the ArticleDetailComponent in the router-outlet)
  • article.component.html
@if(article) {
<article class="flex flex-col justify-between h-full bg-white border-gray-200 border-2 p-6 shadow">
    <h1 class="text-4xl my-3">{{article.title}}</h1>
    <h4 class="text-xl my-3">{{article.subtitle}}</h4>
    <figure class="self-center my-3">
        <img class="max-w-full h-auto" src="{{article.imageUrl}}" alt="{{article.imageCaption}}">
        <figcaption class="italic text-center">{{article.imageCaption}}</figcaption>
    </figure>
    @if(!isDetail) {
    <p class="text-justify my-3">
        {{article.content}}
    </p>
    } @else {
    <p class="text-justify my-3">
        {{article.content}}
    </p>
    }
    <p class="py-2">
        <button class="bg-orange-500 hover:bg-orange-700 text-white font-bold py-2 px-4 rounded" (click)="detail(article.id)">Read more</button>
    </p>
    <footer class="italic uppercase">
        <p>Author: {{article.author}}, Published: {{article.publishDate}}</p>
    </footer>
</article>
}








 
 
 
 
 
 
 
 
 
 
 
 





codedescription
@if(!isDetail) { ... }based on the property isDetail we want to show the short or full content
(click)="detail(article.id)"click event that executes the detail() method with the id of the article as parameter

Create a pipe

  • All good, but we need to change how the content of an article is displayed. In detail mode we want to show the content entirely, else we just want to show the first x characters, trailed by ...
  • Values can be easily transformed by using pipes. Angular has some built-in pipes, but we will create one ourselves
  • Execute the following command te create the ShortenContentPipe:
ng g pipe shorten-content
  • Modify the shorten-content.pipe.ts file
import { Pipe, PipeTransform } from "@angular/core";

@Pipe({
  name: "shortenContent",
})
export class ShortenContentPipe implements PipeTransform {
  transform(value: string, numberOfCharacters?: number): string {
    if (value.length < (numberOfCharacters ?? 250)) {
      return value;
    } else {
      return value.slice(0, numberOfCharacters ?? 250) + " ...";
    }
  }
}
  • The basic idea of a pipe is that it receives a value, transforms it, and returns the new transformed value
  • The first parameter is the received value, the second one is an argument. In this case we want to specify the number of characters which are allowed for the short content
  • In the transform method we perform the transformation and return the new value
  • We can now use this pipe in the template article-component.html:
@if(article) {
<article class="flex flex-col justify-between h-full bg-white border-gray-200 border-2 p-6 shadow">
    <h1 class="text-4xl my-3">{{article.title}}</h1>
    <h4 class="text-xl my-3">{{article.subtitle}}</h4>
    <figure class="self-center my-3">
        <img class="max-w-full h-auto" src="{{article.imageUrl}}" alt="{{article.imageCaption}}">
        <figcaption class="italic text-center">{{article.imageCaption}}</figcaption>
    </figure>
    @if(!isDetail) {
    <p class="text-justify my-3">
        {{article.content | shortenContent : 250}}
    </p>
    } @else {
    <p class="text-justify my-3">
        {{article.content}}
    </p>
    }
    <p class="py-2">
        <button class="bg-orange-500 hover:bg-orange-700 text-white font-bold py-2 px-4 rounded" (click)="detail(article.id)">Read more</button>
    </p>
    <footer class="italic uppercase">
        <p>Author: {{article.author}}, Published: {{article.publishDate}}</p>
    </footer>
</article>
}










 














Pipe notation

  • Using a pipe always requires the following notation: - Within double curly braces - Using the | (pipe) symbol after the property - Arguments after the : (colon)

ArticleComponent

  • A standalone pipe should always be imported by the component that is using it!
  • We are using the pipe in the ArticleComponent, so we add it to the imports array:
// article.component.ts
@Component({
  selector: 'app-article-component',
  imports: [ShortenContentPipe],
  templateUrl: './article-component.html',
  styleUrls: ['./article-component.css']
})
export class ArticleComponent implements OnInit {



 




Result:

short contentdetail full content

Exercise

  • At this moment the article in the detail page also has the 'Read more' button - Remove this button (only when on the detail page) - Add a new button to go back the previous page

    TIP

    You can write JavaScript in your component to go back to the previous page

Result:

article detail exercise
article detail exercise

Exercise

  • Create a new pipe to capitalize the first word of the content

Result:

article detail pipe exercise
article detail pipe exercise