Getting Started with Angular: Inputs & Outputs

drownedintech

Steven Boyd-Thompson

Posted on June 29, 2023

Getting Started with Angular: Inputs & Outputs

In the last post, we covered the creation and usage of components in Angular. When we left it we had created some standalone components, but they had a few problems.

In this post, we’re going to cover how we can use @Input and @Output decorators to communicate between parent and child components. At the end of the last post, I said we would also be covering some of the built-in directives and elements as well. To be honest, covering just those two decorators took more than I expected so I’ll be covering the rest in subsequent posts.

The starting code for this post can be found here:
https://github.com/drownedintech/getting-started-with-angular/tree/posts/inputs-outputs

💡 If you’ve been following along with these posts you will notice some differences from the end of the last post. The product list page has been restyled and the product page has been tidied up. Functionally it’s the same though.

@Input

To start we’re going to take a look at our product image component. Last time we created a new standalone component that we could share across our application, but everywhere it was used ended up showing the same image. We'll fix this now with @Input.

The @Input decorator allows us to specify a field on a component that can receive a value from a parent component. We’ll add an input to product-image.component.ts called imageUrl:

@Input()
public imageUrl: string | undefined;
Enter fullscreen mode Exit fullscreen mode

Now we can start using this for our image source in product-image.component.html:

<img src="{{ imageUrl }}" />
Enter fullscreen mode Exit fullscreen mode

💡 I’ve shown a feature above that we haven’t previously discussed, template binding with double curly brackets ({{ }}). This allows us to use properties from our .ts file in our templates. There are a few different methods for achieving this, and we’ll cover more of them in the future. For now, just know that you can use {{ }} to access a value to provide content or as inputs to HTML attributes (whether base HTML or your own). One caveat I will mention is to avoid binding to accessors or methods. By default, these will get rerun on every change detection cycle regardless of any changes to the value. Depending on the logic being run this can have severe performance implications.

Now we’ve got our product image component set up to accept the image URL, let’s use it. We’ll now go to product-list.component.ts and add a new imageUrl attribute to our app-product-image elements:

<app-product-image mat-card-image class="product-image" [imageUrl]="'../../assets/images/taco.png'" />
Enter fullscreen mode Exit fullscreen mode

There are 3 images available in our assets for this, so update each product with a different one to see our new input in action. We can now see our product list has a different image for each product. However, if we go to the product page we can see we’ve lost our image there.

Adding the new attribute here is easy enough, but in a large application, it would be easy for a missing attribute to go unnoticed. Previously we would have had to just suck it up and put extra checks in place to ensure we had all the data we needed. The Angular team has got our backs though, and with v16 we now have the ability to set an input as required. Let’s do that now, in product-image.component.ts we change our @Input decorator to required:

@Input({ required: true })
public imageUrl: string | undefined;
Enter fullscreen mode Exit fullscreen mode

When we build our application we’ll now get the following error:

Error: src/app/product/product.component.html:2:5 - error NG8008: Required input 'imageUrl' from component ProductImageComponent must be specified.
Enter fullscreen mode Exit fullscreen mode

We can now rest easy knowing that our app with fail to build if we’ve missed something. We’re going to fix this in our product component, but first we’re going to use this to cover another option in inputs: transform.

💡 Before we go over transform, we’re going to need to check our version of Angular. Everything I’ve covered so far is available in Angular v16. The transform configuration requires Angular v16.1. So, check your Angular version in package.json, if it’s below 16.1 you’ll need to run ng update @angular/core@16.1 @angular/cli@16.1.

Let’s start by adding the imageUrl attribute to our product component. Go to product.component.html and update the image element to look like this:

<app-product-image [imageUrl]="undefined" />
Enter fullscreen mode Exit fullscreen mode

Since we set our data type to string | undefined this won’t cause an error when building, but if we run our app nothing will be shown. This example might seem a little forced, but that’s because we’re hard-coding our image URLs. In a real-world scenario it’s perfectly reasonable to expect a placeholder to be shown when an image hasn’t been provided, and that’s what we’re going to do with transform.

So, what does it do? The transform configuration takes a function that will be applied to the value being set on the input. Previously, we would have needed to implement this ourselves with getters/setters and possibly a private field. This all gets tidied up by being able to apply a transformation. We’ll add it now by going back to product-image.component.ts and changing our input to:

@Input({
  required: true,
  transform: (value: string | undefined) => value ?? '/assets/images/image-placeholder.png'
})
public imageUrl: string | undefined;
Enter fullscreen mode Exit fullscreen mode

You will need an image to point it to. If you don’t have anything available just change the image-placeholder.png to one of the images we use elsewhere, the effect is the same. Once we’ve got our image in place we can see the product page is now showing it. Here we’ve just used a null-coalescing operator to return either the actual value if it has one or the placeholder URL if not. Now we don’t need to rely on actually getting a value passed in.

The final configuration we can pass to our input is alias. This gives us the option to use one name for our variable within the component but have any parent components pass it in as something else. We’ll update our input decorator one final time:

@Input({
  required: true,
  transform: (value: string | undefined) => value ?? '/assets/images/image-placeholder.png',
  alias: 'ThisIsAnAlias'
})
public imageUrl: string | undefined;
Enter fullscreen mode Exit fullscreen mode

When we now try another build we’ll see errors start to show up. This is because we’ve passed imageUrl when using this component, but that’s no longer valid. We now have to pass the image URL in as an attribute named ThisIsAnAlias. I won’t be keeping the alias configuration, but it doesn’t hurt to know it’s available.

That covers getting data into our components, let’s move on to sending data the other way with outputs.

@Output

Outputs are the opposite of inputs. Where inputs are used by the parent component to pass relevant data down to the child, outputs are used by the child component to notify the parent of changes. All outputs must be typed as EventEmitter, but they can be used to just notify (by specifying void as the generic type) or to pass data to the parent.

To demonstrate this we’re going to use our add-to-basket component. Here we’re going to add an output that fires whenever a button is clicked and sends the number of times it has been clicked. We’ll start by going to add-to-basket.component.ts and adding the following code:

@Output()
public itemAdded = new EventEmitter<number>();

private currentAmount: number = 0;

public addItemToBasket(): void {
  this.itemAdded.emit(++this.currentAmount);
}
Enter fullscreen mode Exit fullscreen mode

We now have an output (itemAdded), indicated by the @Output decorator. We’ve also added a method that will send values through the output with the emit call. And finally, we’ve got a variable holding the current count. Now we need something to call this, we’ll go and change add-to-basket.component.html:

<button mat-button color="primary" (click)="addItemToBasket()">Add to Basket</button>

<button mat-icon-button color="primary" (click)="addItemToBasket()">
    <mat-icon>add_shopping_cart</mat-icon>
</button>
Enter fullscreen mode Exit fullscreen mode

Now we’ve got everything we need to make use of our output. We’ll move over to our product component to add some handling and see this in action. First, we’ll open product.component.ts and add the following:

public amountInBasket: number = 0;

public handleAddedToBasket(currentAmount: number): void {
  this.amountInBasket = currentAmount;
}
Enter fullscreen mode Exit fullscreen mode

And in product.component.html we’ll change add to basket component usage:

<div class="buttons">
    <app-add-to-basket (itemAdded)="handleAddedToBasket($event)" />

    <p>Amount added: {{ amountInBasket }}</p>
</div>
Enter fullscreen mode Exit fullscreen mode

This will now handle the output from our add-to-basket component, keep track of the last value, and display that in our template. Now we can try this on our product page and see the number incrementing each time we click add to basket.

You’ll notice that each time we’re using our add to basket component we’re getting 2 buttons. We’ll tackle this next using our built-in directives, starting with ngIf.

💡 The only configuration option provided by the @Output decorator is alias. This works the same way as the alias operator for @Input, so we won’t cover it again.

What’s next?

While inputs and outputs have helped us add communication between our components, we’ve still got problems. Two buttons for our add-to basket? Repeating code for our product list? I know, I know. There are ways to deal with all of this, and in the next post, we’ll be covering the built-in directives and elements that will help us do just that.

For now, please bear with me.

The final code for this post can be found here:
https://github.com/drownedintech/getting-started-with-angular/tree/posts/inputs-outputs-complete

💖 💪 🙅 🚩
drownedintech
Steven Boyd-Thompson

Posted on June 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related