Modernizing Angular Components: Signals, Inputs, Outputs, and Required Queries in Angular 17+

Felipe Norato
4 min readJan 28, 2025

--

Introduction

Angular continues to evolve with Signal-based APIs, introducing a cleaner, more declarative, and reactive approach to building components. In Angular 17+, Signals replace traditional decorators like @Input, @Output, @ViewChild, and @ViewChildren, while also enabling new capabilities like required queries and dynamic transformations.

This post dives deep into these advancements, showcasing practical examples, what Signals unlock that traditional decorators cannot achieve, and how they simplify component communication, DOM queries, and runtime guarantees.

1. Signals in Inputs and Outputs

Signals with @Input

The input() function enhances component communication by making inputs reactive, supporting transformations, and enforcing required values. This eliminates the need for manual change detection (ngOnChanges) or additional logic.

Example: Reactive Input with Transformation

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

@Component({
selector: 'app-transformed-input',
template: `<p>Formatted Input: {{ formattedValue() }}</p>`
})
export class TransformedInputComponent {
formattedValue = input('', { transform: (value: string) => value.trim().toUpperCase() });
}

What Signals Enable:

Reactive Access: Use formattedValue() to get the current value.

Transformations: Process input data (e.g., trimming, formatting) before it is used in the component.

Using required with Inputs

Ensure critical inputs are always provided using the required option:

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

@Component({
selector: 'app-required-input',
template: `<p>Required Input: {{ requiredValue() }}</p>`
})
export class RequiredInputComponent {
requiredValue = input('', { required: true });
}

What Signals Enable:

  • Angular will throw a runtime error if the parent component fails to provide the input:
Error: Required input 'requiredValue' is missing for component RequiredInputComponent.

Signals with @Output

The output() function simplifies event emitters by directly integrating signals into the output system.

Example: Signal-Based Output

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

@Component({
selector: 'app-custom-button',
template: `<button (click)="notify()">Click Me</button>`
})
export class CustomButtonComponent {
action = output<string>();

notify() {
this.action.emit('Button clicked!');
}
}

What Signals Enable:

  • Type Safety: Ensures emitted events match the expected type.
  • Simplified Syntax: No need for EventEmitter.

2. Signals in Component Queries

Replacing @ViewChild and @ViewChildren

Signal-based queries like viewChild() and viewChildren() make DOM or child component access reactive, eliminating the need for manual null checks or lifecycle synchronization.

Example: Replacing @ViewChild

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

@Component({
selector: 'app-input-focus',
template: `<input #focusInput type="text">`
})
export class InputFocusComponent {
focusInput = viewChild<HTMLInputElement>('focusInput');

ngAfterViewInit() {
this.focusInput().focus(); // Reactive access to the DOM element
}
}

Example: Replacing @ViewChildren

import { Component, viewChildren } from '@angular/core';
import { ItemComponent } from './item.component';

@Component({
selector: 'app-items',
template: `<app-item *ngFor="let item of items"></app-item>`
})
export class ItemsComponent {
items = ['Item 1', 'Item 2', 'Item 3'];
itemComponents = viewChildren(ItemComponent);

ngAfterViewInit() {
console.log(this.itemComponents().length); // Logs the number of ItemComponent instances
}
}

Required Queries

Required queries ensure that certain child components or DOM elements must exist in the template, adding runtime guarantees and eliminating null-check logic.

Example: Required Component Queries

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

@Component({
selector: 'app-custom-card',
template: `
<div class="card">
<ng-container *ngIf="header()">
<ng-container *ngTemplateOutlet="header()?.template"></ng-container>
</ng-container>
<ng-content select="app-card-body"></ng-content>
</div>
`
})
export class CustomCard {
header = viewChild.required(CustomCardHeader); // Required query
body = contentChild(CustomCardBody); // Optional query
}

3. What Signals Enable That Traditional Decorators Cannot

4. Example: Chained Computed Signals

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

@Component({
selector: 'app-price-calculator',
template: `<p>Total Price: {{ totalPrice() }}</p>`
})
export class PriceCalculatorComponent {
quantity = signal(2);
pricePerUnit = signal(50);
totalPrice = computed(() => this.quantity() * this.pricePerUnit());
}

What Signals Enable:

• Reactive, dynamic derived values that automatically update when dependencies change.

Why Traditional Decorators Fall Short:

Requires methods or lifecycle hooks for derived state:

@Input() quantity = 2;
@Input() pricePerUnit = 50;
totalPrice!: number;

ngOnChanges() {
this.totalPrice = this.quantity * this.pricePerUnit;
}

Conclusion

Angular’s Signal-based API unlocks a host of capabilities that make components more declarative, reactive, and type-safe. From enforcing required inputs and queries to enabling dynamic outputs and computed state, these advancements improve code clarity, runtime safety, and performance.

Call-to-Action:

“Which Signal-only feature excites you the most? Share your thoughts and experiences in the comments!”

--

--

Felipe Norato
Felipe Norato

Written by Felipe Norato

A person who likes to solve people’s lives using Code and sometimes play Guitar. Lover of TV Shows and Movies, as well as beautiful and performative code.

No responses yet