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 responsibilities, we would come up with things such as listing tasks, adding new tasks to the list, and sorting and 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 rendering 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 the encapsulation.
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.
For this, we need to create a new sub folder within the task-list
folder called task
. Within this folder, we will create a template file with the name task.html
:
<input type="checkbox" [checked]="task.done"> <div class="task__title">{{task.title}}</div>
The content of our new task.html
file is pretty much the same as what we already have within our task-list.html
template. The only difference is that we will now refer to a new model called task
.
Now, within the task
folder, let's create the JavaScript file, task.js
, which will contain the controller class of our component:
import {Component, Input, ViewEncapsulation} from '@angular/core'; import template from './task.html!text'; @Component({ selector: 'ngc-task', host: { class: 'task' }, template, encapsulation: ViewEncapsulation.None }) export class Task { // Our task model can be attached on the host within the view @Input() task; }
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
annotation on a class instance field. In order to use this annotation, we will first need to import it from the angular core module.
Input properties in Angular allow us to bind the expressions in our templates to class instance fields on our components. This way, we can pass data from the outside of the component to the component inside, using the components template. This can be thought of as an example of one-way binding, from the view to the component.
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:
Usage |
Description |
---|---|
|
This allows us to bind the Angular assumes that the attribute of the element has the same name as that of the |
|
You can also override the name of the attribute that should be mapped to this input. Here, the |
The last missing piece to 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 an <ngc-task>
element, as specified in the selector within our task component. Also, we create a property binding on the task element. There, we 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 task.html
file with the following lines of code:
<ngc-task *ngFor="let task of tasks" [task]="task"></ngc-task>
In order to make our task-list
component recognize the task component element, we need to add it to the task-list
component's directives
property within the task-list.js
file:
... import {Task} from './task/task'; @Component({ ... directives: [Task] }) ...
Congratulations! You've successfully refactored your task list by extracting the task into its own component and have established a clean encapsulation. Also, we can now say that our task list is a composition of tasks.
If you think about maintainability and reusability, this was actually a very important step in the process of building our application. You should constantly look out for such encapsulation 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 encapsulation is the right one.
The right granularity of 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.
In this building block, we cleaned up our component tree and established clean encapsulation using subcomponents. Then, we set up the interfaces provided by Angular using input bindings. We performed these actions by following the ensuing steps:
task-list
component.