Lesson Wednesday

In the Nested Components lesson, we decided the eventual component structure of our application would look like this:

component-planning-diagram-stage-four

Now that we know how to communicate between components by passing data downward with @Inputs and passing actions upwards with @Outputs, let's finish making our child components.

In this lesson, we will create our EditTaskComponent to manage editing Task objects. In the next lesson, we'll add a NewTaskComponent to create new objects directly through our application instead of relying solely on a hard-coded array.

EditTasksComponent

Our new EditTaskComponent will need to know which Task we are editing, display an edit form to do so, and communicate this new information to its parent AppComponent.

We'll begin by creating an edit-task.component.ts file in our app directory and importing Task, Component, Input, Output, and EventEmitter into this file. We'll also move our existing edit form from AppComponent into this new component's template:

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

@Component({
  selector: 'edit-task',
  template: `
    <div>
        <div *ngIf="selectedTask">
          <h3>{{selectedTask.description}}</h3>
          <p>Task Complete? {{selectedTask.done}}</p>
          <hr>
          <h3>Edit Task</h3>
          <label>Enter Task Description:</label>
          <input [(ngModel)]="selectedTask.description">
          <label>Enter Task Priority (1-3):</label><br>
          <input type="radio" [(ngModel)]="selectedTask.priority" [value]="1">1 (Low Priority)<br>
          <input type="radio" [(ngModel)]="selectedTask.priority" [value]="2">2 (Medium Priority)<br>
          <input type="radio" [(ngModel)]="selectedTask.priority" [value]="3">3 (High Priority)
          <button (click)="finishedEditing()">Done</button>
        </div>
      </div>
  `
})

export class EditTaskComponent {

}

Let's load our new component in app.module.ts by importing it at the top of the file and adding it to the array of declarations:

app/app.module.ts
import { NgModule }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent }   from './app.component';
import { FormsModule }  from '@angular/forms';
import { TaskListComponent }  from './task-list.component';
import { EditTaskComponent } from './edit-task.component';


@NgModule({
  imports: [ BrowserModule,
            FormsModule ],
  declarations: [ AppComponent,
                  TaskListComponent,
                EditTaskComponent,
                                        ],
  bootstrap:    [ AppComponent ]
})

export class AppModule { }

We've assigned our new component the selector edit-task. Let's make sure to include <edit-task></edit-task> tags in our root component, in the spot we'd like to render EditTaskComponent. We'll place them right where the edit form was previously located:

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

@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <h1>To Do List for {{month}}/{{day}}/{{year}}</h1>
      <h3>{{currentFocus}}</h3>
      <task-list [childTaskList]="masterTaskList" (clickSender)="editTask($event)"></task-list>
      <hr>
      <edit-task></edit-task>
    </div>
  `
})

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;
  }

}

The EditTaskComponent's class declaration will need to contain any properties or methods included in the template. Let's take another peek:

app/edit-task.component.ts
...
 template: `
    <div>
        <div *ngIf="selectedTask">
          <h3>{{selectedTask.description}}</h3>
          <p>Task Complete? {{selectedTask.done}}</p>
          <hr>
          <h3>Edit Task</h3>
          <label>Enter Task Description:</label>
          <input [(ngModel)]="selectedTask.description">
          <label>Enter Task Priority (1-3):</label><br>
          <input type="radio" [(ngModel)]="selectedTask.priority" [value]="1">1 (Low Priority)<br>
          <input type="radio" [(ngModel)]="selectedTask.priority" [value]="2">2 (Medium Priority)<br>
          <input type="radio" [(ngModel)]="selectedTask.priority" [value]="3">3 (High Priority)
          <button (click)="finishedEditing()">Done</button>
        </div>
      </div>
  `
...

As we can see, this includes the selectedTask property, and the finishedEditing() method. Let's begin with selectedTask.

Remember, "data down, actions up". This data will have to come down from the EditTaskComponent's parent, which is the root AppComponent.

First, let's make sure EditTaskComponent is prepared to receive data from its parent. We'll use the @Input decorator to create the selectedTask property in our EditTaskComponent just as we defined the childTaskList in our TaskListComponent.

Let's change the name of this property from selectedTask to childSelectedTask for clarity. We'll change any other instances of selectedTask in the template to childSelectedTask, too:

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

@Component({
  selector: 'edit-task',
  template: `
    <div>
        <div *ngIf="childSelectedTask">
          <h3>{{childSelectedTask.description}}</h3>
          <p>Task Complete? {{childSelectedTask.done}}</p>
          <hr>
          <h3>Edit Task</h3>
          <label>Enter Task Description:</label>
          <input [(ngModel)]="childSelectedTask.description">
          <label>Enter Task Priority (1-3):</label><br>
          <input type="radio" [(ngModel)]="childSelectedTask.priority" [value]="1">1 (Low Priority)<br>
          <input type="radio" [(ngModel)]="childSelectedTask.priority" [value]="2">2 (Medium Priority)<br>
          <input type="radio" [(ngModel)]="childSelectedTask.priority" [value]="3">3 (High Priority)
          <button (click)="finishedEditing()">Done</button>
        </div>
      </div>
  `
})

export class EditTaskComponent {
  @Input() childSelectedTask: Task;
}

Next, we'll inform the parent AppComponent to pass this data to the child EditTaskComponent by updating the <edit-task></edit-task> tags we just placed in the parent component's template.

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

@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <h1>To Do List for {{month}}/{{day}}/{{year}}</h1>
      <h3>{{currentFocus}}</h3>
      <task-list [childTaskList]="masterTaskList" (clickSender)="editTask($event)"></task-list>
      <hr>
      <edit-task [childSelectedTask]="selectedTask"></edit-task>
    </div>
  `
})
...

Here, we're telling the parent component that the EditTaskComponent has an @Input called childSelectedTask that is prepared to receive data. We instruct the parent to pass its selectedTask property to the child's childSelectedTask @Input.

Remember, whatever is on the left side of the = in the line above (in [square brackets]) refers to the name of the child component's @Input. Child components must have an @Input to receive data from their parent. It's like an "inbox" for incoming data.

Whatever's on the right side of = refers to the property or a method the parent component will place into the child's "inbox", or @Input.

We should now see our edit form appear when we click any of the Task's "Edit" buttons.

Hiding the Form After Editing is Complete

However, our "Done" button isn't hiding the edit form. This is because the EditTaskComponent needs to send an action up to its parent when clicked.

Currently, our "Done" button includes an event binding that triggers finishedEditing(). This method still lives in the parent component. We need to send an action upward from the EditTaskComponent to its parent AppComponent to hide the form.

Let's create another @Output EventEmitter. This will act as a bridge from the child component to the parent.

app/edit-task.component.ts
...

export class EditTaskComponent {
  @Input() childSelectedTask: Task;
  @Output() doneButtonClickedSender = new EventEmitter();
 }

Here, we define a new @Output called doneButtonClickedSender and set it equal to a new EventEmitter.

Let's update our "Done" button to trigger an action called doneButtonClicked().

app/edit-task.component.ts
...
  template: `
    <div>
        <div *ngIf="childSelectedTask">
          <h3>{{childSelectedTask.description}}</h3>
          <p>Task Complete? {{childSelectedTask.done}}</p>
          <hr>
          <h3>Edit Task</h3>
          <label>Enter Task Description:</label>
          <input [(ngModel)]="childSelectedTask.description">
          <label>Enter Task Priority (1-3):</label><br>
          <input type="radio" [(ngModel)]="childSelectedTask.priority" [value]="1">1 (Low Priority)<br>
          <input type="radio" [(ngModel)]="childSelectedTask.priority" [value]="2">2 (Medium Priority)<br>
          <input type="radio" [(ngModel)]="childSelectedTask.priority" [value]="3">3 (High Priority)
          <button (click)="doneButtonClicked()">Done</button>
        </div>
      </div>
  `
})

...

As always, we'll need to define this method in the component's class declaration:

app/edit-task.component.ts
...

export class EditTaskComponent {
  @Input() childSelectedTask: Task;
  @Output() doneButtonClickedSender = new EventEmitter();


  doneButtonClicked() {
    this.doneButtonClickedSender.emit();
  }


}

When our "Done" button is clicked, its event binding will trigger the doneButtonClicked() method. This method will refer to the @Output we've just created (doneButtonClickedSender) and ask it to emit() an action to the parent AppComponent.

Let's ensure AppComponent is prepared to receive this upwards action from its child. We'll update the <edit-task> tags in the AppComponent to include an (output), like this:

app/app.component.ts
...
@Component({
  selector: 'app-root',
  template: `
    <div class="container">
      <h1>To Do List for {{month}}/{{day}}/{{year}}</h1>
      <h3>{{currentFocus}}</h3>
      <task-list [childTaskList]="masterTaskList" (clickSender)="editTask($event)"></task-list>
      <hr>
      <edit-task [childSelectedTask]="selectedTask" (doneButtonClickedSender)="finishedEditing()"></edit-task>
    </div>
  `
})


...

Here, we're instructing AppComponent to expect its child, EditTaskComponent, to send an action up using the doneButtonClickedSender EventEmitter.

By stating (doneButtonClickedSender)="finishedEditing()", we're telling AppComponent that when this action comes upwards, it should run the finishedEditing() method in response.

This method should already be present in our root parent component:

app/app.component.ts
...

  finishedEditing() {
    this.selectedTask = null;
  }
...

Our application should now successfully be able to edit tasks and hide the form afterwards. Great work. In the next lesson, we'll apply these same concepts to a NewTaskComponent that will allow us to create new Task objects directly through our application instead of relying on a hard-coded array.