Search
  • Gábor Csepregi

Chapter 1 - Project BlogTaskManager

Recap


So we have discussed a bit of Data-Driven Design, and then the problem of project definitions created on different levels of the corporate hierarchy.


Where are we heading now?


Now let's get our hands dirty and start the project our client needs. How do we start such a big project from scratch? I prefer going the Lean way and split it into goals that I can achieve in a few minutes. So this time we will:


  1. create a new Angular project

  2. add some dependencies that will be of help later

  3. create a form that we can use to create the most simple task possible

  4. create a list that can display the most simple task

  5. bind the form and the list together


This blog does not intend to be an Angular tutorial, so if I skip something, it's on purpose. The full source code is available at https://bitbucket.org/4shards-blog/blog-task-frontend/.


Step 1 - Create the project and update dependencies


The following lines are simple Angular CLI commands to help you create the project on your own.


Create the project:

ng new BlogTaskFrontend --routing=true --style=scss

Update dependencies:

npm install @angular/material @angular/cdk bootstrap popper.js jquery

Whenever you wish to run the project execute:

ng serve

When you're done, you should see something like this:

Other than these steps, I have created a themes file, following the guides on the angular material site and added some basic configuration stuff here and there. You can find all these in the repository.


Step 2 - Create a task entry form


Now we face the first problem. We want the simplest task possible so that we can prepare the skeleton of our application, but won't distract us from this goal. I found that a task with only a summary is enough for our purposes. Make it a required property, but don't care for any other validation right now. We will have plenty of time to expand on this feature later.


So as a first step remove the demo template from the app.component.html. Then create a component that will handle the UI for the task form:

ng generate component components/BlogTaskCreate

Add this to the app component:

//app.component.html
<app-blog-task-create></app-blog-task-create>

Fill the BlogTaskCreateComponent class and template with the form.


The template:

//blog-task-create.component.html
<mat-card class="m-4">
  <mat-card-header>
    <mat-card-title>Create new task</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <div [formGroup]="blogTaskCreateForm">
      <mat-form-field>
        <mat-label>Summary</mat-label>
        <input matInput placeholder="Summary of task" formControlName="summary">
        <mat-error *ngIf="summaryControl.hasError('required')">
          Summary is <strong>required</strong>
        </mat-error>
      </mat-form-field>
    </div>
  </mat-card-content>
  <mat-card-footer>
    <div class="d-flex flex-row m-2">
      <button mat-flat-button color="warn" (click)="cancel()">
        <mat-icon>home</mat-icon>
        Cancel
      </button>
      <div class="flex-fill"></div>
      <button mat-flat-button color="primary" (click)="save()">
        <mat-icon>save</mat-icon>
        Add task
      </button>
    </div>
  </mat-card-footer>
</mat-card>

Some styling:

//blog-task-create.component.scss
.mat-form-field {
  display: flex;
  flex-grow: 1;
  flex-direction: row;
}

::ng-deep .mat-form-field-wrapper {
  flex-grow: 1;
}

And the class:

//blog-task-create.component.ts
import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup, RequiredValidator, Validators} from "@angular/forms";
import {MatSnackBar} from "@angular/material/snack-bar";

@Component({
  selector: 'app-blog-task-create',
  templateUrl: './blog-task-create.component.html',
  styleUrls: ['./blog-task-create.component.scss']
})
export class BlogTaskCreateComponent implements OnInit {
  blogTaskCreateForm = new FormGroup({
    summary: new FormControl(null, [Validators.required])
  });

  get summaryControl(): FormControl { 
    return this.blogTaskCreateForm.controls.summary as FormControl; 
  }

  constructor(private readonly snackBar: MatSnackBar) { }

  ngOnInit(): void {
  }

  cancel() {
    this.blogTaskCreateForm.reset();
  }

  save() {
    this.validate();
    if (!this.blogTaskCreateForm.valid) {
      this.snackBar.open('Task is invalid, cannot save!', null, {
        duration: 2000
      })
      return;
    }
    this.snackBar.open("Task saved successfully", null, {
      duration: 1000
    });
  }

  private validate() {
    this.blogTaskCreateForm.controls.summary.markAsTouched();
  }
}


When done, it should look something like this:


Intermezzo - About KISS, DRY and YAGNI


Okay, now we can create tasks. Wait, can we? When you look at the 'save' method, it doesn't do anything other than validating the input. Does it count as creating a task?


When I plan out the work that needs to be done, I keep 3 principles in mind all the time: KISS, DRY and YAGNI. When we simplified our problem to one property of one feature (summary) then we used KISS. Skipping anything related to the unknown 'save' functionality (where? how?) falls under the YAGNI. Anyways, our task was to implement a form to collect data. As long as there's no way to present it, can anyone tell - without looking at the code - whether we save it or not? And if we can't, does it matter?


Step 3 - Create a list of tasks


Let's return to our list. In the next step, we want to implement a list of tasks. Keeping the above principles in mind, as we have no other requirement than a basic list we do the following:


Generate a new component for the list:

ng generate component components/BlogTaskList

Add the list to the same page in the app component:

//app.component.html
<app-blog-task-create></app-blog-task-create>
<app-blog-task-list></app-blog-task-list>

Fill in the template:

//blog-task-list.component.html
<mat-card class="m-4">
  <mat-card-header>
    <mat-card-title>Tasks</mat-card-title>
  </mat-card-header>
  <mat-card-content>
    <p>Finished {{taskList.selectedOptions.selected.length}} tasks of {{taskList.options ? taskList.options.length : 0}}</p>
    <mat-selection-list #taskList>
      <mat-list-option *ngFor="let task of tasks | async">
        {{task}}
      </mat-list-option>
    </mat-selection-list>
  </mat-card-content>
</mat-card>

Fill in the class using some dummy data:

//blog-task-list.component.ts
import { Component, OnInit } from '@angular/core';
import {BehaviorSubject} from "rxjs";

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

  tasks = new BehaviorSubject<string[]>(["task 1", "task 2"]);

  constructor() { }

  ngOnInit(): void {
  }

}

Don't forget, we haven't defined where we want to get the data from, we just want a simple list. So now the UI should look like this:

Unfortunately, it causes an error on the javascript console. As it doesn't affect the usability yet, just ignore.


Step 3 - Bind the form to the list


As the last step, we will make sense of the form and list by binding them together. For this, we need a service that will handle the data itself, so the form can ask it to create a new task, while the list can read the list of tasks at any time.

ng generate service services/BlogTask

Fill in the service:

//blog-task.service.ts
import { Injectable } from '@angular/core';
import {BehaviorSubject, Observable} from "rxjs";
import {BlogTask} from "../model/blog-task";

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

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

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

  constructor() { }

  create(value: BlogTask): Observable<any> {
    return new Observable<boolean>(observer => {
      this.tasks$.next([...this.tasks$.value, value.summary]);
      observer.next();
    });
  }
}

Create a model for the task, so we can pass typed objects around instead of a simple string:

ng generate interface model/BlogTask

Add our only property:

//model/blog-task
export interface BlogTask {
  summary: string;
}

Modify the save method of the form, to call the service's create method:

//blog-task-create.component.ts
save() {
  this.validate();
  if (!this.blogTaskCreateForm.valid) {
    this.snackBar.open('Task is invalid, cannot save!', null, {
      duration: 2000
    })
    return;
  }
  this.taskSvc.create(this.blogTaskCreateForm.value as BlogTask).subscribe(
    () => {
      this.snackBar.open("Task saved successfully", null, {
        duration: 1000
      });
    },
    () => {
      this.snackBar.open("Saving task failed", null, {
        duration: 2000
      });
    }
  );
}

Modify the list to read from the service:

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

constructor(private taskSvc: BlogTaskService) {
  this.tasks = taskSvc.tasks;
}

Now whenever we fill in the form and hit the save button, a new element will appear in the list. In 3 simple steps and a few minutes of coding, we are done with the tasks. What usually happens at this stage is dependent on the size and workload of the team you are working with. Now we just keep it as it is, and call it a day.


Until next time some exercise:


  1. Try to add the details property to the task - both to form and list.

  2. Try to add an enumerated status property with possible values: new, open, in progress, closed. Try to change the list, so that you can change the status this way. Don't forget the counter above the list!

45 views

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