Search
  • Gábor Csepregi

Chapter 3 - Connecting the two halves

Preface


In the previous chapters, we have created a new project called BlogTaskManager. First, we implemented the frontend, next we dived into the backend part. For some, it might have felt unnatural not to do everything at once. The reason not to do so is grabbing only the size of work, that can be done in a few minutes.


I have learned this about 15 years ago while being at my first real-world job. I was just a junior and my manager emphasized that there's no way I will meet my estimations until I learn by experience how to estimate. Trying to do my best, I was failing around 20% most of the times. Then he told me, if my estimation says my work would take more than 30 minutes, then I need to rethink and split it into smaller subtasks.


And that made sense ever since. If I plan something for 30 minutes and have an error rate of 20% in general, that means that I will finish in 36 minutes instead. On the other hand, planning for 5 days means it will take 6 days. If circumstances are the same. But the larger the estimate the bigger the error of the estimate becomes.


I know in the world of agile there are no more estimates. At least no one seems to like to do it. Am I an ageing dinosaur of an extinct era trying to follow the same habits still?


Anyways, today we will finish what we've started so far, and connect to two ends of the application. This will require some changes in the way we handle the data on the frontend side. Remember, we've introduced the new status field on the backend!


Step 1 - Update creating a new task


We will start with a little update to the service. If you remember we have extracted all the task-related functionality so that we could share this service between the list and the form components. Now it comes handy as we need to update only one place to connect to the backend. The form and list don't need to know how they get, or where they submit the data.


First, inject the API endpoint address into the environment settings, so we can have a different setup for different runtimes:

//src/environments/environment.ts
export const environment = {
  production: false,
  apiEndpointRoot: 'http://localhost:7071/api'
};

Next, we add the Http module to our app module, so that we can inject instances of it into our components:

//src/app/app.module.ts
...
imports: [
    ...
    HttpClientModule,
    ...
]

And at last, we modify our service to accept an HttpClient and accommodate the create method, to call the actual endpoint instead of storing the value in a local cache:

//blog-task.service.ts
...
constructor(private readonly http: HttpClient) { }
...
create(value: BlogTask): Observable<any> {
  return new Observable<boolean>(observer => {
    this.http.post(`${environment.apiEndpointRoot}/tasks`, value).subscribe(
      () => { // success path
        observer.next();
      },
      () => { // error path
        observer.error();
      });
  });
}
...


Beware of the side effect of this change, your UI won't get updated anymore, when you check if it works! For that, you will need the next step.


Step 5.2 - Get the list of tasks


Now let's quickly fix the list as well. For this, we update the subject and the observable to return arrays of BlogTasks instead of strings. Then from the constructor call a new private method to update our subject with a value returned from the server:

//blog-task.service.ts

private tasks$ = new BehaviorSubject<BlogTask[]>([]);

get tasks(): Observable<BlogTask[]> { return this.tasks$.asObservable();  }

constructor(private readonly http: HttpClient) {
  this.updateTasks();
}

private updateTasks() {
  this.http
    .get<BlogTask[]>(`${environment.apiEndpointRoot}/tasks`)
    .subscribe(
      result => this.tasks$.next(result),
    );
}
...
//in the success path of the create method:
  this.updateTasks();
...

If you want to auto-update the UI on new task creation, then you will need to change the create method again to call the 'updateTasks' method in the success path. So whenever you create a new task it will fetch the list from the server.

All we are left to do is update the template. Remember? Up to now, we were handling strings, now we switched to an object, so use the summary property:

//blog-task-list.component.html
<mat-list-option *ngFor="let task of tasks | async">
  {{task.summary}}
</mat-list-option>

And change the component as well to remove the compile issue:

//blog-task-list.component.ts
...
tasks: Observable<BlogTask[]>;
...


At this point, you should see the same result when running the application that you had before. The only exception is that your new tasks are persisted now, so on page refresh, you get your list back.


Step 5.3 - Update task status

On the server-side, we have moved forward a bit with the statuses and introduced a multivalued status instead of a simple to-do/done state pair. If we wish to replicate this on the frontend, we need to modify the list a bit. Do we need to change the form as well? I don't think so. Every task enters the system as new, so the form is good as it is for now.


First, we need to update the frontend representation of a BlogTask to include the id and the status:

//models/blog-task.ts 
export interface BlogTask {
   id: string;
   summary: string;
   status: string;
}  

On the server-side, we have moved forward a bit with the statuses and introduced a multivalued status instead of a simple to-do/done state pair. If we wish to replicate this on the frontend, we need to modify the list a bit. Do we need to change the form as well? I don't think so. Every task enters the system as new, so the form is good as it is for now.

//blog-task-list.component.ts
taskCount: number; 
finishedCount: number;

We need to add a new subscription on the constructor as well, so we can update these counters whenever the observable changes:

//blog-task-list.component.ts 
this.tasks.subscribe(tasks => {   
    this.taskCount = tasks.length;   
    this.finishedCount = tasks.filter((task) => task.status === 'done').length; 
});

Now let's change the layout of the list to include a status selector instead of a checkbox. For this, we will need a different component from the material library. Also, we update the output for the counters at the same time:

//blog-task-list.component.html
<p>Finished {{finishedCount}} tasks of {{taskCount}}</p>
<mat-list #taskList>
  <mat-list-item *ngFor="let task of tasks | async">
      <mat-form-field>
        <mat-select [value]="task.status">
          <mat-option value="new">New</mat-option>
          <mat-option value="open">Open</mat-option>
          <mat-option value="in progress">In progress</mat-option>
          <mat-option value="done">Done</mat-option>
        </mat-select>
      </mat-form-field>
      <div class="ml-2">
        {{task.summary}}
      </div>
  </mat-list-item>
</mat-list>

If you followed every step then you should have a UI like this:

For the statuses to work, we still miss one step. We need to call our patch endpoint whenever the selection changes for a task. To achieve this, we can use the following method in the task service that has access to the backend:

//blog-task.service.ts
updateStatus(taskId: string, status: string): Observable<any> {
  return new Observable<any>(observer => {
    this.http
    .patch(`${environment.apiEndpointRoot}/tasks/${taskId}`, {
      status: status
    })
    .subscribe(
      () => {
        this.updateTasks();
        observer.next();
      },
      () => observer.error()
    )
  });
}

There are multiple ways of connecting it to the UI changes, I prefer to call it straight from the template. Change the mat-select tag to look like this:

//blog-task-list.component.html
<mat-select [value]="task.status" (selectionChange)="taskSvc.updateStatus(task.id, $event.value).subscribe()">

Now, whenever we change a checkbox the corresponding record in the database is updated. And if you look at the updateStatus method, you can see, that we update our list from this success as well.


Wrap it up


So far we have created a mini-project. On one side, it's good to practice the basics from time to time. On the other hand, we have something to expand on later. And last but not least it will allow me to showcase the benefits of a technique I found quite useful for the last decade. And it has a lot to do with how you capture data about the data you want to store.


See you next time!

12 views

©2020 by Gabor's blog. Proudly created with Wix.com