Creating a directive to calculate the read time for articles
In this recipe, you'll create an attribute directive to calculate the read time of an article, just like Medium. The code for this recipe is highly inspired by my existing repository on GitHub, which you can view at the following link: https://github.com/AhsanAyaz/ngx-read-time.
Getting ready
The project for this recipe resides in chapter02/start_here/ng-read-time-directive
:
- Open the project in VS Code.
- Open the terminal, and run
npm install
to install the dependencies of the project. - Once done, run
ng serve -o
.This should open the app in a new browser tab, and you should see something like this:
How to do it…
Right now, we have a paragraph in our app.component.html
file for which we need to calculate the read time in minutes. Let's get started:
- First, we'll create an attribute directive named
read-time
. To do that, run the following command:ng g directive directives/read-time
- The preceding command created an
appReadTime
directive. We'll first apply this directive todiv
inside theapp.component.html
file with theid
property set tomainContent
, as follows:... <div class="content" role="main" id="mainContent" appReadTime> ... </div>
- Now, we'll create a configuration object for our
appReadTime
directive. This configuration will contain awordsPerMinute
value, on the basis of which we'll calculate the read time. Let's create an input inside theread-time.directive.ts
file with aReadTimeConfig
exported interface for the configuration, as follows:import { Directive, Input } from '@angular/core'; export interface ReadTimeConfig { Â Â wordsPerMinute: number; } @Directive({ Â Â selector: '[appReadTime]' }) export class ReadTimeDirective { Â Â @Input() configuration: ReadTimeConfig = { Â Â Â Â wordsPerMinute: 200 Â Â } Â Â constructor() { } }
- We can now move on to getting the text to calculate the read time. For this, we'll use the
ElementRef
service to retrieve thetextContent
property of the element. We'll extract thetextContent
property and assign it to a local variable namedtext
in thengOnInit
life cycle hook, as follows:import { Directive, Input, ElementRef, OnInit } from '@angular/core'; ... export class ReadTimeDirective implements OnInit { Â Â @Input() configuration: ReadTimeConfig = { Â Â Â Â wordsPerMinute: 200 Â Â } Â Â constructor(private el: ElementRef) { } Â Â ngOnInit() { Â Â Â Â const text = this.el.nativeElement.textContent; Â Â } }
- Now that we have our text variable filled up with the element's entire text content, we can calculate the time to read this text. For this, we'll create a method named
calculateReadTime
by passing thetext
property to it, as follows:... export class ReadTimeDirective implements OnInit {   ...   ngOnInit() {     const text = this.el.nativeElement.textContent;     const time = this.calculateReadTime(text);   }   calculateReadTime(text: string) {     const wordsCount = text.split(/\s+/g).length;     const minutes = wordsCount / this.configuration.    wordsPerMinute;     return Math.ceil(minutes);   } }
- We've got the time now in minutes, but it's not in a user-readable format at the moment since it is just a number. We need to show it in a way that is understandable for the end user. To do so, we'll do some minor calculations and create an appropriate string to show on the user interface (UI). The code is shown here:
... @Directive({ Â Â selector: '[appReadTime]' }) export class ReadTimeDirective implements OnInit { ... Â Â ngOnInit() { Â Â Â Â const text = this.el.nativeElement.textContent; Â Â Â Â const time = this.calculateReadTime(text); Â Â Â Â const timeStr = this.createTimeString(time); Â Â Â Â console.log(timeStr); Â Â } ... Â Â createTimeString(timeInMinutes) { Â Â Â Â if (timeInMinutes === 1) { Â Â Â Â Â Â return '1 minute'; Â Â Â Â } else if (timeInMinutes < 1) { Â Â Â Â Â Â return '< 1 minute'; Â Â Â Â } else { Â Â Â Â Â Â return `${timeInMinutes} minutes`; Â Â Â Â } Â Â } }
Note that with the code so far, you should be able to see the minutes on the console when you refresh the application.
- Now, let's add an
@Output()
to the directive so that we can get the read time in the parent component and display it on the UI. Let's add it as follows in theread-time.directive.ts
file:import { Directive, Input, ElementRef, OnInit, Output, EventEmitter } from '@angular/core'; ... export class ReadTimeDirective implements OnInit {   @Input() configuration: ReadTimeConfig = {     wordsPerMinute: 200   }   @Output() readTimeCalculated = new   EventEmitter<string>();   constructor(private el: ElementRef) { } ... }
- Let's use the
readTimeCalculated
output to emit the value of thetimeStr
variable from thengOnInit()
method when we've calculated the read time:... export class ReadTimeDirective { ... Â Â ngOnInit() { Â Â Â Â const text = this.el.nativeElement.textContent; Â Â Â Â const time = this.calculateReadTime(text); Â Â Â Â const timeStr = this.createTimeString(time); Â Â Â Â this.readTimeCalculated.emit(timeStr); Â Â } ... }
- Since we emit the read-time value using the
readTimeCalculated
output, we have to listen to this output's event in theapp.component.html
file and assign it to a property of theAppComponent
class so that we can show this on the view. But before that, we'll create a local property in theapp.component.ts
file to store the output event's value, and we'll also create a method to be called upon when the output event is triggered. The code is shown here:... export class AppComponent { Â Â readTime: string; Â Â onReadTimeCalculated(readTimeStr: string) { Â Â Â Â this.readTime = readTimeStr; Â Â } }
- We can now listen to the output event in the
app.component.html
file, and we can then call theonReadTimeCalculated
method when thereadTimeCalculated
output event is triggered:... <div class="content" role="main" id="mainContent" appReadTime (readTimeCalculated)="onReadTimeCalculated($event)"> ... </div>
- Now, we can finally show the read time in the
app.component.html
file, as follows:<div class="content" role="main" id="mainContent" appReadTime (readTimeCalculated)="onReadTimeCalculated($event)">   <h4>Read time = {{readTime}}</h4>   <p class="text-content">     Silent sir say desire fat him letter. Whatever     settling goodness too and honoured she building     answered her. ...   </p> ... </div>
How it works…
The appReadTime
directive is at the heart of this recipe. We use the ElementRef
service inside the directive to get the native element that the directive is attached to, then we take out its text content. The only thing that remains then is to perform the calculation. We first split the entire text content into words by using the /\s+/g
regular expression (regex), and thus we count the total words in the text content. Then, we divide the word count by the wordsPerMinute
value we have in the configuration to calculate how many minutes it would take to read the entire text. Easy peasy, lemon squeezy.
See also
- Ngx Read Time library (https://github.com/AhsanAyaz/ngx-read-time)
- Angular attribute directives documentation (https://angular.io/guide/testing-attribute-directives)