Skip to main content

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

typescript
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:

html
<app-child [message]="parentMessage"></app-child>

And in the parent component class:

typescript
@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:

typescript
@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:

html
<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

typescript
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:

typescript
@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:

typescript
@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:

html
<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 tasks
  • TodoItemComponent (child): Represents an individual task and allows marking it as complete

Step 1: Create TodoItemComponent

typescript
// 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

typescript
// 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

  1. Parent-to-Child Communication: The TodoListComponent passes task data to each TodoItemComponent via the @Input() todo property.

  2. Child-to-Parent Communication: The TodoItemComponent has two @Output() properties:

    • complete: Emits when a user checks/unchecks a todo
    • delete: Emits when a user clicks the delete button
  3. 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:

typescript
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

  1. Keep components focused: Each component should have a single responsibility.

  2. 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>();
    }
  3. Avoid excessive nesting: Too many levels of parent-child relationships can become confusing.

  4. Consider using state management for complex applications where many components need to share data.

  5. 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:

typescript
// 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:

typescript
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

  1. 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
  2. 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
  3. 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

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! :)