Lesson Wednesday

As we discussed in the Nested Components lesson, we eventually want our application to contain three components, each with their own dedicated responsibility

component-planning-diagram-stage-four

So far we've successfully created an EditTaskComponent and TaskListComponent, including all necessary @Inputs and @Outputs to pass data down and actions up. In this lesson, we'll create our NewTaskComponent. It will contain a form that will allow us to create new Task objects through our application directly instead of hard-coding an array of Tasks.

NewTaskComponent

First, we'll create a new component file called new-task.component.ts and import Component, Output, and EventEmitter from the Angular core, as well as our Task model at the top of the file.

We'll also add the @Component decorator and annotation. This should all be review:

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

@Component({
  selector: 'new-task',
  template: `
    <h1>New Task</h1>
  `
})

export class NewTaskComponent {

}

Next, we'll need to load the new component in our app.module.ts file and include it in the declarations array in order to make it available to our root module:

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';
import { NewTaskComponent } from './new-task.component';

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

export class AppModule { }

We'll also need to place our <new-task></new-task> tags where we'd like to render our NewTaskComponent. Let's place them under our <edit-task></edit-task> tags in the root component's template:

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>
      <new-task></new-task>
    </div>
  `
})
...

Finally, our NewTaskComponent's template will require a form to collect information for new Tasks. It will need fields for a description, and priority:

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

@Component({
  selector: 'new-task',
  template: `
  <h1>New Task</h1>
   <div>
     <label>Enter Task Description:</label>
     <input #newDescription>
   </div>
   <div>
    <label>Task Priority:</label>
    <select #newPriority> 
      <option [value]="1"> Low Priority </option>
      <option [value]="2"> Medium Priority </option>
      <option [value]="3"> High Priority </option>
    </select>
    </div>
  `
})

export class NewTaskComponent {

}

We've included a basic input field for our description and a select drop-down menu for our priority. Even though our "Edit" form used radio buttons for the priority, we've opted for a select field here in order to practice using multiple form inputs in Angular.

Template Reference Variables

Notice the # symbols included in the form above. These are template reference variables. As the name suggests, they refer to a DOM element (or Angular directive) within a template. Template reference variables save the HTML element they're attached to into the variable name provided.

That means that by including a # in a form field, we're telling Angular to place that entire form field, including any content inserted into the field by the user, into the variable named after the #.

By adding the template reference variable #newDescription to this field: <input #newDescription>, we'll be able to save a user's input in a newDescription variable. We can then retrieve the user input by calling newDescription.value.

When a user selects an option from the dropdown, this choice will be saved to #newPriority. We can then retrieve the user's input by calling newPriority.value:

app/new-task.component.ts
...
<label>Task Priority:</label>
<select #newPriority> 
  <option [value]="1"> Low Priority </option>
  <option [value]="2"> Medium Priority </option>
  <option [value]="3"> High Priority </option>
</select>
...

Note the different syntax here. Form fields like <select> drop-downs and radio buttons are formatted a little differently than text fields. They have values added like this: [value]="3".

This is because we cannot gather user input from radio buttons or drop-downs in the same manner we do with a text box. We need to first determine which option the user selected, then gather the value from that option. This is why we include property bindings that alter the value of the #newPriority template reference variable depending on which option the user selects from the drop-down list.

Template Reference Variables versus Two-Way Data Binding

Keep in mind that template reference variables are generally used in forms that create new objects while two-way data binding is used in forms that edit existing objects.

This is because while we're filling out a form to create a new object, that object does not yet exist. Therefore, we cannot bind our data to the object in the same manner we bind edit form data to existing models.

Instead, we use template reference variables to retrieve user input from the form so we can provide this information to a constructor to create a new object.

For more information on template reference variables, check out the Angular Documentation.

Creating New Objects with Form Data

Let's add a button to our form. It'll include an event binding that will trigger a method to create a new Task and add it to our overall list of all Tasks. The method will take the user's form input as arguments:

app/new-task.component.ts
...
  <h1>New Task</h1>
   <div>
     <label>Enter Task Description:</label>
     <input #newDescription>
   </div>
   <div>
    <label>Task Priority:</label>
    <select #newPriority> 
      <option [value]="1"> Low Priority </option>
      <option [value]="2"> Medium Priority </option>
      <option [value]="3"> High Priority </option>
    </select>
    <button (click)="submitForm(newDescription.value, newPriority.value)">Add</button>
   </div>
...

Now we can declare the new method in our component class and use the two values to create a new Task object:

app/new-task.component.ts
...
export class NewTaskComponent {
  submitForm(description: string, priority: number) {
    var newTaskToAdd: Task = new Task(description, priority);
  }
}

Next, we'll need to send our newTaskToAdd object upwards to the parent AppComponent where the rest of our data resides. We'll add it to our existing array of all Tasks.

Remember, we need an @Output to pass an action up. Let's call it newTaskSender. We'll emit the new Task object in our submitForm() method, right after we've created the object:

app/new-task.component.ts
...
export class NewTaskComponent {
  @Output() newTaskSender = new EventEmitter();

  submitForm(description: string, priority: number) {
    var newTaskToAdd: Task = new Task(description, priority);
    this.newTaskSender.emit(newTaskToAdd);
  }
}

You may wonder why we aren't just sending the description and priority values to the parent AppComponent and relying on it to construct the new Task. As mentioned in the Data Down, Actions Up with Inputs and EventEmitters lesson, EventEmitters can only send one argument at a time. If we have multiple pieces of information that we need to send up, we must bundle them in an object or an array.

Because our goal is to create a new Task object anyway, it makes sense to construct it here before sending it upwards. Since this component's distinct responsibility is to create new Tasks, this is an appropriate place to instantiate a new instance of Task.

Let's inform the parent AppComponent to expect this event to move upwards from its child NewTaskComponent. We'll update the <new-task> tags in the parent component to the following:

app/app.component.ts
...
    <new-task (newTaskSender)="addTask($event)"></new-task>
...
  • Here we tell AppComponent to expect an action to come upwards from its NewTaskComponent child component.

  • We specify that the action will come from an @Output called newTaskSender.

  • Additionally, we instruct the parent component that when this action occurs, it should trigger an addTask() method, passing in the argument newTaskSender brought along.

Let's define this addTask() method on our AppComponent next. It will be responsible for adding the new Task to the array of all Tasks:

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

  addTask(newTaskFromChild: Task) {
    this.masterTaskList.push(newTaskFromChild);
  }

}

We use the standard JavaScript push method to add the new Task to our masterTaskList.

If we load our page in the browser, we should see our new task form. If we fill out this form and submit it, we should see new Task objects appear in our list.

Clearing Form After Submission

Finally, let's clear our form's text field after we submit it. We can do this by modifying the existing template statement in our button's event binding.

app/new-task.component.ts
...
    <button (click)="submitForm(newDescription.value, newPriority.value); newDescription.value='';">Add</button>
...

As you may recall from the Event Binding lesson, an event binding's template statement can contain both method calls and property assignments. Here we include a property assignment along with our method call. We set newDescription.value back to an empty string.

When you include multiple actions in an event binding's template statement, such as when we use two property assignments in the example above, you must separate the actions with semicolons.

We should be able to launch our application and use our new button to clear the form after creating an object. Fantastic!

In the next lesson, we'll explore pipes that can be used to live-filter data right on the page and put the finishing touches on our To Do application.