Lesson Tuesday

In the last two lessons, we learned about two types of one-way data binding: event binding and property binding. This lesson will explore how we can combine these to create two-way data binding.

We'll explore what two-way data binding is, how it works, and how best to use it. Then we'll implement this new concept ourselves to create a live-updating edit form in our To Do List. Let's get started!

Two-Way Data Binding

As the name suggests, actions that occur as a result of two-way binding involve information flowing two different ways. What does this mean? Well, you'll recall that event binding is a type of one-way data binding. Its flow of data looks something like this:

one-way-data-binding-diagram

Property binding is another type of one-way data binding. However, when a property binding is executed, data flows the opposite direction, like this:

property-binding-diagram

Two-way data binding combines the powers of both event binding and property binding into a single action. This allows us to dynamically alter template values and properties when certain events occur. This process looks something like:

angular-two-way-data-binding-diagram

Here, the same event binding steps still occur, but after the template statement is evaluated, data then flows back in the other direction. Angular updates the same template whose event binding triggered this process. This allows us to display data, trigger an action to update that data, and see these changes automatically reflected in the DOM.

Two-Way Bindings and Form Data

Two-way data binding is most commonly used in forms. As the user edits or inputs information into a form field, the template automatically updates to display what they've provided. Let's explore exactly how this works.

(This next part is only for demonstration purposes. You don't need to add this code to your To Do List. We'll update our application together soon.)

Let's assume we have a basic form with a single input field in an Angular component template.

<form>
  <input type="text" id="name">
</form>

The input field collects a user's name. We want to display that name in a welcome message above the form:

Welcome, {{name}}!

<form>
  <input type="text">
</form>

We can add an event binding to <input> that will listen for the user's text input by assigning input as the event binding's target event. (input is a valid DOM event. For more details, see here).

We can also add a property binding that will update the <input> field's value property dynamically:

Welcome, {{name}}!

<form>
  <input [value]="name" (input)="name = $event.target.value" type="text">
</form>

(input)="name = $event.target.value" is our event binding. input is the target event, and name = $event.target.value is our template statement. ($event.target.value is special Angular code that retrieves the contents of the form field. Don't worry about remembering this. We'll learn a shortcut before the lesson is over.)

So far we've only called methods in our template statements. We can assign variables in template statements, too. Remember, the definition of a template statement from the Event Bindings Lesson is "The template statement is a method call or property assignment that runs when the target event occurs."

This event binding will define name as the contents of the <input> field. If the user types "Dan", thename variable will be "Dan". If the user types "Delilah", it will contain "Delilah".

[value]="name" is our property binding. It sets the HTML value property of <input> to the "name" variable. (Remember, property bindings can use both methods and variables as their template expressions).

When the user begins entering information into the form field, the event binding will trigger. This will define the name variable as the current contents of the <input> field. By defining name, we are then also defining the "name" used in the property binding. The value of the <input> field always mirrors the contents of the form:

dynamic-welcome-message-example-1

Or, like this:

dynamic-welcome-message-example-2

Because the field's value property is defined as the contents of the field, we could also dynamically hide and show the form and its contents would remain.

To recap, two-way data binding is the combination of an event-binding handling the flow of data from the template to the back-end of the component and a property binding managing the process of updating an HTML property on the template after this action has occurred.

ngModel Shortcut

But the example above was a little long, wasn't it? Especially that funky $event.target.value portion. Thankfully, Angular includes a built-in directive to implement two-way data binding using far less code. It's called ngModel.

Let's revisit our example form:

Welcome, {{name}}!

<form>
  <input [value]="name" (input)="name = $event.target.value" type="text">
</form>

Using the ngModel directive, we could refactor the form above to look like this:

Welcome, {{name}}!

<form>
  <input [(ngModel)]="name" type="text">
</form>

When bound to an <input> element, ngModel automatically sets that element's value property and the property listed to the right of = (which is name in the example above). This means whatever is input into the field will both be assigned as the <input>'s value property and be assigned to the name variable in the welcome message.

It is both an event binding that listens for input events and a property binding that updates the DOM when those events occur.

Notice the code above includes both the [square brackets] of property bindings and the (parentheses) of event bindings. This is required to implement ngModel because it is the combination of an event binding and property binding in one fluid action.

It might be tricky to remember which set of brackets is inside of which. Angular's own documentation suggests you associate two-way binding with "a banana in a box" because the parentheses always reside within the square brackets, which resembles a banana sitting in a box. [()]

Moving forward, we will use the ngModel directive to implement two-way data binding in our forms.

Two-Way Data Binding in To Do List

Now that we've walked through the basics of two-way binding and the ngModel directive, let's use these concepts to create a dynamic edit form in our To Do List.

First, we'll need to let Angular know what Task is currently selected so it knows which details to edit. We can add a property to our class to keep track of this:

app/app.component.ts
...
export class AppComponent {
  ...
  tasks: 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)  ];
  selectedTask: Task = this.tasks[0];

  editTask() {
    alert("You just requested to edit a Task!");
  }
}

...

We define a selectedTask property, declare that it will be of the Task type, and set it to the first Task in our tasks array.

Let's add an area to display the selectedTask's details in our template, including an edit form using ngModel:

app/app.component.ts
...
  template: `
      <div class="container">
        <h1>To Do List for {{month}}/{{day}}/{{year}}</h1>
        <h3>{{currentFocus}}</h3>
        <ul>
          <li [class]="priorityColor(currentTask)" (click)="isDone(currentTask)" *ngFor="let currentTask of tasks">{{currentTask.description}} <button (click)="editTask()">Edit!</button></li>
        </ul>
        <hr>
        <div>
          <h3>{{selectedTask.description}}</h3>
          <p>Task Complete? {{selectedTask.done}}</p>
          <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)
       </div>
     </div>
  `
})
...

Let's look at our two form fields closely.

First, we've added an <input> field where the user can update the Task's description. It looks like this: input [(ngModel)]="selectedTask.description">.

Remember, ngModel is both an event binding and a property binding. When the user begins inputting text into this form field, the contents of the form will be assigned to both selectedTask.description (the description property of the current selectedTask) and the DOM will be updated with the new value.

We also include three radio buttons to update the Task's priority attribute. You'll notice that in addition to including [(ngModel)]="selectedTask.priority", they also have a property binding ([value]="1"). This is because using radio buttons with ngModel is a bit of a special circumstance. We cannot assign the value property of each radio button to the current priority property of the selectedTask` because HTML radio buttons require predefined values to select from. So, we include an additional property binding to set the available values a user can choose.

Note: For more information on using ngModel in conjunction with non-text form fields, check out this blog post: "How to Deal with Different Form Controls in Angular 2".

However, if we attempt to view our new form in the browser, we receive a console full of errors!

ngmodel-errors

This is because Angular doesn't automatically know what ngModel means. We must import the piece of Angular's core code that defines this directive into our application.

We do this by importing the FormsModule into our app.module.ts file. Code for the ngModel directive resides in the FormsModule because two-way binding with ngModel is almost always used in forms.

We add an import statement at the top:

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

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

export class AppModule { }

Then we must also include the module we've just imported into the file in the imports array, like this:

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

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

export class AppModule { }

The import statement provides this file access to the FormsModule. This file imports the FormsModule so that it is available to all components in this module. Our root AppComponent resides in this module, so it will have access to FormsModule as well as ngModel.

If we refresh our page, we should see our new edit form.

to-do-list-with-edit-form

If you update the form now, it will automatically update the DOM.

We'll want to view and edit other Tasks, too, not just the first one. Let's use the editTask() method to redefine selectedTask as the Task with the most recently clicked edit button.

app/app.component.ts
...
export class AppComponent {
  ...

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

}
...

As you can see, editTask() now takes the specific Task the user selects as an argument. It redefines the component's selectedTask property to the Task whose "Edit" button the user clicked.

Next, we just need to include an argument when we call this method in the existing event binding:

app/app.component.ts
...
  template: `
    <div class="container">
     <h1>To Do List for {{month}}/{{day}}/{{year}}</h1>
     <h3>{{currentFocus}}</h3>
     <ul>
      <li [class]="priorityColor(currentTask)" (click)="isDone(currentTask)" *ngFor="let currentTask of tasks">{{currentTask.description}} <button (click)="editTask(currentTask)">Edit!</button></li>
    </ul>
...

When we refresh the page, we can click a Task and see our edit form update automatically. If we edit any Task with our new form, the DOM updates to display the new information immediately.

Use two-way binding and the ngModel directive in all your edit forms moving forward. In the next lesson, we'll clean up our UI further by learning about new directives to dynamically hide and show areas of our template.


Example GitHub Repo for Angular 2 To Do List