In my previous post, we saw what Signals are and the basics of how to use them. In this post, we will see how to use the new input() and output() functions, and how we can simplify our state management by using the inject() function with a Service.
But first of all, what are Signal Inputs and Outputs?
“With recent updates, Angular introduced a new way to handle component communication. Instead of the traditional @Input() and @Output() decorators, we can now use input() and output functions.
A Signal Input acts as a read-only Signal inside our component, which means it perfectly integrates with computed() and effect(). An Output function works identically to the classic EventEmitter but provides a much cleaner syntax.”
Finally, the inject() function is a modern alternative to constructor injection. It allows us to pull services directly into our properties, making components cleaner and easier to read.
WHEN SHOULD WE USE SIGNAL INPUTS AND OUTPUTS?
We should consider Signal Inputs when we want to pass data from a parent component to a child component and react to changes seamlessly. Because they are Signals, Angular tracks them automatically, completely eliminating the need for ngOnChanges. Outputs are used when the child component needs to communicate those changes back up to the parent.
THE BENEFITS TO USE THEM
- Simplicity: Less boilerplate code compared to using decorators.
- Reactivity: Inputs are native Signals, meaning we can easily create computed values based on them or react to them using an effect.
- Cleaner Dependencies: Using inject() removes the need to pass numerous services through the constructor, which keeps our code incredibly tidy.
Let’s see a practical example by creating a ToDo List application where we select an item from a list, pass it to a detail page to edit, and emit the updated value back.
[THE SERVICE]
First, we create a service to manage our data using a writable signal:
ng g s services/TodoService
[todo.service.ts]
import { Injectable, signal } from '@angular/core';
export interface Todo {
id: number;
title: string;
completed: boolean;
}
@Injectable({
providedIn: 'root'
})
export class TodoService {
// We initialize our state with a writable signal
todos = signal<Todo[]>([
{ id: 1, title: 'Learn Angular Signals', completed: true },
{ id: 2, title: 'Write Blog Post', completed: false }
]);
updateTodo(updatedTodo: Todo) {
// We update the array by replacing the modified item
this.todos.update(currentTodos =>
currentTodos.map(todo => todo.id === updatedTodo.id ? updatedTodo : todo)
);
}
}
[THE CHILD COMPONENT: DETAIL/FORM]
Then, we create the component that will receive the task via input() and send it back via ouput().
ng g c components/TodoDetail
[todo-details.ts]
import { Component, input, output, signal, effect } from '@angular/core';
import { Todo } from '../../services/todo-service';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-todo-detail',
imports: [FormsModule],
templateUrl: './todo-detail.html',
styleUrl: './todo-detail.css'
})
export class TodoDetailComponent {
// We define an optional input as a Signal
todo = input<Todo>();
// We define an output to send data back
todoUpdated = output<Todo>();
// Local state for the form input
editTitle = signal('');
constructor() {
// We use an effect to react whenever the input changes
effect(() => {
const currentTodo = this.todo();
if (currentTodo) {
this.editTitle.set(currentTodo.title);
}
});
}
save() {
const currentTodo = this.todo();
if (currentTodo) {
// Emit the updated object back to the parent
this.todoUpdated.emit({ ...currentTodo, title: this.editTitle() });
}
}
}
[todo-details.html]
@if (todo()) {
<div class="todo-form">
<h3>Edit Task</h3>
<input type="text" [ngModel]="editTitle()" (ngModelChange)="editTitle.set($event)">
<button (click)="save()">Save</button>
</div>
}
[todo-details.css]
.todo-form {
margin-top: 2rem;
padding: 1.5rem;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
border-left: 4px solid #10b981; /* Green accent to show it's an active edit */
}
h3 {
margin-top: 0;
color: #1a1a1a;
margin-bottom: 1rem;
}
input {
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #d1d5db;
border-radius: 4px;
box-sizing: border-box;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s ease;
}
input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
button {
background-color: #10b981; /* Green for saving */
color: white;
border: none;
padding: 0.6rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 1rem;
transition: background-color 0.2s ease;
}
button:hover {
background-color: #059669;
}
[THE PARENT COMPONENT: LIST]
Finally, we create the main component where we use inject to get our service, display the list, and handle the child component.
ng g c components/TodoList
[todo-list.ts]
import { Component, inject, signal } from '@angular/core';
import { TodoService, Todo } from '../../services/todo-service';
import { TodoDetailComponent } from '../todo-detail/todo-detail';
@Component({
selector: 'app-todo-list',
imports: [TodoDetailComponent],
templateUrl: './todo-list.html',
styleUrl: './todo-list.css'
})
export class TodoListComponent {
// We inject the service directly without using a constructor
private todoService = inject(TodoService);
// We expose the signal to the template
todos = this.todoService.todos;
// Local signal to track the selected item for editing
selectedTodo = signal<Todo | undefined>(undefined);
select(todo: Todo) {
this.selectedTodo.set(todo);
}
onSave(updatedTodo: Todo) {
// We pass the updated data to the service
this.todoService.updateTodo(updatedTodo);
// Reset the selection to hide the form
this.selectedTodo.set(undefined);
}
}
[todo-list.html]
<p>todo-list works!</p>
<div>
<h2>My ToDo List</h2>
<ul>
@for (item of todos(); track item.id) {
<li>
{{ item.title }} - {{ item.completed ? 'Done' : 'Pending' }}
<button (click)="select(item)">Edit</button>
</li>
}
</ul>
<app-todo-detail
[todo]="selectedTodo()"
(todoUpdated)="onSave($event)">
</app-todo-detail>
</div>
[todo-list.css]
h2 {
color: #1a1a1a;
border-bottom: 2px solid #f0f0f0;
padding-bottom: 0.75rem;
margin-top: 0;
}
ul {
list-style: none;
padding: 0;
margin: 1.5rem 0;
}
li {
display: flex;
justify-content: space-between;
align-items: center;
background: #f9fafb;
margin-bottom: 0.75rem;
padding: 1rem;
border-radius: 6px;
border: 1px solid #e5e7eb;
font-size: 1rem;
}
button {
background-color: #3b82f6; /* Bright blue */
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
}
button:hover {
background-color: #2563eb;
}
Now, in order to run correctly the app, we need to change these three files:
[app.ts]
import { Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
// Import your new component (adjust the path if your folder structure is slightly different)
import { TodoListComponent } from './components/todo-list/todo-list';
@Component({
selector: 'app-root',
// Add TodoListComponent to the imports array
imports: [RouterOutlet, TodoListComponent],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
protected readonly title = signal('TestSignals2');
}
[app.html]
<app-todo-list></app-todo-list>
<router-outlet />
[app.css]
:host {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: block;
max-width: 600px;
margin: 3rem auto;
padding: 1.5rem;
color: #333;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
We have done and now, if we run the application, this will be the result:


