We've already been able to inspect the form data in the browser console in our previous application, and Angular provides other mechanisms for data transfer as well.

Still, when dealing with Angular Forms, our focus shifts to FormsModule and FormGroup, which "tracks the value and validity state of a group of FormControl instances".

Initial Form Validation

There are various strategies for validating form input. Our first approach will use the required attribute on the input element.

<input 
  placeholder="Write a task" 
  ngModel 
  name="userInput" 
  required 
/>
Enter fullscreen mode Exit fullscreen mode

As MDN explains, the required attribute, "if present, indicates that the user must specify a value for the input before the owning form can be submitted".

In our scenario, though, it doesn't behave as expected.
Clicking Add will always produce a log entry.

That's due to Angular's default behavior: "By default, Angular disables native HTML form validation by adding the novalidate attribute on the enclosing form tag and uses directives to match these attributes with validator functions in the framework. If you want to use native validation in combination with Angular-based validation, you can re-enable it with the ngNativeValidate directive".

We'll add the ngNativeValidate directive to the form tag and give it a try.

To Do app with Angular Forms - part 2 — figure 1

The results aren't polished, but they're functional.

Rendering the Items

We'll extend the application so the user's additions become visible.

The template file, app.component.html, is where we start.

Below the form, we insert this snippet:

// app.component.html

...

<ul>
  <li *ngFor="let item of todoList">{{ item }}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

What's going on here?

I'll assume you're comfortable with ul and li tags.

The ngFor directive is the more intriguing part. If it's new to you, you can interpret the *ngFor expression as: for each item in todoList, render a fresh <li> and place that item inside it.

Where does todoList come from? We haven't defined it yet. But as you'd expect, todoList is where we keep the user's entries. So we'll introduce an array called todoList in AppComponent.

// app.component.ts

...
export class AppComponent {
  userInput = '';
  todoList = ['Study Angular', 'Add one elememt', 'Correct typo'];

  onSubmit() { ... }
}
Enter fullscreen mode Exit fullscreen mode

Now we update onSubmit so it appends the value of userInput to the todoList array.

// app.component.ts

...
onSubmit() {
    this.todoList = this.todoList.concat(String(form.form.value.userInput));
  }
Enter fullscreen mode Exit fullscreen mode

To Do app with Angular Forms - part 2 — figure 2

Making Things Better

We'll add a few more lines to accomplish the following:

  • todoList becomes an array of objects
  • every object in todoList holds a unique id, a task, and optionally a date
  • items become removable from the UI
// app.component.ts

...
export class AppComponent {
  title = 'Ng To Do';
  userInput: string;
  dateInput: string;
  todoList: { id: number; title: string; date?: string }[] = [
    { id: 1, title: 'Study Angular' },
    { id: 2, title: 'Add one elememt' },
    { id: 3, title: 'Correct typo' },
    { id: 4, title: 'Add dates', date: '2022-09-10' },
  ];

  onSubmit(form: NgForm) {
    this.todoList = this.todoList.concat({
      id: Math.random(),
      title: form.form.value.userInput,
      date: form.form.value.date,
    });
    console.log('Submitted', form.form.value);
  }

  onDelete(id: number) {
    this.todoList = this.todoList.filter((item) => item.id !== id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Keep in mind this isn't necessarily the ideal way to structure forms. We'll begin grouping our controls shortly.

Pay attention to the todoList type: { id: number; title: string; date?: string }[]. It's an array of objects, with each object required to have an id and a title. Adding a question mark after date, as in date?, marks that property as optional.

In onSubmit, we build a fresh object using the values collected from the UI. That object is then appended to todoList.

For deletion, the onDelete method takes an id parameter of type number and removes the corresponding item.

Our template gets updated accordingly

// app.component.html

<h1>{{ title }}</h1>

<form (ngSubmit)="onSubmit(myForm)" #myForm="ngForm" ngNativeValidate>
  <label for="userInput">Add Task</label>
  <input placeholder="Write a task" ngModel name="userInput" required />
  <label for="date">By when</label>
  <input type="date" name="date" ngModel />
  <button type="submit">Add</button>
</form>

<ul>
  <li *ngFor="let item of todoList">
    <button (click)="onDelete(item.id)">X</button>
    {{ item.title }} {{ item.date && 'by' }} {{ item.date ? item.date : '' }}
  </li>
</ul>
Enter fullscreen mode Exit fullscreen mode

Each li element now has its own button. Pressing it calls the onDelete method, passing the id of the item that should be removed.

The expression {{ item.date && 'by' }} {{ item.date ? item.date : '' }} introduces a couple of techniques for conditional rendering.

In JavaScript, the logical AND (&&) works so that the value to the right of && only shows up when the left-hand side evaluates to true.

Another conditional approach is the conditional (ternary) operator.

Combining Form Controls

Angular Forms also allows us to bundle controls together. This can come in handy for organizing data like user profile details or preferences.

Our form is quite compact at the moment, so we'll introduce a description input along with a label.

After that, we wrap everything tied to userInput and taskDescription inside a div. Adding ngModelGroup="taskInfo" to that div groups the contained elements.

// app.component.html

...
<div ngModelGroup="taskInfo">
    <label for="userInput">Add Task</label>
    <input placeholder="Write a task" ngModel name="userInput" required />
    <label for="taskDescription">Description</label>
    <input
      placeholder="Steps to complete the task"
      ngModel
      name="taskDescription"
    />
</div>
Enter fullscreen mode Exit fullscreen mode

We can observe the impact by logging the value object of the form.

To Do app with Angular Forms - part 2 — figure 3

Angular created a taskInfo field, which is itself an object holding the values for userInput and taskDescription.

You'll notice an equivalent structure in the controls. This is quite handy because it carries all the properties from the controls in the group. That means we can apply validation checks, like touched or dirty, across the entire group.

For instance, we could conditionally show certain UI elements only when the group as a whole has been touched.

Summary

Here's what you need to do to work with Angular Forms:

  1. Import FormsModule in AppModule
  2. Use the form tag to wrap all form elements
  3. Declare controls: Declare each control by adding ngModel and the name of the control
  4. Expose form object: Set a local reference equal to ngForm in the form tag #myForm="ngForm"
  5. Submit: Submit the form to pass data to the class. You can use event binding (ngSubmit)="onSubmit(myForm)"
  6. Group controls: You may want to group elements by category. Use ngModelGroup="group-name" to wrap the elements you want to group.