Angular Input Output
Introduction
One of Angular's most powerful features is its component-based architecture, which allows you to build complex applications using smaller, reusable building blocks. For these components to work together effectively, they need ways to communicate with each other. This is where Angular's @Input()
and @Output()
decorators come in.
These decorators create a contract between parent and child components:
@Input()
allows a parent component to pass data to a child component@Output()
enables a child component to send data back to the parent component when an event occurs
By the end of this tutorial, you'll understand how to create components that can effectively communicate with each other, making your Angular applications more dynamic and interactive.
Prerequisites
Before we dive in, you should have:
- Basic understanding of Angular components
- Familiarity with TypeScript decorators
- Angular development environment set up
Understanding @Input Decorator
The @Input()
decorator is used to make a class property available for data binding by parent components. In simpler terms, it allows a parent component to update data in the child component.
Basic Syntax
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>Message from parent: {{message}}</p>`
})
export class ChildComponent {
@Input() message: string = '';
}
In the parent component's template, you can use the child component and pass data like this:
<app-child [message]="parentMessage"></app-child>
And in the parent component class:
@Component({
selector: 'app-parent',
template: `
<h2>Parent Component</h2>
<app-child [message]="parentMessage"></app-child>
`
})
export class ParentComponent {
parentMessage = 'Hello from parent!';
}
Aliasing Input Properties
Sometimes you might want to use a different property name within your component than the one exposed to external components. You can do this by specifying an alias:
@Component({
selector: 'app-child',
template: `<p>Message from parent: {{childMessage}}</p>`
})
export class ChildComponent {
@Input('message') childMessage: string = '';
}
Now the parent component would still use message
but internally the child refers to the property as childMessage
:
<app-child [message]="parentMessage"></app-child>
Understanding @Output Decorator
While @Input()
passes data down from parent to child, @Output()
sends data up from child to parent. It does this through event emitters.
Basic Syntax
import { Component, Output, EventEmitter } from '@angular/core';
@Component({
selector: 'app-child',
template: `
<button (click)="sendMessageToParent()">Send Message to Parent</button>
`
})
export class ChildComponent {
@Output() messageEvent = new EventEmitter<string>();
sendMessageToParent() {
this.messageEvent.emit('Hello from child!');
}
}
In the parent component:
@Component({
selector: 'app-parent',
template: `
<h2>Parent Component</h2>
<p>Message from child: {{messageFromChild}}</p>
<app-child (messageEvent)="receiveMessage($event)"></app-child>
`
})
export class ParentComponent {
messageFromChild: string = '';
receiveMessage(message: string) {
this.messageFromChild = message;
}
}
Aliasing Output Properties
Just like with @Input()
, you can also specify an alias for @Output()
properties:
@Component({
selector: 'app-child',
template: `
<button (click)="sendMessageToParent()">Send Message to Parent</button>
`
})
export class ChildComponent {
@Output('childEvent') messageEvent = new EventEmitter<string>();
sendMessageToParent() {
this.messageEvent.emit('Hello from child!');
}
}
Then in the parent:
<app-child (childEvent)="receiveMessage($event)"></app-child>
Practical Example: ToDo List Component
Let's create a more practical example where we build a simple todo list application with two components:
TodoListComponent
(parent): Manages the list of tasksTodoItemComponent
(child): Represents an individual task and allows marking it as complete
Step 1: Create TodoItemComponent
// todo-item.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
interface TodoItem {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todo-item',
template: `
<div class="todo-item" [class.completed]="todo.completed">
<input
type="checkbox"
[checked]="todo.completed"
(change)="markComplete()">
<span>{{ todo.title }}</span>
<button (click)="deleteTodo()">Delete</button>
</div>
`,
styles: [`
.todo-item {
padding: 10px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
}
.completed {
text-decoration: line-through;
color: #888;
}
span {
margin: 0 10px;
flex-grow: 1;
}
`]
})
export class TodoItemComponent {
@Input() todo!: TodoItem;
@Output() complete = new EventEmitter<number>();
@Output() delete = new EventEmitter<number>();
markComplete() {
this.complete.emit(this.todo.id);
}
deleteTodo() {
this.delete.emit(this.todo.id);
}
}
Step 2: Create TodoListComponent
// todo-list.component.ts
import { Component } from '@angular/core';
interface TodoItem {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-todo-list',
template: `
<div class="todo-list">
<h2>My Todo List</h2>
<div class="add-todo">
<input
#newTodo
type="text"
placeholder="Add new task..."
(keyup.enter)="addTodo(newTodo.value); newTodo.value = ''">
<button (click)="addTodo(newTodo.value); newTodo.value = ''">Add</button>
</div>
<app-todo-item
*ngFor="let item of todos"
[todo]="item"
(complete)="toggleComplete($event)"
(delete)="removeTodo($event)">
</app-todo-item>
<p *ngIf="todos.length === 0">No tasks yet! Add one above.</p>
</div>
`,
styles: [`
.todo-list {
width: 400px;
margin: 0 auto;
font-family: Arial, sans-serif;
}
.add-todo {
display: flex;
margin-bottom: 20px;
}
.add-todo input {
flex-grow: 1;
padding: 8px;
margin-right: 8px;
}
`]
})
export class TodoListComponent {
todos: TodoItem[] = [
{ id: 1, title: 'Learn Angular', completed: false },
{ id: 2, title: 'Create a todo app', completed: false },
{ id: 3, title: 'Profit!', completed: false }
];
nextId = 4;
addTodo(title: string) {
if (title.trim()) {
this.todos.push({
id: this.nextId++,
title: title.trim(),
completed: false
});
}
}
toggleComplete(id: number) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
}
}
removeTodo(id: number) {
this.todos = this.todos.filter(todo => todo.id !== id);
}
}
How This Example Works
-
Parent-to-Child Communication: The
TodoListComponent
passes task data to eachTodoItemComponent
via the@Input() todo
property. -
Child-to-Parent Communication: The
TodoItemComponent
has two@Output()
properties:complete
: Emits when a user checks/unchecks a tododelete
: Emits when a user clicks the delete button
-
Flow of Events:
- When the user adds a new todo, the parent component adds it to its list
- When the user marks a todo as complete, the child component emits an event with the todo's ID, and the parent component updates its data model
- When the user deletes a todo, the child component emits the ID, and the parent component removes it from the list
This pattern creates a clean separation of concerns: the parent manages the data, while the child manages the UI representation and user interactions.
Advanced Concepts: OnChanges Lifecycle Hook
Sometimes you need to react when an @Input()
property changes. For this, you can use the OnChanges
lifecycle hook:
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'app-child',
template: `<p>Name: {{name}}</p><p>Age: {{age}}</p>`
})
export class ChildComponent implements OnChanges {
@Input() name: string = '';
@Input() age: number = 0;
ngOnChanges(changes: SimpleChanges) {
// This will run whenever any input property changes
console.log('Input properties changed:', changes);
if (changes['name']) {
console.log('Previous name:', changes['name'].previousValue);
console.log('Current name:', changes['name'].currentValue);
console.log('First change?', changes['name'].firstChange);
}
}
}
This is particularly useful when you need to:
- Validate input data
- Transform input data before using it
- Trigger side effects when specific inputs change
Best Practices
-
Keep components focused: Each component should have a single responsibility.
-
Document your inputs and outputs: Make it clear how your components should be used.
typescript/**
* Displays a customizable button.
* @Input() color - The button color ('primary', 'warning', 'danger')
* @Output() click - Emitted when button is clicked
*/
@Component({ ... })
export class ButtonComponent {
@Input() color: string = 'primary';
@Output() click = new EventEmitter<void>();
} -
Avoid excessive nesting: Too many levels of parent-child relationships can become confusing.
-
Consider using state management for complex applications where many components need to share data.
-
Use appropriate data types for your inputs and outputs, not just
any
.
Common Errors and Debugging
1. Forgetting to declare Input or Output
If you're trying to bind to a property that isn't decorated with @Input()
:
Template parse errors:
Can't bind to 'message' since it isn't a known property of 'app-child'
2. Mismatched event names
Make sure your event binding in the parent matches the @Output()
property name in the child:
// Child
@Output() userSelected = new EventEmitter<User>();
// Parent (CORRECT)
<app-user-list (userSelected)="handleUserSelection($event)"></app-user-list>
// Parent (WRONG)
<app-user-list (userSelect)="handleUserSelection($event)"></app-user-list>
3. Not importing required modules
Always ensure you've imported:
import { Component, Input, Output, EventEmitter } from '@angular/core';
Summary
@Input()
and @Output()
decorators are fundamental to component communication in Angular:
- @Input() allows parent components to pass data to child components, enabling parent-driven, top-down data flow.
- @Output() with
EventEmitter
allows child components to notify parent components of events or changes, enabling bottom-up communication.
This bidirectional communication pattern is the foundation of building complex, interactive UIs with a clean component architecture. By mastering these concepts, you'll be able to create well-structured Angular applications with reusable, composable components.
Exercises
-
Create a star rating component that:
- Takes a
maxStars
input (default: 5) - Takes a
rating
input for the initial value - Emits a
ratingChange
event when the user selects a new rating
- Takes a
-
Build a form with validation that:
- Uses
@Input()
to configure validation rules (e.g., min/max length) - Uses
@Output()
to emit validation status to the parent
- Uses
-
Create a shopping cart system with:
- A product list component that emits when items are added
- A cart component that receives items via inputs
- Quantity controls that emit changes back to the parent
Additional Resources
- Angular Official Documentation on Component Interaction
- Angular University: Input and Output in Angular
- Understanding ViewEncapsulation in Angular
- NgRx for State Management (for more complex applications)
Remember that practice is key to mastering these concepts. Try building small projects that utilize component communication to solidify your understanding!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)