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({
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({
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({
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
totodo-form.component.ts
:
export class TodoFormComponent {
todoSubmitted = output<Todo>();
NOTE: There are two things that need to be imported here: output
,
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.
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({
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!