michaelAwad.io

Creating a time management app in Angular and Typescript Part 2

...

December 13, 2020

<--- Angular Time management App Part 1

In the previous article we started with the creation of our Angular Typescript app. We created the building blocks to display our week schedule In this article we will start diving deeper into Typescript and even do some unit testing. I will also guide you through the thought process of refactoring your code and making it more readable

On the screen you see the week roster, below you see the arrows and the week number. This is the component responsible for browsing through weeks.

Screen two

Time management app screen one -Angular Typescript Time Management Weekly Planner

This is the component I want to focus on for this part of the series. It looks small and simple, but you will notice the further we get creating this component, the deeper we get into the layers of complexity.

Generate the component

First, we need to think of a descriptive name and I would suggest calling it: week-selector. Browse to the components folder and generate the Angular component:

ng generate component week-selector

How are we going to build it?

In this component we have several challenges we need to tackle:

  • Having to work with dates
  • We can’t scroll into the future, because we can’t track weeks from the future
  • We can’t scroll more back than the start date, because we don’t have any data before that
  • The week-roster component needs to know about the week number, so it can display data accordingly

Working with dates

JavaScript has it’s own Date functions and could be used. However, it doesn’t take all edge cases into account and will leave you questioning certain behaviors and you can loose a lot of time trying to find the issue.

Therefore I would recommend using a separate package for this, that specializes in dates. There are several, but I prefer date-fns. What date-fns for example will do it will give you dumbed-down functions for making calculations with dates, like adding and subtracting weeks.

So let’s start by adding this to our project:

yarn add date-fns

Add the following to imports from date-fns in our week-selector.component.ts file:

import { addWeeks, subWeeks, getWeek } from 'date-fns';

@Input and @Output

To determine in what part of the year we are with the week-selector and to determine whether what scroll directions should be disabled, we need multiple inputs:

  • currentDate
  • startDate

When we know these dates we can determine the week scrolling restrictions. The week-selector also needs to send the week number to the week roster Angular Typescript Component so it can process that. So we will emit a week number based on what week we are in. That is where @Output comes in to play.

Right below export class add the following lines:

  @Input('currentDate') currentDate: Date;
  @Input('startDate') startDate: Date;
  @Input('subTitle') subTitle: string;

  @Output() passDate = new EventEmitter<number>();

You will encounter red lines under Input, Output, and EventEmitter, that is correct. They need to be imported from Angular core. So make the first import look like this:

import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';

Data attributes

Now let’s think about the data attributes we need when we are scrolling through weeks. The first one that comes to mind of course is the week number, which is if the number type.

We have two restrictions:

  • Scrolling into future weeks
  • Scrolling before the start date

For these two restrictions we will create methods for later on.

We want to disable scrolling through weeks when these booleans test true. So let’s create two data attributes for that as well:

  • disabledLeft of boolean type
  • disabledRight of boolean type

Last but not least the date attribute of type Date, which is required to get the week number.

The data attributes section will look like this:

  weekNumber: number; 
  isCurrentWeek: boolean = true;
  disabledLeft: boolean = false;
  disabledRight: boolean = true;
  date: Date

Initial state

When you load this component, we want to make sure a certain state is already set. That is where the ngOnit function comes into play. In these functions, you can put anything you want to have executed on load.

So let’s think about this, what do we want as an initial state:

  • We want to make sure we set the date to the current date, which comes from the Input prop
  • We want to have the week number from the current date
  • We want this component to emit the week number

In the end it should look something like this:

  ngOnInit() {
    this.date = this.currentDate
    this.weekNumber = getWeek(this.date);
    this.passDate.emit(this.weekNumber)
  }

Creating the methods

Let’s start with the methods to check the state. On load, we always want to load the current week. But also when we scroll we want to know when we get to the current week again because that is the point we want to prevent the user from scrolling to the next week.

Check if we are in the current week

Let me break this method down, what do we want to achieve:

  1. Let’s think of a descriptive name first. We want to check something to be true or false so let’s start with: check. We want to know if it’s the current week so: isCurrentWeek. In the end that makes: checkIsCurrentWeek
  2. We use the getYear method from date-fns which takes a date as an argument, in this case, this.date. This is the date that is initially set to this.currentDate but will be overwritten with the date that is scrolled to. We will discuss that later on
  3. We compare the year with this.currentDate to check if the selected date equals that.
  4. We use the && operator to check the same for the week using the getWeek method from date-fns which works the same as the getYear method
  5. If they both test true the method will return true, otherwise, it will return false

This the method I came up with achieving the steps above:

  private checkIsCurrentWeek(): boolean {
    return getYear(this.date) === getYear(this.currentDate) && 
    getWeek(this.date) === getWeek(this.currentDate);
  }

Check if the date is before the start date

Let me break down the following method:

  1. Let’s think of a name first we want to check something so: is. We want to know something about a date so: Date. We want to know if it is before the start date: BeforeStartDate. That makes: isDateBeforeStartDate
  2. First we compare the years with each other and check if this.date is smaller than this.startDate. We do this again with the getYear method from date-fns. 2.Second we compare again the years, but check if they are the same and with the && operator also check if the week from this.date is equal or smaller than this.startDate’s week.
  3. If one of the conditions is met the method will return true

This the method I came up with achieving the steps above:

  private isDateBeforeStartDate() {
    return getYear(this.date) < getYear(this.startDate) || (getYear(this.date) === getYear(this.startDate) && getWeek(this.date) <= getWeek(this.startDate) );
  }

Selecting weeks

When we load the page the first and only action we have available is to scroll to the previous week. So let’s think about what this method should do.

  1. Let’s think of the name first we expect something to happen based on that function so: on. It will go to the previous week so: PreviousWeek. That gives us onPreviousWeek
  2. It should deduct one week from this.date. The data attribute that keeps the state of the selected date. This can be achieved with one of the methods we get from date-fns which is subWeeks. It takes two arguments a date and a number to deduct it by.
  3. The weekNumber data attribute should be updated with the new week number. This can be achieved with the getWeek method from date-fns we used earlier. So we can use the result of the previous step as an argument for this method.
  4. Now that we have the new date set we need to check if we are in the current week, so let’s invoke the checkIsCurrentWeek function by assigning it to the data attribute this.disabledRight. Why is that? That is the attribute that disables the right button for selecting the next month and this way be disabled when the checkIsCurrentWeek method returns true
  5. The other check we want to do is if the date is before the start date. So let’s invoke that function the same way is we do for this.disabledRight. To assign it to this.disabledLeft and will be disabled when isDateBeforeStartDate returns true.
  6. The final thing we want the function to do is to emit the weekNumber value to the parent component so it can act on that number. We assigned passDate to @Output at the beginning of this article. So we can call emit on that with this.weekNumber as an argument.

This the method I came up with achieving the steps above:

  onPreviousWeek() {
    this.date = subWeeks(this.date, 1);
    this.weekNumber = getWeek(this.date)
    
    this.disabledRight = this.checkIsCurrentWeek();;
    this.disabledLeft = this.isDateBeforeStartDate();
    
    this.passDate.emit(this.weekNumber);
  }

Let’s do the same for selecting the next week

  1. Let’s think of the name first we expect something to happen based on that function so: on. It will go to next week so: NextWeek. That gives us onNextWeek 1.only difference with the previous week it should add one week from this.date instead of deducting. This can be achieved with one of the methods we get from date-fns which is addWeeks. It takes two arguments a date and a number to add it with.

This the method I came up with achieving the steps above:

  onNextWeek() {
    this.date = addWeeks(this.date, 1);
    this.weekNumber = getWeek(this.date)
    
    this.disabledRight = this.checkIsCurrentWeek();;
    this.disabledLeft = this.isDateBeforeStartDate();
    
    this.passDate.emit(this.weekNumber);
  }

Refactoring

So we covered all the logic, and your Angular Typescript file should look like this:

import { 
  Component, 
  OnInit, 
  Input, 
  Output, 
  EventEmitter, 
  AfterContentChecked } from '@angular/core';
import { addWeeks, subWeeks, subYears, getWeek, getYear } from 'date-fns';

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

  @Input() currentDate: Date;
  @Input() startDate: Date;
  @Input() subTitle: string;

  @Output() passDate = new EventEmitter<number>();

  weekNumber: number;
  year: number;
  isCurrentWeek: boolean;
  isStartingWeek: boolean;
  disabledLeft: boolean = false;
  disabledRight: boolean;
  date: Date

  constructor() {}

  ngOnInit() {
    this.isCurrentWeek = true;
    this.disabledRight = true;
    this.date = this.currentDate
    this.weekNumber = getWeek(this.date);
    this.passDate.emit(this.weekNumber)
    this.year = getYear(this.date)
  }

  onPreviousWeek() {
    this.date = subWeeks(this.date, 1);
    this.weekNumber = getWeek(this.date)
    
    this.checkIsCurrentWeek();
    
    
    this.disabledRight = false;
    this.disabledLeft = this.isDateBeforeStartDate();
    
    this.passDate.emit(this.weekNumber);
  }

  onNextWeek() {
    this.date = addWeeks(this.date, 1);
    this.weekNumber = getWeek(this.date)
    
    this.passDate.emit(this.weekNumber);
    
    this.disabledLeft = false;
    this.isStartingWeek = false
    
    this.disabledRight = this.checkIsCurrentWeek();
  }

  private checkIsCurrentWeek() {
    return getYear(this.date) === getYear(this.currentDate) && getWeek(this.date) === getWeek(this.currentDate);
  }

  private isDateBeforeStartDate() {
    return getYear(this.date) < getYear(this.startDate) || (getYear(this.date) === getYear(this.startDate) && getWeek(this.date) <= getWeek(this.startDate) );
  }
}

When you take a better look, you will see some duplicated code in the onPreviousWeek and onNextWeek. Let’s handle that first.

So what is the only difference between these functions? Right, it’s either adding or subtracting weeks. What is determining that? It is either previous or next week.

So let’s make a new private function, which is a copy of onNextWeek. Rename it to selectWeek. Now we give it an argument, which will be the following object:

selection: { direction: '' }

On the first line we check based on direction next, when that is true we add weeks else we substract. Which will look something like this:

selection['direction'] === 'next' ? this.date = addWeeks(this.date, 1) : this.date = subWeeks(this.date, 1);

The rest of the function remains the same. However, this is not very readable, since on the previous and next week the function calls would be the same just passing different arguments. A big miss conception about code readability is that it should result in less code. This is not the case, adding more code and making it more explicit is perfectly fine.

So let’s create functions that invokes our selectWeek function and pass it te right argument. Let’s rename the functions as well, since they don’t select the weeks anymore, but just invoke the selectWeek function. It will look something like this:

  goToNextWeek() {
    let selection = { direction: 'next' }
    this.selectWeek(selection)
  }

Here you will find the new functions:

  goToNextWeek() {
    let selection = { direction: 'next' }
    this.selectWeek(selection)
  }

  goToPreviousWeek() {
    let selection = { direction: 'previous' }
    this.selectWeek(selection)
  }

  private selectWeek(selection: object) {
    selection['direction'] === 'next' ? this.date = addWeeks(this.date, 1) : this.date = subWeeks(this.date, 1);
    this.weekNumber = getWeek(this.date)
  
    const isDateBeforeStartDate = this.checkIsDateBeforeStartDate();
    const isCurrentWeek = this.checkIsCurrentWeek()
    
    this.checkIsCurrentWeek();
    
    this.disabledLeft = isDateBeforeStartDate
    this.disabledRight = isCurrentWeek 
    
    this.passDate.emit(this.weekNumber);
  }

For the complete refactored file check out the GitHub repo: week-selector.component.ts

So If we talk about readability and look again at our file, you will probably find another function that is hard to read and that is the isDateBeforeStartDate function. It now makes two comparisons on one line. Like I mentioned earlier it is perfectly fine to make to code longer and more explicit if it makes it more readable so let’s split it up:

  private checkIsDateBeforeStartDate() {
    if(getYear(this.date) === getYear(this.startDate)) {
      return getWeek(this.date) <= getWeek(this.startDate);
   }
   return getYear(this.date) < getYear(this.startDate);  
  }

Way more readable right?

Putting it all together

Copy the following HTML and paste it into the week-selector.component.html file:

<div>
    <div>
        <button
            (click)="goToPreviousWeek()"
            [disabled]="disabledLeft"
        > 
            < 
        </button>
    </div>

    <div>
        {{ weekNumber }}
    </div>

    <div>
        <button
            (click)="goToNextWeek()"
            [disabled]="disabledRight"
        >
              > 
        </button>
    </div>
</div>

<div>
    <h1 *ngIf="disabledRight"> 
        Current Week
    </h1>
    <h1 *ngIf="disabledLeft"> 
        Starting Week
    </h1>
</div>

To test if it works correctly, browse to the week-roster.component.html file paste the following at the bottom of the file:

  <app-week-selector
  [currentDate]="currentDate"
  [startDate]="startDate"
> </app-week-selector>

Add currentDate and startDate to the data attributes in your week-roster.component.ts

  currentDate = new Date(Date.now())
  startDate = new Date(2020, 9 ,1)

Your screen should look like this:

Time management app week overview -Angular Typescript Time Management Weekly Planner

Unit testing our component

Let’s start with setting up the starting conditions for our tests. In this case, in the beforeEach we set the input props and we can start expecting different things to happen when our function is triggered.

  beforeEach(() => {
    fixture = TestBed.createComponent(WeekSelectorComponent);
    component = fixture.componentInstance;
    component.currentDate = new Date(2020, 11);
    component.startDate = new Date(2020, 11);

    fixture.detectChanges();
  });

  it('should emit weeknumber minus one when clicked', () => {
    spyOn(component.passDate, 'emit');
 
    fixture.detectChanges();
    let currentWeekMinusOne = getWeek(subWeeks(component.date, 1))
    
    component.goToPreviousWeek()

    expect(component.weekNumber).toBe(currentWeekMinusOne);
    expect(component.passDate.emit).toHaveBeenCalledWith(currentWeekMinusOne)
    expect(component.disabledLeft).toBe(true)
    expect(component.disabledRight).toBe(false)
  });

For the full spec file check out the GitHub repo: week-selector.component.spec.ts

Conclusion and round-up of Creating a time management app in Angular and Typescript Part 2

We started diving more into Typescript by creating the week selector component. We had to work with dates and decided that date-fns is a great package for this use case.

After we created our component we refactored our code and made it more readable. Based on those final changes we created unit tests for the functions we wrote.

You can find the code we discussed in this repo: Angular Time management App Github Repository

What’s next?

In Creating a time management app in Angular and Typescript Part 3, we start introducing data and act upon that in the week roster component based on the week selector component output.

<--- Angular Time management App Part 1

Stay tuned for Creating a time management app in Angular and Typescript Part 3


Michael Awad

I'm fascinated by the power of a strong mindset. I combine this with being a web developer, which keeps me motivated. But how, you may ask? That's what I share on this website. For more information about me personally, check out the about me page: About Me

Are you enjoying this blog and do you want to receive these articles straight into your inbox? Enter your e-mail below and hit the subscribe button!



I won't send you spam and you can unsubscribe at any time