An Introduction to Angular Components

Angular components have been around since the framework’s inception, yet their proper utilization remains a common stumbling block for many developers, both novice and experienced. Throughout my career, I’ve observed a range of misuses, from completely neglecting components to incorrectly employing them in place of attribute directives – mistakes I myself have made. This article aims to bridge this knowledge gap, providing practical guidance and use cases that go beyond the explanations found in both official and unofficial documentation.

This article will delve into the correct and incorrect applications of Angular components, using illustrative examples to provide clarity on:

  • The fundamental concept of Angular components;
  • Situations that warrant the creation of distinct Angular components; and
  • Instances where creating separate Angular components is unnecessary.
Cover image for Angular components

Before exploring the appropriate use of Angular components, let’s briefly revisit the basics. Every Angular application inherently possesses at least one component, the root component. From this foundation, we have the flexibility to structure our application as we see fit. A common approach involves creating a component for each page, with each page further divided into multiple components. As a general guideline, a component should adhere to the following criteria:

  • It must have a defined class that manages data and logic; and
  • It must be linked to an HTML template responsible for presenting information to the user.

Consider an application with two pages: Upcoming tasks and Completed tasks. On the Upcoming tasks page, users can view, mark as “done,” and add tasks. Similarly, the Completed tasks page allows viewing and marking tasks as “undone.” Navigation links facilitate movement between these pages. This setup can be conceptually divided into three sections: the root component, pages, and reusable components.

Diagram of upcoming tasks and completed tasks
Example application wireframe with separate component sections colored out

The screenshot above suggests an application structure resembling this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
── myTasksApplication
├── components
│   ├── header-menu.component.ts
│   ├── header-menu.component.html
│   ├── task-list.component.ts
│   └── task-list.component.html
├── pages
│   ├── upcoming-tasks.component.ts
│   ├── upcoming-tasks.component.html
│   ├── completed-tasks.component.ts
│   └── completed-tasks.component.html
├── app.component.ts
└── app.component.html

Let’s map these component files to the elements in the wireframe:

  • header-menu.component and task-list.component are reusable components, highlighted with green borders in the wireframe;
  • upcoming-tasks.component and completed-tasks.component represent pages, indicated by yellow borders; and
  • app.component is the root component, denoted by the red border.

This structure allows for distinct logic and design for each component. With two pages utilizing a single task-list.component, a question arises: how do we control the type of data displayed on each page? Angular addresses this through Input and Output variables, configurable during component creation.

Input Variables

Input variables facilitate data transfer from a parent component to its child. In our example, task-list.component could have two input parameters: tasks and listType. tasks would be a string array, displaying each string in a separate row, while listType would be either upcoming or completed, determining whether the checkbox is checked. A simplified code snippet illustrates this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// task-list.component.ts

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

@Component({
  selector: 'app-task-list',
  templateUrl: 'task-list.component.html'
})
export class TaskListComponent  {
  @Input() tasks: string[] = []; // List of tasks which should be displayed.
  @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list.

  constructor() { }
}

Output Variables

Similar to input variables, output variables enable data exchange between components, but in the opposite direction – from child to parent. For instance, task-list.component could have an output variable itemChecked, notifying the parent component about checkbox state changes. Output variables must be event emitters. Here’s how the component might look with an output variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// task-list.component.ts

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

@Component({
  selector: 'app-task-list',
  templateUrl: 'task-list.component.html'
})
export class TaskListComponent  {
  @Input() tasks: string[] = []; // List of tasks which should be displayed.
  @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list.
  @Output() itemChecked: EventEmitter<boolean> = new EventEmitter();
  @Output() tasksChange: EventEmitter<string[]> = new EventEmitter();

  constructor() { }

  /**
   * Is called when an item from the list is checked.
   * @param selected---Value which indicates if the item is selected or deselected.
   */
  onItemCheck(selected: boolean) {
    this.itemChecked.emit(selected);
  }

  /**
   * Is called when task list is changed.
   * @param changedTasks---Changed task list value, which should be sent to the parent component.
   */
  onTasksChanged(changedTasks: string[]) {
    this.taskChange.emit(changedTasks);
  }
}
Possible task-list.component.ts content after adding output variables

Using Child Components and Binding Variables

Let’s examine how to utilize this component within its parent and implement various variable binding methods. Angular offers two primary ways to bind input variables: one-way binding, where the property is enclosed in square brackets [], and two-way binding, using both square and round brackets [()]. The following example demonstrates these different data passing mechanisms between components.

1
2
<h1>Upcoming Tasks</h1>
<app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list> 
Possible upcoming-tasks.component.html content

Let’s break down each parameter:

  • The tasks parameter utilizes two-way binding, meaning any changes to it within the child component will be reflected in the parent component’s upcomingTasks variable. This requires an output parameter following the “[inputParameterName]Change” convention, in this case, tasksChange.
  • The listType parameter employs one-way binding, allowing changes within the child component but not propagating them back to the parent. Assigning 'upcoming' directly to a parameter within the child component would have the same effect.
  • The itemChecked parameter acts as a listener function, triggered whenever onItemCheck is executed in task-list.component. A checked item results in $event holding true, while an unchecked item yields false.

Angular provides a robust system for inter-component communication. Use this system judiciously to avoid overcomplication.

When to Create a Separate Angular Component

While Angular components are powerful tools, their use should be strategic.

Diagram of Angular components in action
Make wise use of Angular components

So, when is it appropriate to create separate Angular components?

  • Always create a separate component for reusable elements, like our task-list.component, referred to as reusable components.
  • Consider creating a component if it enhances the parent component’s readability and facilitates additional test coverage. These are known as code organization components.
  • Always create a component for infrequently updated page sections to boost performance through change detection strategy optimization. These are called optimization components.

These guidelines help determine the necessity of a new component and define its role within the application. Ideally, you should have a clear purpose in mind when creating a component.

Having explored reusable components, let’s examine code organization components. Imagine a registration form with a lengthy “Terms and Conditions” section at the bottom. This legal text can significantly increase the HTML template’s size, hindering readability.

Initially, we have a single registration.component containing everything, including the form and the terms and conditions.

1
2
3
4
5
6
7
8
9
<h2>Registration</h2>
<label for="username">Username</label><br />
<input type="text" name="username" id="username" [(ngModel)]="username" /><br />
<label for="password">Password</label><br />
<input type="password" name="password" id="password" [(ngModel)]="password" /><br />
<div class="terms-and-conditions-box">
   Text with very long terms and conditions.
</div>
<button (click)="onRegistrate()">Registrate</button>
Initial state before separating </code>registration.component</code> into multiple components

The template appears concise now, but imagine replacing “Text with very long terms and conditions” with actual text exceeding 1000 words – editing the file would become cumbersome. We can address this by creating a dedicated terms-and-conditions.component to house everything related to the terms.

Here’s the HTML for terms-and-conditions.component:

1
2
3
<div class="terms-and-conditions-box">
   Text with very long terms and conditions.
</div>
Newly created </code>terms-and-conditions.component</code> HTML template

Now, we can modify registration.component to utilize terms-and-conditions.component:

1
2
3
4
5
6
7
<h2>Registration</h2>
<label for="username">Username</label><br />
<input type="text" name="username" id="username" [(ngModel)]="username" /><br />
<label for="password">Password</label><br />
<input type="password" name="password" id="password" [(ngModel)]="password" /><br />
<app-terms-and-conditions></app-terms-and-conditions>
<button (click)="onRegistrate()">Registrate</button>
Updated </code>registration.component.ts</code> template with code organization component

By extracting this section, we’ve significantly reduced the size and improved the readability of registration.component. While this example focuses on template changes, the same principle applies to component logic.

For optimization components, I recommend exploring the provided resource for comprehensive information on change detection and its applications, including the OnPush strategy. While not always necessary, strategic implementation can yield performance gains by minimizing unnecessary checks.

While components are valuable, there are instances where their creation should be avoided.

When to Avoid Creation of a Separate Angular Component

Despite the advantages of components, their overuse can be detrimental.

Illustration of too many components
Too many components will slow you down.

Let’s outline the scenarios where creating a separate component is not recommended:

  • Avoid creating components for DOM manipulations. Utilize attribute directives for such tasks.
  • Avoid creating components if they complicate the codebase, contradicting the purpose of code organization components.

Let’s illustrate these scenarios with examples. Consider a button that logs a message when clicked. One might mistakenly create a dedicated component for this functionality, housing the button and its action.

Here’s the incorrect approach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// log-button.component.ts

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

@Component({
  selector: 'app-log-button',
  templateUrl: 'log-button.component.html'
})
export class LogButtonComponent  {
  @Input() name: string; // Name of the button.
  @Output() buttonClicked: EventEmitter<boolean> = new EventEmitter();

  constructor() { }

  /**
   * Is called when button is clicked.
   * @param clicked - Value which indicates if the button was clicked.
   */
  onButtonClick(clicked: boolean) {
    console.log('I just clicked a button on this website');
    this.buttonClicked.emit(clicked);
  }
}
Logic of incorrect component log-button.component.ts

The corresponding HTML view would look like this:

1
<button (click)="onButtonClick(true)">{{ name }}</button>
Template of incorrect component log-button.component.html

While functional, this approach is not ideal. A more appropriate solution involves using directives, which are more concise and allow applying the functionality to any element, not just buttons.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { Directive } from '@angular/core';

@Directive({
    selector: "[logButton]",
    hostListeners: {
        'click': 'onButtonClick()',
    },
})
class LogButton {
    constructor() {}
  /**
   * Fired when element is clicked.
   */ 
  onButtonClick() {
   console.log('I just clicked a button on this website');
   }
}
logButton directive, which can be assigned to any element

With the directive in place, we can apply it to any element across the application. Let’s revisit our registration.component:

1
2
3
4
5
6
7
<h2>Registration</h2>
<label for="username">Username</label><br />
<input type="text" name="username" id="username" [(ngModel)]="username" /><br />
<label for="password">Password</label><br />
<input type="password" name="password" id="password" [(ngModel)]="password" /><br />
<app-terms-and-conditions></app-terms-and-conditions>
<button (click)="onRegistrate()" logButton>Registrate</button>
logButton directive used on the registration forms button

Now, let’s address the second scenario, where component creation contradicts code organization principles. In our registration.component example, creating separate components for labels and input fields with numerous input parameters would be counterproductive.

Consider this example of bad practice:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// form-input-with-label.component.ts

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

@Component({
  selector: 'app-form-input-with-label',
  templateUrl: 'form-input-with-label.component.html'
})
export class FormInputWithLabelComponent  {
  @Input() name: string; // Name of the field
  @Input() id: string; // Id of the field
  @Input() label: string; // Label of the field
  @Input() type: 'text' | 'password'; // Type of the field
  @Input() model: any; // Model of the field

  constructor() { }

}
Logic of the form-input-with-label.component

And its corresponding view:

1
2
<label for="{{ id }}">{{ label }}</label><br />
<input type="{{ type }}" name="{{ name }}" id="{{ id }}" [(ngModel)]="model" /><br />
View of the form-input-with-label.component

While this reduces code within registration.component, it unnecessarily increases overall complexity and hinders readability.

Next Steps: Angular Components 102?

In essence: Embrace components, but with a clear purpose and understanding of their impact. The scenarios discussed represent common situations, but your context might demand unique solutions. Hopefully, this article equips you to make informed decisions.

For a deeper dive into Angular’s change detection strategies and the OnPush strategy, I recommend exploring the suggested resource. This knowledge complements your understanding of components and can significantly enhance application performance.

Since components are just one facet of Angular directives, familiarizing yourself with attribute directives and structural directives is highly beneficial. A comprehensive grasp of directives empowers developers to write cleaner and more efficient code.

Licensed under CC BY-NC-SA 4.0