Understanding Component Architecture: Refactoring an Angular App

In part one of this series, we learned how to get our Todo application up and running and deploy it to GitHub pages. This worked just fine, but unfortunately the whole app was crammed into a single component. In this article, we’ll examine a more modular component architecture. We’ll look at how to break this single component into a structured tree of smaller components that are easier to understand, re-use and maintain.

You don’t need to have followed part one of this tutorial, for part two to make sense. You can simply grab a copy of our repo, checkout the code from part one, and use that as a starting point. This is explained in more detail below.

--ADVERTISEMENT--

A Quick Recap

So let’s look at what we covered in part one in slightly more detail. We learned how to:

initialize our Todo application using the Angular CLI

create a Todo class to represent individual todos

create a TodoDataService service to create, update and remove todos

use the AppComponent component to display the user interface

deploy our application to GitHub pages.

The application architecture of part 1 looked like this:

The components we discussed are marked with a red border.

In this second article, we’ll delegate some of the work that AppComponent is doing to smaller components that are easier to understand, re-use and maintain.

We’ll create:

a TodoListComponent to display a list of todos

a TodoListItemComponent to display a single todo

a TodoListHeaderComponent to create a new todo

a TodoListFooterComponent to show how many todos are left.

By the end of this article, you’ll understand:

the basics of Angular component architecture

how you can pass data into a component using property bindings

how you can listen for events emitted by a component using event listeners

why it is a good practice to split components into smaller reusable components

the difference between smart and dumb components and why keeping components dumb is a good practice.

So let’s get started!

Up and Running

The first thing you’ll need to follow along with this article is the latest version of the Angular CLI. You can install this with the following command:

npm install -g @angular/cli@latest

If you need to remove a previous version of of the Angular CLI, here’s how:

After that you’ll need a copy of the code from part one. This is available at https://github.com/sitepoint-editors/angular-todo-app. Each article in this series has a corresponding tag in the repository so you can switch back and forth between the different states of the application.

The code that we ended with in part one and that we start with in this article is tagged as part-1. The code that we end this article with is tagged as part-2.

You can think of tags like an alias to a specific commit id. You can switch between them using git checkout. You can read more on that here.

So, to get up and running (the the latest version of the Angular CLI installed) we would do:

Although our AppComponent works fine technically, keeping all code in one big component does not scale well and is not recommended.

Adding more features to our Todo application would make the AppComponent larger and more complex, making it harder to understand and maintain.

Therefore, it’s recommended to delegate functionality to smaller components. Ideally the smaller components should be configurable so that we don’t have to rewrite their code when the business logic changes.

For example, in part three of this series we’ll update the TodoDataService to communicate with a REST API, and we want to make sure that we won’t have to change any of the smaller components when we refactor the TodoDataService.

If we look at the AppComponent template, we can extract its underlying structure as:

Adding a component to the module declarations is required to make sure that all view templates in the module can use it the component. Angular CLI conveniently added TodoListHeaderComponent for us so we don’t have to add it manually.

If TodoListHeaderComponent was not in the declarations and we used it in a view template, Angular would throw the following error:

Error: Uncaught (in promise): Error: Template parse errors:
'app-todo-list-header' is not a known element:
1. If 'app-todo-list-header' is an Angular component, then verify that it is part of this module.
2. If 'app-todo-list-header' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.

Every time we call add.emit(value) in TodoListHeaderComponent, the onAddTodo($event) handler will be called and $event will be equal to value.

This decouples our TodoListHeaderComponent from the TodoDataService and allows the parent component to decide what needs to happen when a new todo is created.

When we update the TodoDataService to communicate with a REST API in part three, we won’t have to worry about the TodoListHeaderComponent because it’s not even aware that the TodoDataService exists.

Smart vs Dumb Components

You may have already heard of smart and dumb components. Decoupling the TodoListHeaderComponent from the TodoDataService makes the TodoListHeaderComponent a dumb component. A dumb component is not aware of what happens outside itself. It only receives input via property bindings and only emits output data as events.

Using smart and dumb components is a good practice. It greatly improves separation of concerns, making your application easier to understand and maintain. If your database or back-end API changes, you don’t have to worry about your dumb components. It also makes your dumb components more flexible, allowing you to reuse them more easily in different situations. If your application needs the same component twice, where one time it needs to write to a back-end database and another time it needs to write to an in-memory database, a dumb component allows you to accomplish exactly that.

So now that we’ve created our TodoListHeaderComponent, let’s update our AppComponent template to use it:

Notice that we’ve introduced a TodoListItemComponent which doesn’t yet exist. However, adding it to the template already allows us to explore what API the TodoListItemComponent should offer. This makes it easier for us to write the TodoListItemComponent in the next section, because we now know what input and outputs we would expect TodoListItemComponent to have.

We pass in the todo item through the todo property using the [todo]input property syntax and attach event handlers to the events we would expect the TodoListItemComponent to emit, such as the toggleComplete event and the remove event.

Let’s open up src/app/todo-list/todo-list.component.ts and add the logic we need for our view template:

To further demonstrate the difference between smart and dumb components, we’ll also make the TodoListComponent a dumb component.

First we define an input propertytodos by marking it with the @Input() decorator. This allows us to inject the todos from the parent component.

Next, we define two output events, remove and toggleComplete, using the @Output() decorator. Notice how we set their type to EventEmitter<Todo> and assign them each a new EventEmitter instance.

The EventEmitter<Todo> type annotation is a TypeScript generic that tells TypeScript that both remove and toggleComplete are EventEmitter instances and that the values they emit are a Todo instance.

Finally, we define the onToggleTodoComplete(todo) and onRemoveTodo(todo) event handlers that we specified in our view using (toggleComplete)="onToggleTodoComplete($event)" and (remove)="onRemoveTodo($event)".

Notice how we use $event as the argument name in the view template and todo as the parameter name in the method definition. To access the payload (emitted value) of an event in an Angular template, we must always use $event as the argument name.

So by specifying (toggleComplete)="onToggleTodoComplete($event)" in our view template, we tell Angular to use the event payload as the first argument when calling the onToggleTodoComplete method, which will match the first parameter of the onToggleTodoComplete method, namely todo.

We know that the payload will be a todo instance, so we define the onToggleTodoComplete method as onToggleTodoComplete(todo: Todo), making our code easier to read, understand and maintain.

Finally, we define our event handlers to also emit a toggleComplete and remove event when they receive an incoming payload and specify the todo as the event payload.

In essence, we let TodoListComponent bubble up the events from its child TodoListItemComponent instances.

This allows us to handle the business logic outside of TodoListComponent, keeping TodoListComponentdumb, flexible and lightweight.

If we try to run our application at this stage, Angular will throw an error:

Unhandled Promise rejection: Template parse errors:
Can't bind to 'todo' since it isn't a known property of 'app-todo-list-item'.
1. If 'app-todo-list-item' is an Angular component and it has 'todo' input, then verify that it is part of this module.
2. If 'app-todo-list-item' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message.

We don’t have to change anything to the markup, but we do have to make sure the events are handled properly, so let’s add the necessary code our TodoListItemComponent in src/app/todo-list-item/todo-list-item.component.ts:

Notice how we don’t actually update or remove data. We merely emit events from the TodoListItemComponent when a user clicks a link to complete or remove a todo, making our TodoListItemComponent also a dumb component.

Remember how we attached event handlers to these events in the TodoListComponent template:

The TodoListComponent then simply re-emits the events from TodoListItemComponent.

Bubbling up events from TodoListItemComponent through TodoListComponent allows us to keep both components dumb and ensures that we don’t have to update them when we refactor the TodoDataService to communicate with a REST API in part three of this series.

How cool is that!

Before we move on, let’s update our AppComponent template to use our new TodoListComponent:

Services registered in AppComponent are only available to AppComponent and its component tree. Services registered in AppModule are available to all components in the entire application.

If our Todo application would grow and introduce lazy loaded modules at some point, the lazy loaded modules would not be able to access the TodoDataService, because TodoDataService would only be available to AppComponent and its component tree and not within the entire application.

Therefore we remove TodoDataService as a provider in the AppComponent: