Mastering Angular Components
上QQ阅读APP看书,第一时间看更新

The right size of components

Our task list is displayed correctly and the code we used to achieve this looks quite okay. However, if we want to follow a better approach for composition, we should rethink the design of our task list component. If we draw a line at enlisting the task list's responsibilities, we would come up with things such as listing tasks, adding new tasks to the list, sorting or filtering the task list; however, operations are not performed on an individual task itself. Also, rendering the task itself falls outside of the responsibilities of the task list. The task list component should only serve as a container for tasks.

If we look at our code again, we will see that we're violating the single responsibility principle and rendered the whole task body within our task list component. Let's take a look at how we can fix this by increasing the granularity of our components.

The goal now is to do a code refactoring exercise, also known as extraction. We are pulling our task's relevant template out of the task list template and creating a new component that encapsulates the tasks.

Let's use the Angular CLI to create a new task component. Open a command line and enter the root folder of our application. Execute the necessary code to create the task component:

ng generate component --spec false -ve none tasks/task

This will generate a new folder which includes all the code for our new task component. Now, let's open the HTML template on the path src/app/tasks/task/task.component.html and change the content to represent a single task:

<input type="checkbox" [checked]="task.done"> 
<div>{{task.title}}</div> 

The content of our new task.component.html file is pretty much the same as what we already have within our task list component. However, within the newly created task component, we're only concerned about what a task looks like and not about the whole list of tasks.

Let's change the task component TypeScript file located on the path src/app/tasks/task/task.component.ts:

import {Component, Input, ViewEncapsulation} from '@angular/core'; 
 
@Component({ 
  selector: 'mac-task',
  templateUrl: './task.component.html', 
  encapsulation: ViewEncapsulation.None 
}) 
export class TaskComponent {
  @Input() task: any; 
}

In the previous chapter of this book, we spoke about encapsulation and the preconditions to establish a clean encapsulation for UI components. One of these preconditions is the possibility to design proper interfaces in and out of the component. Such input and output methods are necessary to make the component work within compositions. That's how a component will receive and publish information.

As you can see from our task component implementation, we are now building such an interface using the @Input decorator on a class instance property. In order to use this decorator, we will first need to import it from the angular core module.

Input properties in Angular allow us to bind expressions in our templates to class instance properties on our components. This way, we can pass data from the outside of the component to the component inside, using the component's template. This can be thought of as an example of one-way binding, from the view of a parent component to the child component instance.

If we're using property binding on a regular DOM property, Angular will create a binding of the expression directly to the element's DOM property. We're using such a type of binding to bind the task completed flag to the checked property of the checkbox's input element:

 

The last missing piece so that we can use our newly created task component is the modification of the existing template of the task list.

We include the task component within our task list template by using a <mac-task> element, as specified in the selector within our task component. Also, we need to create an input binding on the task component to pass the task object from the current NgFor iteration to the task input of the task component. We need to replace all the existing content in the src/app/tasks/task-list/task-list.component.html file with the following lines of code:

<mac-task *ngFor="let task of tasks"
          [task]="task"></mac-task> 

Congratulations! You've successfully refactored your task list by extracting the task into its own component and established a clean composition, encapsulation, and single responsibility. Also, we can now say that our task list is a composition of tasks.

If you think about maintainability and re-usability, this was actually a very important step in the process of building our application. You should constantly look out for such composition opportunities, and if you feel something could be arranged into multiple subcomponents, you should probably go for it. Of course, you can also overdo this. There's simply no golden rule to determine what granularity of composition is the right one. 

The right granularity of composition and encapsulation for a component architecture always depends on the context. My personal tip here is to use known principles from OOP, such as single responsibility, to lay the groundwork for a good design of your component tree. Always make sure your components are only doing things that they are supposed to do as their names suggest. A task list has the responsibility of listing tasks and providing some filters or other controls for the list. The responsibility of operating on individual task data and rendering the necessary view clearly belongs to a task component and not the task list.