Key Takeaways
- As Angular is built using a component-based application structure, it’s possible to pass data between components.
- You can pass data to child components using `@Input()`
- You can capture data from child components using `@Output()`
- As applications grow, parent:child component communication can become difficult.
- You can use an external data store such as ngrx to architect larger applications.
Components are the building blocks of Angular and every visual element in an Angular application is made with components. What's great about component-based architectures is that, much like JavaScript functions, if a single piece of code gets too complex or has too much responsibility, you can break it apart so that each code snippet only has one job.
That said, when we start breaking components apart into smaller components, we need to make sure they're able to pass data back and forth. That's when proper component communication becomes essential in our apps to keep all of our data in sync. Fortunately, Angular equips us with the tools to accomplish this.
We could build our entire app under AppComponent, but then that component would have too much responsibility. In component-based architectures, it's considered a better practice to break components apart so they only have one responsibility.
Passing data to components
In Angular, we use @Input
when a parent component needs to pass to a child component. Let's pretend we’re building an app to display comments on a page. The AppComponent
will take care of loading the array of comments, and we will send the data for each comment into the Comment
component.
We will allow a comment parameter to be passed to a child component by using @Input() comment
. Here's what the whole component code looks like:
@Component({
selector: 'comment',
templateUrl: './comment.component.html',
styleUrls: ['./comment.component.css']
})
export class CommentComponent {
@Input() comment;
}
Now we can call this component from other parts of our code and pass in the comment data that it is expecting. Here's what that code might look like:
<comment [comment]="comment"></comment>
Understanding the syntax
First, we have a component selector: <comment></comment>
. If you've ever used Angular before, this should look pretty familiar.
Second is the property binding: [comment]. These square brackets around our element's attribute might seem a little confusing at first. In fact, they’re not required in order to pass data to components, but without them we will be passing just a plain string to the component's @Input()
. Square brackets tell Angular that this is a property binding and that the value passed to is dynamic, so instead of passing a string, it will interpolate and pass dynamic data to the component.
Lastly, we have the value of our attribute after the property binding: "comment"
. This part tells Angular to look at the this.comment property and pass that over to our Comment component.
Combining the concepts
To display a list of comments like we see in the picture above, we can combine a few Angular concepts, including *ngFor. Let's pretend we were able to load in the comment data as part of our component's class as a property called this.comments
.
<comment
*ngFor="let comment of comments"
[comment]="comment"></comment>
The final result will look something like this:
List of comments using the Comment component
Capturing child component events
Now that we know how to pass data to the Comment component, how can a component be deleted and therefore be removed from the list? This is quite tricky to do as the data lives in the Comment
's parent component, AppComponent.
One way to solve the problem would be to use something called @Output(), which helps child components trigger events that can be captured by parent components by taking advantage of the EventEmitter
.
The first step to making child components communicate with their parents is adding a new class property to our component using the @Output() decorator.
@Component({
selector: 'comment',
templateUrl: './comment.component.html'
})
export class CommentComponent {
@Input() private comment;
@Output() private onDelete = new EventEmitter();
deleteComment() {
this.onDelete.emit(this.comment);
}
}
Within the CommentComponent
's template, we're able to call the deleteComment()
method when the delete button is clicked, leaving us ready to start capturing these events from the parent component.
<button (click)="deleteComment()">Delete</button>
Capturing events from the parent component
Within AppComponent, we need a new method that will handle the comment delete action.
@Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent {
/**[code omitted]**/
onCommentDelete(comment) {
// logic to remove comment from comments array
}
}
Within our view, we just need to tell Angular that the onCommentDelete()
method needs to be called when the onDelete
event is triggered.
<comment
*ngFor="let comment of comments"
[comment]="comment"
(onDelete)="onCommentDelete($event)"></comment>
This is what our app looks like once our delete functionality is in place:
Now, we're able to delete comments thanks to Angular's @Output()
decorator
Yet another way to get data
So far, we've only covered component communication that involves a parent/child hierarchy. While this might suit most of your needs, as applications grow, it gets harder to maintain passing data in a parent to child fashion. For larger applications, it often makes sense to use a 'data store' to reduce the workload for individual components. Data-stores work as central repositories for individual components that can be called into parts of an application when they're needed. Instead of manually passing the data down the child-chain, individual components are able to subscribe to parts of the data-store to only use the data they need, reducing the burden placed on parent and child components to pass data back and forth.
If you're familiar with React, this is the problem that Redux solves. With Angular, however, we have an alternative library we can use that was inspired by Redux called ngrx. There are few key-differences between both these libraries: types and observables. The ngrx library relies heavily on the TypeScript ecosystem, which does require a little more boilerplate than Redux, but also makes debugging and tracking down bugs a breeze.
When using ngrx, our state is a single immutable data structure. In order to mutate our state, we're able to call functions that are referred to as actions. In turn, those actions tell our reducers, which are composed of pure functions, what part of the state should be mutate. As a result, our app will now have a next version of the state and all of the components that are subscribed to it will receive the new data immediately. When the components receive the new data, they also get re-rendered automatically, keeping our rendered views always in sync with our store.
What does this mean for development?
While it makes sense conceptually that it’s possible to pass data from an external source directly into a component without passing it through a parent-child relationship, what does this actually mean for development?
What if we wanted to show the number of comments in two separate places? Relying on component hierarchy would be difficult, that's why having a central store using ngrx makes sense.
Passing data directly into a component makes a lot of sense in larger applications. As components are required to handle more tasks, adding additional tasks like keeping track of passing data to children can get complicated. While it’s not necessary to move the burden of passing data to external data-stores, keeping your sanity as your maintain large Angular applications may make sense!
In the example above, an external store (not pictured) is being used to pass information about how many comments are available, “2 comments available,” to the SidebarComponent
, as well as pass the actual Comments into the CommentList
component. This data bypasses the parent AppComponent entirely.
Quick recap on component communication in Angular
And with that, we know the basics of how to make components communicate in Angular. We've learned how to pass data from a parent component to a child component and we're able to use Angular's @Input()
decorator.
We also learned how to use the @Output()
decorator and the EventEmitter to send data back to a parent from a child component.
Alternatively, we learned about ngrx
to reduce the burden placed on parent-child relationships as applications grow. By keeping data in a separate data-store, we're able to access data directly from components without having to pass it through multiple child-components to reach its destination. This makes it easier to maintain larger applications. These principles can also be applied to libraries like React, whose teams have solved the same problem in very similar ways.
You can see our demo app live in this link. And the code for it is also available here.
How to keep learning
In case you are curious to learn more about Angular, be sure to check out Code School's Accelerating through Angular 2 course, and see myself and Gregg Pollack build an Angular 2 app with component interaction & routing (exclusive to Code School members).
Also, check out Angular's documentation on Component Interaction to further understand the underlying mechanisms, as well as the most up-to-date syntax and best practices as the framework is rapidly evolving.
Finally, you can also read through the ngrx documentation on the official ngrx
repo, or check out a sample application I built using ngrx here.
About the Author
Sergio Cruz is an application developer and instructor at Code School, focusing on all things JavaScript. Recently, he taught Code School’s React course, "Powering up With React." When he is not typing code at Code School, you’ll find him speaking at conferences like ng-conf and OSCON about JavaScript.