3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 2

Creating a Todo

Accepting user input and using a model

STANDARD

Creating a Todo

We want to create a todo in our application now. When we want to represent some kind of entity in our application like our todo — we also might want to create things like a user, post, comment, article, cart, and so on — we need to consider and define what that entity is exactly.

We can use a TypeScript interface to define what a todo should look like. Let’s create it first, and then we will talk about it.

Create a file at src/app/shared/interfaces/todo.ts and add the following:

export interface Todo {
  title: string;
  description: string;
}

I have created this inside of an interfaces folder, but we will also generally refer to these as models. A model is a way to represent data in your application, and that is exactly what our interface is doing for us.

In TypeScript, we can have types like string and number, but by creating a custom interface called Todo we are essentially creating a new type called Todo that we can use. Using this interface as a type will enforce that the data conforms to the interface we defined — i.e. it must have a title property that is a string, and a description property that is a string.

If I do this:

const myTodo: Todo = {
    title: 'hello',
    description: 'this is a todo'
}

TypeScript will be happy. If I do this:

const myTodo: Todo = {
    title: 'hello',
}

We will get a type error, because our object is missing the description property. This is great for keeping a consistent and well defined notion of what what a todo is and what properties it has, but it’s also great for our general development workflow. Let’s say we have a method that accepts a todo:

saveTodo(todo: Todo){
    console.log(todo.title);
}

This method knows what type the todo parameter is, which is great because it will make sure we are passing it the right type of data, and since the type is known our code editor can give us auto completion information within the saveTodo method.

Creating a Basic Form

We have an idea of what a todo is now. Now we need a way for our user to supply the information to create one.

Create a file at src/app/home/ui/todo-form.component.ts and add the following:

import { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';

@Component({
  standalone: true,
  selector: 'app-todo-form',
  template: `
    <form [formGroup]="todoForm">
      <input type="text" formControlName="title" placeholder="title..." />
      <input
        type="text"
        formControlName="description"
        placeholder="description..."
      />
      <button type="submit">Add todo</button>
    </form>
  `,
  imports: [ReactiveFormsModule],
})
export class TodoFormComponent {
  private fb = inject(FormBuilder);

  todoForm = this.fb.nonNullable.group({
    title: ['', Validators.required],
    description: [''],
  });
}

For the most part, we have the same basic set up as our HomeComponent. But, there are some important differences. First, notice that we have created this component inside of a ui folder. We are going to talk a lot more about this later, but a difference between this component and our HomeComponent is that the HomeComponent is routed to and displayed using the <router-outlet>.

NOTE: We are specifically using a nonNullable form here. We will talk a little more about why later, but the general idea is that when this form is submitted we want to know that title and description will definitely be defined. If we use a nullable form, we would also need to handle the possibility that the form values might not be defined.

This component is not routed to, it will be embedded directly within an existing component (our HomeComponent). Our TodoFormComponent is a presentational or dumb component whose role is to just create and display a form — again, we are going to discuss this concept in detail later.

To create the form, we are using the same techniques we discussed in the basics section. We use the ReactiveFormsModule and FormBuilder to create a basic form, and then we bind that form to our template using [formGroup] and formControlName.

Now we need to embed this form inside of the template of our HomeComponent. As I keep mentioning, we are going to discuss this architecture in more detail later, but the general idea here is that we split up our feature into multiple components with single responsibilities (like displaying a form) so that our single HomeComponent does not need to handle everything. Typically, the role of routed components like HomeComponent will just be to orchestrate how all of the single responsibility components and services it uses work together.

So, we create a separate presentational component, and then we add that to our HomeComponent. The end result is more or less the same, but it improves the architecture and maintainability a lot.

Let’s add our form component to the HomeComponent now.

Modify the HomeComponent to reflect the following:

import { Component } from '@angular/core';
import { TodoFormComponent } from './ui/todo-form.component';

@Component({
  standalone: true,
  selector: 'app-home',
  template: `
    <h2>Todo</h2>
    <app-todo-form />
  `,
  imports: [TodoFormComponent],
})
export default class HomeComponent {}

Notice how light weight the form’s presence is in our home component — in the template we add the entire thing with a single line. Imagine that our HomeComponent were more complex, and we actually had say 5 more components similar to app-todo-form included in it. By having them separated like we did with the todo form, everything will still look small and neat. But if we tried to add everything to the HomeComponent directly instead of breaking it up into parts, this HomeComponent would quickly become massive and hard to maintain.

If you serve your application with ng serve (it’s a good idea to have this running the whole time you are developing your application — that way you can quickly see if there are any errors) you should be able to see the result. It’s always a good idea to have the console up by right clicking the screen and choosing Inspect Element. You will then be able to select the Console tab in the Chrome DevTools which will tell you if there are any errors.

It’s looking pretty basic right now, but we do have our form displayed on the screen!

Capturing User Input

We have our form fields, but now we need a way to take values entered into those fields and do something with them.

If you have been manually writing out the imports, I want you to try something different in this section. You will rarely need to write out imports like this manually:

import { Component } from '@angular/core';
import { TodoFormComponent } from './ui/todo-form.component';

Sometimes the editor tools won’t work properly and you will have to manually write out the import yourself, which might involve having to look up the documentation to see where a specific import comes from. But, most of the time, you can just start writing the code and your code editor will auto complete it for you.

For example, if I start typing:

@Compo...

My editor will give me the option to autocomplete @Component which will also automatically add the import for me. If you are using the Angular language service extension, this will also work with importing components. For example, if you try to add the form component like this:

import { Component } from '@angular/core';

@Component({
  standalone: true,
  selector: 'app-home',
  template: `
    <h2>Todo</h2>
    <app-todo-form />
  `,
})
export default class HomeComponent {}

Note that the TodoFormComponent is not being imported. You will get an error in your template. But you just trigger the code action in your editor for that error, and it will automtaically add the import and and the component to your imports array.

I highly recommend getting used to writing code this way if you are not doing that already, as it will save a lot of time manually writing out imports. Not only does it save you typing but it saves you a lot of time having to look up specifically where you need to import something from.

NOTE: From now on, I am not necessarily always going to show you the appropriate imports to add in code snippets. If you get stuck on where to import something from (e.g. if your auto complete isn’t behaving properly) I would first recommend seeing if you can figure it out if it is being imported from within the application (e.g. like the TodoFormComponent import above). If it is being imported from Angular I would recommend Googling and checking the documentation, as this is what you will be doing in future mostly. If you are still stuck, then you can take a look at the completed source code for this application (the first lesson in any module where we build an application will be have links to the source code and any other resources).

Add an @Output to todo-form.component.ts:

export class TodoFormComponent {
  @Output() todoSubmitted = new EventEmitter<Todo>();

NOTE: There are three things that need to be imported here: Output, EventEmitter, and Todo. See if you can get them to auto import — you can do that by auto completing as you type, or by going back and triggering code actions on the errors after you have typed the line. Also keep in mind that the EventEmitter import must come from @angular/core (there are multiple different EventEmitter types).

Update the template to include an (ngSubmit) event binding that reflects the following:

    <form
      [formGroup]="todoForm"
      (ngSubmit)="todoSubmitted.emit(todoForm.getRawValue())"
    >

We have added an (ngSubmit) event binding that is going to be triggered when a button with the type submit is clicked within the form (which we have already added).

You might often see (ngSubmit) used to call a separate function, e.g:

(ngSubmit)="handleSubmit()"

But, if it is short enough, we can also just trigger the code we want to run directly in the event binding. In this case, we want to take current values in the form and emit them on the todoSubmitted output we created.

Let’s talk a little about nonNullable forms and why we are using this strange sounding getRawValue() method to get the form value rather than the simpler:

todoForm.value

The “problem” is that our output expects to have its emit called with something of the type Todo. This isn’t actually a problem, this is a good thing, because it is what we want and TypeScript is forcing us to do it.

When we use:

todoForm.value

It gives us the values for all of the fields in the form as an object. You might want/expect an object that looks like this:

{
    title: string;
    description: string;
}

However, the type we would get by default is actually:

{
    title: string | undefined | null;
    description: string | undefined | null;
}

…and our output is not going to be happy about that.

The reason for this is that a form control in Angular has the ability to be reset which would make the fields null, and it has the ability to be disabled which would make the fields undefined. We aren’t using these features right now, but we could, and TypeScript knows that these fields have the potential to be undefined or null rather than our desired string values and it wants us to handle those potential cases.

Our @Output is not expecting these types of values, it is expecting a title to always be a string, and a description to always be a string. So, we can either make sure that those values are always strings, or we could change our @Output to accept null and undefined values (but then that will cause complications elsewhere).

The reason we use a nonNullable form is so that our form values can not be null. The reason we use getRawValue() is because it will return us the value of all of the values from our form even if they are disabled. With both of these things in place it means our values can no longer be null, and they can no longer be undefined. That means the type coming from the value of the form is now exactly what we want:

{
    title: string;
    description: string;
}

Another problem we have at the moment is that we are allowing this form to be submitted even when the form is not valid. We are specifically making the title field required with a Validator:

  todoForm = this.fb.nonNullable.group({
    title: ['', Validators.required],
    description: [''],
  });

But we aren’t doing anything to prevent the form being submitted even if there is no title supplied.

A simple fix for this is to simply make the submit button disabled if the valid property of the todoForm is false.

Disable the submit button if the form is not valid:

<button [disabled]="!todoForm.valid" type="submit">Add todo</button>

This valid property will update based on the current validity of the form — this is a feature we get for free from the ReactiveFormsModule.

It’s worth mentioning that we are just keeping things simple here — just disabling the submit in isolation is not generally a good idea. In this case, the user has no obvious indicator that the submit is disabled, and even if they know that it is disabled there is no explanation as to why. We will deal with these sorts of things later in the course.

Now we can listen for the event from the output we just emitted data on in our HomeComponent and handle it.

Update the HomeComponent to reflect the following:

import { Component } from '@angular/core';
import { TodoFormComponent } from './ui/todo-form.component';
import { Todo } from '../shared/interfaces/todo';

@Component({
  standalone: true,
  selector: 'app-home',
  template: `
    <h2>Todo</h2>
    <app-todo-form (todoSubmitted)="createTodo($event)" />
  `,
  imports: [TodoFormComponent],
})
export default class HomeComponent {
  createTodo(todo: Todo) {
    console.log(todo);
  }
}

Now if we check the application, enter some values into the fields, and click the submit button, we should see values like this logged out to the browser console (you can open the browser Dev Tools by right-clicking and choosing Inspect Element, then open the Console):

{title: 'test', description: 'hello'}

For now, we are just logging the values out to the console. In the next lesson, we will actually do something with this value!