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 but 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 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 compilation 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 that 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 { ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { IonicModule } from '@ionic/angular';

@Component({
  selector: 'app-todo-form',
  template: `
    <form [formGroup]="todoForm">
      <ion-input
        type="text"
        formControlName="title"
        placeholder="title..."
      ></ion-input>
      <ion-input
        type="text"
        formControlName="description"
        placeholder="description..."
      ></ion-input>
    </form>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TodoFormComponent {
  public todoForm = this.fb.group({
    title: ['', Validators.required],
    description: [''],
  });

  constructor(private fb: FormBuilder) {}
}

@NgModule({
  declarations: [TodoFormComponent],
  exports: [TodoFormComponent],
  imports: [IonicModule, ReactiveFormsModule],
})
export class TodoFormComponentModule {}

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 <ion-router-outlet>. This component is not routed to, it will be embedded directly within an existing component (our HomeComponent) and thus has no routing information in its module. 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. 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.

First, to be able to use our new custom component inside of the HomeComponent, we will need to import the TodoFormComponentModule inside of the HomeComponentModule.

Modify the HomeComponentModule at the bottom of the home.component.ts file to reflect the following:

@NgModule({
  declarations: [HomeComponent],
  imports: [
    IonicModule,
    CommonModule,
    TodoFormComponentModule,
    RouterModule.forChild([
      {
        path: '',
        component: HomeComponent,
      },
    ]),
  ],
})
export class HomeComponentModule {}

Notice that the TodoFormComponentModule is included now. You will also need to add an import for this to the top of the home.component.ts file:

import { TodoFormComponentModule } from './ui/todo-form.component';

But, if you are manually typing out TodoFormComponentModule within the @NgModule you should get auto complete suggestions popping up (I am assuming your are using VS Code). If you select one of those by hitting the TAB button on your keyboard, it should auto complete and automatically add the import to the file for you. If it does not do that, you will get a squiggly red line saying:

Cannot find name 'TodoFormComponentModule'

If you hover over that, you can also automatically set up the import at the top of your file rather than typing it out manually. You can hover over it, select Quick Fix and there will be an option to automatically import. You can also just hit Cmd + . when your cursor is over the error to access quick fix as well. You don’t have to do this, but if you can get used to using auto complete to automatically import things for you it drastically speeds up your workflow. Sometimes auto complete just doesn’t work and you have to do it manually anyway, in these cases you might be forced to look up where something is imported from in the Angular or Ionic documentation.

Now that our HomeComponentModule knows about the TodoFormComponentModule we can use it inside of our HomeComponent.

Modify the template in home.component.ts to reflect the following:

    <ion-header>
      <ion-toolbar>
        <ion-title>Todo</ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content>
      <app-todo-form></app-todo-form>
    </ion-content>

If you serve your application with ionic 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.

For this section, see if you can get everything automatically importing by yourself using the method we just discussed. Start typing and VS Code should automatically pop up suggestions for you. Don’t write the rest yourself, just hit TAB to auto complete and it will auto import for you.

However, if you get stuck, the imports you will need at the top of your form component for this section are:

import { Component, EventEmitter, NgModule, Output } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { Todo } from '../../shared/interfaces/todo';

NOTE: I am generally going to stop showing you the imports for everything unless it is something new/different. If you get stuck on where to import something from I would first recommend seeing if you can figure it out if it is being imported from within the application (e.g. like the Todo import above). If it is being imported from Ionic or 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 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. 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 and a submit button:

    <form [formGroup]="todoForm" (ngSubmit)="handleSubmit()">
      <ion-input
        type="text"
        formControlName="title"
        placeholder="title..."
      ></ion-input>
      <ion-input
        type="text"
        formControlName="description"
        placeholder="description..."
      ></ion-input>
      <ion-button type="submit">Add Todo</ion-button>
    </form>

Create a handleSubmit method inside of the TodoFormComponent class

  constructor(private fb: FormBuilder) {}

  handleSubmit() {
    const value = this.todoForm.value;

    if (this.todoForm.valid && value.title && value.description) {
      const todo: Todo = {
        title: value.title,
        description: value.description,
      };

      this.todoSubmitted.emit(todo);
    }
  }

We’ve set up a few things here. 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 also added). We have this event binding calling a handleSubmit method whose role is to take the current values in the form and emit them on the todoSubmitted output we created.

We need to handle a bit of funkiness to keep TypeScript happy here. Ideally, it would be nice to just do something like this:

  handleSubmit() {
    if (this.todoForm.valid) {
      this.todoSubmitted.emit(this.todoForm.value);
    }
  }

And you could do this if not for types. It might seem like TypeScript is annoying and gets in the way, but it is only really pointing out potential problems with your code and gives you a chance to fix it before you ship your code to production and find out about it in the form of some bug/error.

The reason for this extra stuff, is the values we are getting from:

this.todoForm.value

This 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 are getting from the form is actually:

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

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 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).

This is why we do this:

  handleSubmit() {
    const value = this.todoForm.value;

    if (this.todoForm.valid && value.title && value.description) {
      const todo: Todo = {
        title: value.title,
        description: value.description,
      };

      this.todoSubmitted.emit(todo);
    }
  }

We are checking that those values are defined/not-null with our if statement, and then we construct a Todo with those values to emit. We are just keeping things simple here for now, but later on we will look at some more advanced ways to handle this situation.

Now we can listen for that event in our home component and handle it.

Update the HomeComponent to reflect the following:

@Component({
  selector: 'app-home',
  template: `
    <ion-header>
      <ion-toolbar>
        <ion-title>Todo</ion-title>
      </ion-toolbar>
    </ion-header>

    <ion-content class="ion-padding">
      <app-todo-form (todoSubmitted)="createTodo($event)"></app-todo-form>
    </ion-content>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HomeComponent {
  createTodo(todo: Todo) {
    console.log(todo);
  }
}

NOTE: Once again, you are going to have to make sure you import Todo in this file.

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

In the next lesson, we will actually do something with this value!