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 thehome.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
totodo-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 theTodoFormComponent
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!