Updated on

Data reaches a child Angular component through the @Input decorator and comes back through @Output. The parent binds a value into the child’s input property, the child raises an event the parent listens for, and nothing else crosses between them, which is what makes the child reusable somewhere else.

Here we split the owner details page from Angular Error Handling: HTTP Errors and Error Pages into a parent and a child, emit an event back up with EventEmitter, build the two modal components the rest of the series uses, and write a directive that changes the element it sits on.

For the complete navigation and all the basic instructions of the Angular series, check out: Introduction of the Angular series.

To download the source code for this article, you can visit the InputsAndOutputs folder in our GitHub repository. The source code for the whole Angular series is here.
The server application this client calls is the one from our ASP.NET Core Web API series. Its code is in the UsingRepositoryForWriteRequests folder in our GitHub repository. Run it on its http profile, which listens on http://localhost:5000, the address our environment file points to.

How Do Parent and Child Components Share Data in Angular?

Data flows down through inputs and back up through outputs. A parent binds a value into a child’s input. The child emits an event the parent listens for. Nothing else crosses the boundary.

An input is a property the child declares as part of its public surface. The parent writes [accounts]="owner.accounts" in its template, and the child receives whatever that expression evaluates to, re-evaluated whenever it changes.

An output is an event the child raises. The child declares it, calls emit() with a payload, and the parent listens with (accountClick)="handle($event)", where $event is that payload.

Two consequences follow. A child never reaches into its parent, and a parent never reaches into the child’s internals, which is exactly what lets the same child render somewhere else unchanged.

Angular offers two ways to declare both, the original decorators and the newer signal-based functions, and a component can use either.

The whole contract is small enough to draw, so here it is: one binding going down into the child, one event coming back up, and a boundary that neither side reaches across.

Angular parent component binding accounts into a child component's Input, and the child emitting an accountClick event back through its Output

How Do We Pass Data to a Child Component With @Input?

Currently, our OwnerDetails component shows both the owner and its accounts on one page. We can split that up, which makes the component cleaner and easier to maintain.

To do that, let’s start with the creation of a new component:

ng g component owner/owner-details/owner-accounts --skip-tests

We place its files inside the owner-details folder, inside the owner feature we put behind a lazy route, because we only use this component to extract some content from OwnerDetails.

Now, let’s modify the owner-accounts.ts file:

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

import { Account } from '../../../_interfaces/account.model';

@Component({
  imports: [DatePipe],
  selector: 'app-owner-accounts',
  styleUrl: './owner-accounts.css',
  templateUrl: './owner-accounts.html',
})
export class OwnerAccounts {
  @Input() accounts: Account[] = [];
}

We add a single Account array property and decorate it with the @Input decorator. It needs the = [] initialiser, because strictPropertyInitialization is on in an Angular 22 project and a declared property that no constructor assigns has to be given a value where it stands. The component also imports DatePipe, because the template we are about to paste into it formats a date, and a standalone component brings in whatever its own template uses.

Next, we are going to cut the table that shows accounts from the owner-details.html file and replace it with the selector of the OwnerAccounts component:

<!-- previous code -->

  } @else {
    <div class="row">
      <div class="col-md-3">
        <strong>Type of user:</strong>
      </div>
      <div class="col-md-3">
        <span class="text-info">Advanced user.</span>
      </div>
    </div>
  }
</div>

<app-owner-accounts [accounts]="owner()?.accounts ?? []"></app-owner-accounts>

Here, we use the app-owner-accounts selector to place the child component inside the parent, and property binding to hand the owner’s accounts to the child’s @Input property.

The ?? [] on the end is doing real work. The parent holds the fetched owner in a signal, so owner() is Owner | undefined and accounts on it is optional, which makes the whole expression Account[] | undefined. Angular’s strictTemplates checking will not bind that to an input declared Account[], and the empty array is the shortest way to satisfy it.

A standalone parent also has to name the child it renders, so owner-details.ts imports the class and lists it in its own imports array:

import { OwnerAccounts } from './owner-accounts/owner-accounts';

@Component({
  imports: [DatePipe, OwnerAccounts],
  selector: 'app-owner-details',
  styleUrl: './owner-details.css',
  templateUrl: './owner-details.html',
})
export class OwnerDetails implements OnInit {

That array is what replaces the declarations list an older module-based project kept. There is no module to add the child to, and no module for the parent to import.

After that, we can paste the code we cut from the parent component into the owner-accounts.html file:

<div class="row">
  <div class="col-md-12">
    <div class="table-responsive">
      <table class="table table-striped">
        <thead>
          <tr>
            <th>Account type</th>
            <th>Date created</th>
          </tr>
        </thead>
        <tbody>
          @for (account of accounts; track account.id) {
            <tr>
              <td>{{account?.accountType}}</td>
              <td>{{account?.dateCreated | date: 'dd/MM/yyyy'}}</td>
            </tr>
          }
        </tbody>
      </table>
    </div>
  </div>
</div>

We can also notice that we are not using owner()?.accounts any more inside the @for block, just the accounts property. That is the whole point of the input: the child knows about a collection of accounts and nothing about the owner they belong to.

With that in place, we can start the Web API, run our app and navigate to the OwnerDetails component, and we are going to see the same result as before. Only this time, we have split the logic into two components.

How Do We Emit Events From a Child Component With @Output?

Let’s use the simplest example to show how we can emit events from the child component to the parent component.

To start, we are going to add some modifications inside the OwnerAccounts component:

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

import { Account } from '../../../_interfaces/account.model';

@Component({
  imports: [DatePipe],
  selector: 'app-owner-accounts',
  styleUrl: './owner-accounts.css',
  templateUrl: './owner-accounts.html',
})
export class OwnerAccounts {
  @Input() accounts: Account[] = [];
  @Output() accountClick: EventEmitter<Account> = new EventEmitter();

  onAccountClicked = (account: Account) => {
    this.accountClick.emit(account);
  }

}

We use the @Output decorator with the EventEmitter to emit an event from a child to a parent component. By providing a type for the EventEmitter, we state that it can emit only the Account data type. Then we create a new onAccountClicked function, where we accept an account and call emit to send it to the parent component.

The output is named for the event, not for the handler. Angular’s own guidance is to avoid prefixing an output name with on, and the reason shows up in the parent’s template further down: the binding reads (accountClick), where an output called onAccountClick would bind as (onAccountClick) and read as “on on account click”.

To be able to call this onAccountClicked function, we have to modify the owner-accounts.html file:

<tbody>
  @for (account of accounts; track account.id) {
    <tr (click)="onAccountClicked(account)">
      <td>{{account?.accountType}}</td>
      <td>{{account?.dateCreated | date: 'dd/MM/yyyy'}}</td>
    </tr>
  }
</tbody>

We just add the click event to the row inside the table.

Finally, in order to have a pointer cursor when we hover over our account rows, we are going to modify the owner-accounts.css file:

tbody tr:hover {
  cursor: pointer;
}

For our emitter to work, we have to subscribe to it from our parent component.

The first thing we are going to do is to add a single function inside the owner-details.ts file, which also picks up an Account import:

printToConsole = (param: Account) => {
  console.log('Account parameter from the child component', param)
}

And then, we are just going to connect the dots in the owner-details.html file:

<app-owner-accounts [accounts]="owner()?.accounts ?? []"
  (accountClick)="printToConsole($event)"></app-owner-accounts>

Here, we listen for an event with the same name as the @Output property in the child component and assign our local printToConsole function to it. We use $event as the parameter to accept the emitted object from the child component.

At this point, we can start our app, and once we navigate to the OwnerDetails component by clicking the Details button, we can see that as soon as we click on any account, we log a message in the console.

Error Modal Component

The error handling we built in the previous part still has one branch unfinished: handleOtherError in the interceptor carries a comment saying it will be fixed later, and shows the reader nothing. This is the component that comment is waiting for, and the next part wires the two together.

Let’s execute the Angular CLI command to create an error modal component:

ng g component shared/modals/error-modal --skip-tests

There is no shared module to register it in, and it needs none. A standalone component is imported by whatever renders it, and this one is never rendered from a template at all: BsModalService from ngx-bootstrap creates it on demand, so the class is imported by the code that opens the modal and by nothing else.

Now, let’s modify the error-modal.ts file:

import { Component, inject } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap/modal';

@Component({
  selector: 'app-error-modal',
  styleUrl: './error-modal.css',
  templateUrl: './error-modal.html',
})
export class ErrorModal {
  modalHeaderText = '';
  modalBodyText = '';
  okButtonText = '';

  readonly bsModalRef = inject(BsModalRef);
}

Here we have three properties that we are going to use in the HTML part of this component, each initialised to an empty string so that strictPropertyInitialization is satisfied. Whoever opens the modal fills them in. We also inject the BsModalRef class with inject() and leave it public, because the template calls hide() on it.

Let’s continue by modifying the error-modal.html file:

<div class="modal-header">
  <h4 class="modal-title pull-left">{{modalHeaderText}}</h4>
  <button type="button" class="btn-close close pull-right"
  aria-label="Close" (click)="bsModalRef.hide()">
    <span aria-hidden="true" class="visually-hidden">&times;</span>
  </button>
</div>
<div class="modal-body">
  {{modalBodyText}}
</div>
<div class="modal-footer">
  <button type="button" class="btn btn-danger" (click)="bsModalRef.hide()">{{okButtonText}}</button>
</div>

Just a simple HTML file that uses string interpolation to add the text for the header, body, and button. We can also see how we use bsModalRef, which we injected into the class, to call the hide function and close the modal.

Creating Success Modal Component

To continue, let’s create a success modal in the same way as we did with the error modal:

ng g component shared/modals/success-modal --skip-tests

Then, let’s modify our success-modal.ts file:

import { Component, EventEmitter, inject } from '@angular/core';
import { BsModalRef } from 'ngx-bootstrap/modal';

@Component({
  selector: 'app-success-modal',
  styleUrl: './success-modal.css',
  templateUrl: './success-modal.html',
})
export class SuccessModal {
  modalHeaderText = '';
  modalBodyText = '';
  okButtonText = '';
  redirectOnOk: EventEmitter<void> = new EventEmitter();

  private bsModalRef = inject(BsModalRef);

  onOkClicked = () => {
    this.redirectOnOk.emit();
    this.bsModalRef.hide();
  }

}

We are going to use the success component once we execute the create, update, or delete actions successfully, and by pressing the OK button we redirect a user back to the owner list. That is what the EventEmitter is for. This time we keep bsModalRef private, because we use it only inside the .ts file.

redirectOnOk carries no @Output() decorator, and that is deliberate rather than an oversight. A decorator exposes an output to a parent template, and this component has no parent template: BsModalService creates it outside the component tree, so the code that opens the modal subscribes to the emitter through the modal reference instead of binding to it.

Finally, let’s modify the success-modal.html file:

<div class="modal-header">
  <h4 class="modal-title pull-left">{{modalHeaderText}}</h4>
  <button type="button" class="btn-close close pull-right"
  aria-label="Close" (click)="onOkClicked()">
    <span aria-hidden="true" class="visually-hidden">&times;</span>
  </button>
</div>
<div class="modal-body">
  {{modalBodyText}}
</div>
<div class="modal-footer">
  <button type="button" class="btn btn-success" (click)="onOkClicked()">{{okButtonText}}</button>
</div>

We are going to use both of these modal components in the next part of the series.

What Is an Angular Directive, and How Do We Write One?

A directive adds behaviour to an element that already exists, without bringing a template of its own. A component is a directive that also renders markup, which is why the two share so much.

The selector decides where it applies. A directive declared with the selector [appAppend] attaches to any element carrying that attribute, so one class can decorate a span, a table cell or a button without knowing which.

Inputs work exactly as they do on a component. Naming the input after the selector lets a template pass a value through the attribute itself, so [appAppend]="owner()" both applies the directive and feeds it.

ElementRef gives access to the host element, and Renderer2 changes it. Angular’s guide names a second route that touches nothing directly: host bindings declared in the decorator’s host property, which change the element without reaching for it.

Lifecycle hooks apply too, so a directive can react each time its input changes.

So, let’s create the append directive in the shared folder:

ng g directive shared/directives/append --skip-tests

This creates a new file with the @Directive decorator and the [appAppend] selector. We are going to use that selector to add the additional behaviour to elements inside the OwnerDetails component:

@if ((owner()?.accounts?.length ?? 0) <= 2) {
  <div class="row">
    <div class="col-md-3">
      <strong>Type of user:</strong>
    </div>
    <div class="col-md-3">
      <span [appAppend]="owner()" class="text-success">Beginner user.</span>
    </div>
  </div>
} @else {
  <div class="row">
    <div class="col-md-3">
      <strong>Type of user:</strong>
    </div>
    <div class="col-md-3">
      <span [appAppend]="owner()" class="text-info">Advanced user.</span>
    </div>
  </div>
}

We add the [appAppend] selector inside both span elements and pass the owner object to our directive. The parent lists the directive class beside the child component it already imports, so its decorator now reads imports: [DatePipe, Append, OwnerAccounts].

Now, we have to modify the directive file:

import { Directive, ElementRef, Input, OnChanges, Renderer2, SimpleChanges, inject } from '@angular/core';

import { Owner } from '../../_interfaces/owner.model';

@Directive({
  selector: '[appAppend]',
})
export class Append implements OnChanges {
  @Input('appAppend') ownerParam?: Owner;

  private element = inject(ElementRef);
  private renderer = inject(Renderer2);

  ngOnChanges(changes: SimpleChanges) {
    if (changes['ownerParam'].currentValue) {
      const accNum = changes['ownerParam'].currentValue?.accounts?.length ?? 0;
      const span = this.renderer.createElement('span');
      const text = this.renderer.createText(` (${accNum}) accounts`);

      this.renderer.appendChild(span, text);
      this.renderer.appendChild(this.element.nativeElement, span);
    }
  }
}

Here we have the ownerParam property that we decorate with the @Input decorator, aliased to the selector so that a single binding both applies the directive and passes it a value. Then we inject the ElementRef and Renderer2 classes with inject(). We use ElementRef to reference the element our directive is applied to, and Renderer2 to change that element without reaching into the DOM ourselves.

After the injected fields, we add the ngOnChanges lifecycle method. It runs before ngOnInit and again every time one or more input properties change. Inside it, we check that our input property has a currentValue, and if it does, we read the number of accounts, use the renderer to create a new span and a text node, and combine them.

Two small things in that method are there for the compiler. SimpleChanges is an index-signature type and a CLI 22 project sets noPropertyAccessFromIndexSignature, so changes['ownerParam'] is the access form that compiles. And accounts is optional on Owner, so the count reads through ?. and falls back to ?? 0 rather than dereferencing a collection that may not be there.

Finally, we use the same renderer to append the new span to the host element.

Now we can start our apps, and once we navigate to the details page of any owner, we will see our directive in action:

Directive usage to add number of accounts

We see new information next to the user’s description. John Keen has three accounts, so the rendered page above reads “Advanced user. (3) accounts”. An owner with two or fewer takes the other branch and reads “Beginner user.” with its own count appended.

What Are Signal Inputs and Outputs?

Angular has a second way to declare inputs and outputs, built on signals. The documentation recommends it for new projects and says the decorator API remains fully supported, so both forms are current.

An input becomes a field. accounts = input<Account[]>() returns an InputSignal, read as accounts() in the class and in the template, and input.required<Account[]>() makes the binding compulsory so the compiler reports a missing one.

An output becomes accountClick = output<Account>(), which returns an OutputEmitterRef and still exposes emit(). The parent’s template binding does not change at all.

model<T>() covers two-way binding on its own, declaring the input and a matching Change output as one writable signal.

The practical difference is what reads the value. A signal input can be consumed by computed() and effect(), which removes most of the reasons to write an ngOnChanges hook just to notice that something arrived.

What we needDecorator formSignal formDirection
Receive a value from the parent@Input() accounts: Account[]accounts = input<Account[]>()parent to child
Require the parent to supply it@Input({required: true})accounts = input.required<Account[]>()parent to child
Give the input a default@Input() count = 0count = input(0)parent to child
Tell the parent something happened@Output() accountClick = new EventEmitter<Account>()accountClick = output<Account>()child to parent
Two-way bindingan @Input() x paired with an @Output() xChangex = model<string>()both
React when the value changesngOnChanges(changes: SimpleChanges)computed() or effect() reading the signalparent to child

This series holds its fetched data in signals, from the owner list onward, and still declares its inputs and outputs with the decorators, because the two are separate choices. Angular’s guide to custom events with outputs says so directly: “While the Angular team recommends using the output function for new projects, the original decorator-based @Output API remains fully supported.”

Conclusion

Inputs and outputs are the whole contract between a parent and a child: one binding down, one event up. Once the boundary is that narrow, the child can be moved or reused without the parent knowing anything about its internals.

In this post we have learned:

  • How @Input moves a value from a parent into a child, and how the parent binds it
  • How @Output and EventEmitter send an event back up, and what $event carries
  • Why a standalone component is imported by whatever uses it, and why that removes the shared module entirely
  • How to build the error and success modals the rest of the series uses
  • What a directive is, and how ElementRef and Renderer2 let it change its host element
  • What the same inputs and outputs look like written as signals

In the next part, Angular Reactive Form Validation and POST Requests, we build the create-owner form and put both of these modals to work.

This series follows the conventions collected in our Angular best practices guide, which is worth a read before starting a project of your own.

Tested with Angular 22.1.5, Angular CLI 22.1.7, ngx-bootstrap 22.0.0, Bootstrap 5.3.8, TypeScript 6.0.3 and Node 24.