Lesson Wednesday

In the last lesson, we created our first child component. The TaskListComponent resides in our AppComponent and is responsible for listing all Tasks. However, the "edit" buttons don't work yet. Clicking an edit button will result in an error:

edit-task-errors

This is because the TaskListComponent doesn't know what editTask() is. The logic for this method remains in our AppComponent.

We don't want to move editTask() into TaskListComponent because our components each have a single, dedicated responsibility. As we discussed in the last lesson, an EditTaskComponent will handle updating Tasks.

However, we can't remove the "Edit" button from the TaskListComponent, either, because we need a button to appear next to each Task in the list so the user can specify which task should be edited.

How can we resolve this so our components 'communicate' with one another?

In this lesson we'll learn about the "Data down, actions up" paradigm, including how to share data and actions between components. To do this, we'll learn about @Inputs and @Outputs that handle receiving and sending information between components.

"Data Down, Actions Up!"

Before we begin, let's discuss the "Data down, actions up!" principle. This will assist us in understanding how to manage data and actions between multiple Angular components.

Data down, actions up is a programming principle used in multiple frameworks such as Angular and Ember. Data is stored at the "top" in the parent component and passed downward to child components that may require it. Meanwhile, actions are triggered in child components and passed up to the parent component, which manages changes such as updating data.

Consider the diagram we created while planning our application's component structure:

component-planning-diagram-stage-four

Let's take a look at just the components from the diagram above so we can visualize the relationship between the parent (root component) and its children:

data-down-actions-up-one

"Data Down"

The parent component should hold all necessary data, which it can then pass downward to other components. In our case, this is the collection of Task objects for our To Do List.

data-down-actions-up-2

If any child components require that data, they request it from the root component. The data then moves downward from the parent component into that child component.

For example, we know our TaskListComponent requires access to our Task data to display our list of Tasks. In a "data down, actions up" design, our list of Tasks resides in the parent and is passed downward into the child.

This pattern continues through as many "generations" of components as necessary. For instance, if TaskListComponent had a child component of its own, it could continue passing data it receives from its parent down to its child.

"Actions Up"

Actions move upward. When actions are triggered in child components, these actions are sent upward to the parent. The parent may then alter data or perform other actions accordingly.

data-down-actions-up-3

"Data Down, Actions Up" In Our To Do List

Let's make sure our To Do List application follows this design principle, too. Our application will then follow best practices and our components will be able to communicate with one another.

First, let's make sure our data is in our parent component so it can be sent down. We'll need our list of Tasks to reside in the root AppComponent.

We temporarily moved our list of Tasks from AppComponent to TaskListComponent in the last lesson so our TaskListComponent could list our Tasks. Now we'll move it back into the root AppComponent so it can pass data down to its child components.

We'll also rename our tasks array masterTaskList, to clarify that it is the primary "master" list at the top of our hierarchy:

app/app.component.ts
...
export class AppComponent {
  currentFocus: string = 'Angular Homework';
  currentTime = new Date();
  month: number = this.currentTime.getMonth() + 1;
  day: number = this.currentTime.getDate();
  year: number = this.currentTime.getFullYear();
  selectedTask = null;

  masterTaskList: Task[] = [
    new Task('Finish weekend Angular homework for Epicodus course', 3),
    new Task('Begin brainstorming possible JavaScript group projects', 2),
    new Task('Add README file to last few Angular repos on GitHub', 2)
  ];

  editTask(clickedTask) {
    this.selectedTask = clickedTask;
  }

  finishedEditing() {
    this.selectedTask = null;
  }

}

Receiving Data with @Inputs

Now we'll create an input in the child TaskListComponent. A child component can only receive data from its parent if it has an input available.

An input is like an inbox where the parent component places data into its child. Child components cannot receive data from parents without an input.

The TaskListComponent must have access to our Tasks to function so we'll create an input for it to receive data from its parent AppComponent.

To do this, we add Input to our list of imports at the top of the file:

app/task-list.component.ts
import { Component, Input } from '@angular/core';
import { Task } from './task.model';
...

Then we define the input using the @Input decorator. We'll create a new @Input called childTaskList and specify it will be an array of Task objects by using the Task[] data type:

app/task-list.component.ts
import { Component, Input } from '@angular/core';
import { Task } from './task.model';

@Component({
  selector: 'task-list',
  template: `
  <ul>
  <li [class]="priorityColor(currentTask)" (click)="isDone(currentTask)" *ngFor="let currentTask of childTaskList">{{currentTask.description}} <button (click)="editTask(currentTask)">Edit!</button></li>
  </ul>
  `
})

export class TaskListComponent {
  @Input() childTaskList: Task[];

  isDone(clickedTask: Task) {
    if(clickedTask.done === true) {
      alert("This task is done!");
    } else {
      alert("This task is not done. Better get to work!");
    }
  }

  priorityColor(currentTask){
    if (currentTask.priority === 3){
      return "bg-danger";
    } else if (currentTask.priority === 2) {
      return  "bg-warning";
    } else {
      return "bg-info";
    }
  }
}

Notice we also updated our *ngFor directive to use the new variable name childTaskList in the code above.

Again, adding an @Input() is like creating an inbox for the parent component to pass data downward to its child component. And a component must have an @Input() to accept data from its parent.

Now that the child TaskListComponent is prepared to receive data, we must instruct the parent to send it. We can do this in the <task-list> tags directly:

app/app.component.ts
...
<task-list [childTaskList]="masterTaskList"></task-list>
...

We use [ ] to signify an input. That is, data passing into the component whose tags these brackets are attached to (TaskListComponent, in our case). The name of the child component's @Input() is on the left in [square brackets]. The data being passed from the parent component is on the right in "quotations".

The "masterTaskList" refers to the list of data in the parent. By stating [childTaskList]="masterTaskList" in the <task-list> tags, we are sending the parent's masterTaskList to the child component's childTaskList input. Remember, childTaskList is our @Input. An input is like an inbox where children receive data from their parents.

If we refresh our page in the browser, we should see our Tasks displaying correctly.

This is the "Data down" part of "Data down, actions up." It's an incredibly important part of the Angular framework, so let's review one more time:

  1. Model data resides in the parent component. The parent can pass the data down to a child component by passing that data into the child component's tags.
  2. A child component must have an @Input() defined to accept data from a parent. This @Input() is like an "inbox" where the parent can send that data.

Passing Actions Upward with Event Emitters

Let's resolve the error that occurs when we click the "edit" button next to a Task. Remember, our TaskListComponent doesn't know what editTask() is because the method is defined in our AppComponent, which TaskListComponent can't access.

To fix this, we need to pass an action from our TaskListComponent up to its parent AppComponent when the user clicks a task's "edit" button. Our AppComponent can then take the necessary action to handle this request.

In order to do this, the child component will need to have an @Output to pass an action outwards and upwards to its parent. Let's add an @Output to our child TaskList component so it can pass the edit action on to its parent component. An @Output is what's known as an EventEmitter in Angular2. As the Angular Documentation states, an EventEmitter is a built-in Angular class that is used to send "events" (also known as actions) between components.

Let's add Output and EventEmitter to our child component's import code:

app/task-list.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { Task } from './task.model';
...

Next we'll add an @Output to the file's class declaration. We'll call it clickSender and set it equal to an EventEmitter object.

app/task-list.component.ts
...
export class TaskListComponent {
  @Input() childTaskList: Task[];
  @Output() clickSender = new EventEmitter();
 ...

Now let's change the name of the editTask() template statement in our "Edit" button's event binding, just for clarity.

This event binding will no longer be responsible for displaying the edit form on its own. Instead, it will use its new @Output to alert its parent that a user has requested to edit a Task. The parent will handle displaying the form.

Instead of editTask(), let's call it editButtonHasBeenClicked():

app/task-list.component.ts
...
@Component({
  selector: 'task-list',
  template: `
  <ul>
  <li [class]="priorityColor(currentTask)" (click)="isDone(currentTask)" *ngFor="let currentTask of childTaskList">{{currentTask.description}} <button (click)="editButtonHasBeenClicked(currentTask)">Edit!</button></li>
  </ul>
  `
})
...

While we're here, let's also remove the priorityColor() property binding on our <li> tag. We'll explore an alternate manner of sorting Tasks in an upcoming lesson so we no longer need it here:

app/task-list.component.ts
...
@Component({
  selector: 'task-list',
  template: `
  <ul>
  <li (click)="isDone(currentTask)" *ngFor="let currentTask of childTaskList">{{currentTask.description}} <button (click)="editButtonHasBeenClicked(currentTask)">Edit!</button></li>
  </ul>
  `
})
...

Now that the event binding on our "edit" button calls the editButtonHasBeenClicked() method, let's make sure that method is defined in our TaskListComponent's class declaration:

app/task-list.component.ts
...
export class TaskListComponent {
  @Input() childTaskList: Task[];
  @Output() clickSender = new EventEmitter();

  editButtonHasBeenClicked(taskToEdit: Task) {
    this.clickSender.emit(taskToEdit);
  }
...

The editButtonHasBeenClicked() method simply calls emit() on our @Output. emit() is a built-in EventEmitter method that sends an action upward. By calling emit() on our clickSender and passing in taskToEdit, we're notifying the parent of TaskListComponent that someone has requested to edit a Task and providing the specific Task that needs to be edited.

Finally, we need to tell the parent component to expect this event:

app/app.component.ts
...
<task-list [childTaskList]="masterTaskList" (clickSender)="editTask($event)"></task-list>
...

Remember, [square brackets] represent input, which is information flowing from the parent component into the child component.

(Parentheses) represent output, or actions that are moving out of the child component upwards to the parent component.

We alert the parent component that an action will emit upwards from <task-list> by including (clickSender)="editTask($event)" in the <task-list> tags. This instructs the parent component to execute its editTask() method when the clickSender action is sent up from the child component.

$event is simply how you pass an argument upward. Any arguments sent from a child component to a parent component in an upwards action will be housed in $event when it reaches the parent component.

You may only include one argument when passing an action upwards with an EventEmitter @Output(). If we need multiple pieces of data, we must store them in an array or as key-value pairs in an object.

Review

Let's walk through passing an action upwards one more time:

app/task-list.component.ts
...
 <button (click)="editButtonHasBeenClicked(currentTask)">Edit!</button></li>
...

1. When the "Edit" button is clicked, its event binding triggers the editButtonHasBeenClicked() method from the TaskListComponent's class declaration. We also pass the specific Task the user has requested to edit as an argument to this method.

app/task-list.component.ts
...
  @Output() clickSender = new EventEmitter();

  editButtonHasBeenClicked(taskToEdit: Task) {
    this.clickSender.emit(taskToEdit);
  }
...

2. When triggered, the editButtonHasBeenClicked() method gathers our @Output EventEmitter (named clickSender) and instructs it to send an action upwards with emit(). We pass the taskToEdit as an argument to inform the parent component what Task we are attempting to edit.

app/app.component.ts
...
<task-list [childTaskList]="masterTaskList" (clickSender)="editTask($event)"></task-list>
...

3. (clickSender)="editTask($event) in the line above tells the parent component that when the clickSender EventEmitter from this child emits an action, the parent should respond by calling editTask() and passing $event as an argument. ($event is the taskToEdit argument that came along with the clickSender's action when we called emit().)