Working with Components in Angular
We discussed a bunch of different decorators in the last lesson, but the
decorator that we will be using far more than any other is the @Component
decorator to create custom components. We are going to spend this lesson
discussing some core concepts of working with components.
We can think of components as the fundamental building blocks of an Angular application. We have had a glimpse of this already — our entire application is built of components within components within components.
A Basic Component
We have already built some basic components that look like this:
import { Component } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, Josh!</p> `,})export class WelcomeComponent {}and these components can then be added to another components template like this:
import { Component } from '@angular/core';import { WelcomeComponent } from './ui/welcome.component';
@Component({ selector: 'app-home', template: ` <app-welcome /> <p>I am the home component</p> `, imports: [WelcomeComponent]})export class HomeComponent {}or they can be routed to and displayed with a <router-outlet>.
This example is about as basic as a component will get. The reason we might create a component like this is just to help modularise our application — all we are really doing is displaying a template and binding some simple data. We will talk more about architecture concepts later, but the general idea is that it is better to have lots of components each doing some small/targeted thing, rather than having components that try to do too much.
However, there are two fundamental concepts of Angular components we are missing here that help control how parent and child components relate to each other: input and output.
If we put ComponentB inside of the template of ComponentA:
@Component({ selector: 'component-a', template: `<component-b></component-b>`})export class ComponentA {}Then we would say that ComponentA is the parent and ComponentB is the
child. The key idea is that a parent component can communicate with
a child component by providing it with an input. A child component
can communicate with a parent component by providing it with an output.
Let’s investigate how inputs and outputs work in more detail.
input and @Input
This is one of those features that can be implemented in both the new signal
based way, or in the “old” way which was based on decorators. We will look at an
example of both.
We are going to switch back to the more real example of our WelcomeComponent
which currently lives within the template of the HomeComponent.
Now let’s imagine that we want to display a greeting for a particular user’s
name, not just Josh. As we will discuss later, we generally don’t want our
child/dumb components to have to deal with any complex business logic or data
fetching. So, what we might want to do is have our parent component provide
the child component with an input so that it knows what name to display:
import { Component } from '@angular/core';import { WelcomeComponent } from './ui/welcome.component';
@Component({ selector: 'app-home', template: ` <app-welcome [name]="user.name" /> <p>I am the home component</p> `, imports: [WelcomeComponent],})export class HomeComponent { user = { name: 'Josh', };}We are still just using a static value of Josh here, but imagine that our
HomeComponent might pull this user value in from some kind of UserService
that will return the logged in user.
At the moment, this will just cause an error:
error NG8002: Can't bind to 'name' since it isn't a known property of 'app-welcome'.In order to accept this input, we will need to make some changes to the
WelcomeComponent. First, let’s take a look at the “old” way of doing that:
import { Component, Input } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name }}!</p> `,})export class WelcomeComponent { @Input() name = 'friend';}We have set up a class member called name but we are also decorating this
with the @Input decorator. This tells Angular that this class member is an
input, and it will allow our parent component to bind to the value. The
error should be gone now.
With the input set up, our WelcomeComponent can now just treat this input like a normal value:
<p>Hi, {{ name }}!</p>If the parent component changes this input, then our child component will
automatically reflect that change. If we don’t provide any name input to the
component, e.g:
@Component({ selector: 'app-home', template: ` <app-welcome></app-welcome> <p>I am the home component</p> `,})Then our child component will use the default value we supplied, which was
friend, e.g:
Hi, friend!Now let’s see how we would approach this with the new signal based approach.
There are just a few minor differences, first we would define the input like
this:
import { Component, input } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name() }}!</p> `,})export class WelcomeComponent { name = input('friend');}Instead of using a decorator we now use this input function which is also
imported from @angular/core.
There is also some minor differences in using that input within the component:
<p>Hi, {{ name() }}!</p>When we use input to define an input, that input will be provided to the
component as a signal. We are going to dive more into signals in the next
lesson, but what is going on here is that in order to access the value of a
signal we need to call it as a function. Calling name() will give us the value
of the name signal.
output and @Output
Now we know how to have our parent component communicate with the child component, but what about the other way around. This also has an “old” and “new” way.
Let’s suppose that in our WelcomeComponent we are going to display one of
those dreaded cookie notices. We want to add a button that the user can click to
accept the cookies, and when this happens we need to record on our server that
the user has clicked this button.
Again, ideally we want our child components to be “dumb”. We don’t really want them to have to deal with making calls to services to send HTTP requests, because that requires that they know things about how the application works. A “dumb” component should not know anything about what is happening outside of the component itself. It has its inputs, its outputs, it displays some stuff, and that’s about it (we will talk about this more later).
What we want to happen is that when the user clicks the button, we want our parent component to handle making the appropriate request to a service to indicate that the cookies were accepted. That means we need a way for our child component to notify the parent when the button was clicked.
We can do this with an @Output:
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name }}!</p> <p>Do you accept the yummy cookies?</p> <button (click)="cookiesAccepted.emit(true)">I do!</button> `,})export class WelcomeComponent { @Input() name = 'friend'; @Output() cookiesAccepted = new EventEmitter<boolean>();}This sets up an EventEmitter that we can utilise within our child component.
When something of interest happens we can call the emit method on it:
cookiesAccepted.emit(true)Since this class member is marked with the @Output decorator, our parent
component will be able to bind to it just like with the input:
import { Component } from '@angular/core';import { WelcomeComponent } from "./ui/welcome.component";
@Component({ selector: 'app-home', template: ` <app-welcome [name]="user.name" (cookiesAccepted)="handleCookies()" /> <p>I am the home component</p> `, imports: [WelcomeComponent]})export class HomeComponent { user = { name: 'Josh', };
handleCookies() { console.log('do something'); // call some service }}But this time, we are binding to an event, just like we do for a standard
(click). Whenever we trigger that emit method, this event will be triggered,
and we can handle the result. In this case, we are calling a method that invokes
the appropriate injectable service.
With the new output function, all of this works almost exactly the same. You
might expect that output is a “signal based” way of creating an output.
However, outputs are events, and signals are not a great mechanism for dealing
with events.
The “new” way actually still just uses an EventEmitter under
the hood but instead we define the output like this:
import { Component, input, output } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name() }}!</p> <p>Do you accept the yummy cookies?</p> <button (click)="cookiesAccepted.emit(true)">I do!</button> `,})export class WelcomeComponent { name = input('friend'); cookiesAccepted = output<boolean>();}The primary benefit of making this change is that it makes output look more
congruent with input even though output is not actually using signals, and
it is a bit more ergonomic as you don’t have to manually instantiate and import
EventEmitter.
Communicating without Inputs and Outputs
This concept of parent/child relationships and inputs and outputs is important. However, there are other ways for our components to communicate as well. An input or an output works well when a component is a direct child of another component, but what about a situation where our components are siblings? That is to say one component is not within the other component.
A good example of this is the home page and settings page example we
looked at earlier. These components are routed to using the
<router-outlet>. The home page is not within the settings page, and
the settings page is not within the home page.
We may still need to communicate between these two components. Perhaps they need to share the same set of data. In these cases, we will typically use an injectable service to share data between both of the pages. There are other methods we can use as well that we will get into later.