Skip to content

Deciding What to Build First

When I am building an application I try to first focus on getting straight to the thing that my application, or the feature I am working on, “does”. We are going to want to save the data in our application at some point, but that is not what the application is all about. We could start there, but it’s not the best starting point.

The application is about creating and managing checklists, so as a starting point, it makes sense to focus on being able to create a checklist. But how do we get there? It can be intimidating to take in everything all at once, so just focus on whatever the next step is.

If we want to be able to create a checklist, then there is going to need to be some kind of user interface for the user to do that. What will that look like in our application? Well, in this case, we are going to have some kind of add button the user will click when they want to add a new checklist. Let’s focus on that first.

Create the add checklist button

<header>
<h1>Quicklists</h1>
<button>Add Checklist</button>
</header>

Now we have a button, which currently does nothing. You can go ahead and check for yourself if you like by running:

Terminal window
ng serve

It is a good idea to have this running in the background whilst you are developing your application. The earlier you catch an error the easier it will be to solve. You don’t want to add hundreds of lines of code before you check that your application actually works.

Doing this as a first step might seem a little silly — but it is highly effective especially if you do feel a bit overwhelmed in trying to figure out what to do next.

Create the Checklist interface

Before we continue, it will be useful for us to define the basic structure of what our checklists will look like:

export interface Checklist {
id: string;
title: string;
}

Our checklist is quite simple, all we need is a title and an id. The purpose of the id is to uniquely identify a particular checklist which will allow us to do things like select one specific checklist from a service or associate checklist items with a particular checklist.

Notice that we are breaking our typical convention of folder types here by creating an interfaces folder. As I mentioned before, it is more common to break the typical data-access/ui/utils convention within the shared folder. We could place our interfaces inside of data-access but personally I like having a dedicated folder for the models/interfaces in the application.

Create a modal

Now we need to move on to what we actually want to do when our add button is clicked. In this case, we are going to launch a modal. A modal is a view that pops on top of your current view. Rather than navigating to a different page to add a checklist, we will have the required form appear in a pop up.

This is a sensible feature to implement first, but it also happens to be one of the more complex aspects of this application, and it also means we will be jumping right into using the Angular CDK.

We’ve got to do it at some point anyway though, and we already have a bunch of experience, so let’s get it done.

We are going to start by creating a new shared dumb component that will allow us to show our modal more easily.

Strictly, this isn’t required — we could just use the Angular CDK wherever we need to create a modal. But this will help make our code more reusable, and it will allow us to use the modal/dialog functionality declaratively (because the Dialog that we will be using from the Angular CDK does not use a declarative API). When we want to use something that is not declarative in our applications, we can generally create a wrapper for it that is declarative.

import { Dialog } from '@angular/cdk/dialog';
import {
Component,
contentChild,
input,
TemplateRef,
inject,
effect
} from '@angular/core';
@Component({
selector: 'app-modal',
template: `<div></div>`,
})
export class ModalComponent {
dialog = inject(Dialog);
isOpen = input.required<boolean>();
template = contentChild.required(TemplateRef);
constructor() {
effect(() => {
const isOpen = this.isOpen();
if (isOpen) {
this.dialog.open(this.template(), {
panelClass: 'dialog-container',
hasBackdrop: false,
});
} else {
this.dialog.closeAll();
}
});
}
}

If this code is freaking you out, don’t worry. Again, we have jumped up in complexity quite a bit and things will actually get a little easier after this.

The general idea is that to use a Dialog from the Angular CDK we can inject it and then we call its open method:

this.dialog.open(this.template(), {
panelClass: 'dialog-container',
hasBackdrop: false,
});

Remember how I said that dumb components should generally not inject dependencies? Well, we’re breaking the rule already here! To be fair, that is why I said generally. An important thing to consider here is that this Dialog is sort of a “helper” or “utility” type of thing — by using Dialog this component isn’t really gaining any knowledge of the application more broadly. It is more of a problem when we inject things like state services into our dumb components, which allows the dumb component to reach into areas of the application it shouldn’t know about and that it shouldn’t depend on.

Do you remember when we talked about an ng-template? It was way back in the basics section so I don’t blame you if you have forgotten. But the basic idea was that we could create sections like this in our templates:

<ng-template>
You can't see me!
</ng-template>

Even though we add it to a template, Angular will not actually render it. It creates this little chunk of the template that we can dynamically render at some other point.

One of these templates is exactly what we want to pass to the open method:

this.dialog.open(this.template(), {
panelClass: 'dialog-container',
hasBackdrop: false,
});

We pass the template to the Dialog and it will handle dynamically displaying it in the application for us. The panelClass used here is just a CSS class we want to attach to the dialog so that we can more easily style it later. We disable the backdrop because we don’t want the user to be able to dismiss the dialog without our own custom handling that we will implement later.

But how do we get this template? That is where the second part of this component comes into play (again, remember this is actually pretty advanced, we rarely ever use contentChild, we use viewChild far more often):

template = contentChild.required(TemplateRef);

The component we are creating has a selector of app-modal which will allow us to use it like this:

<app-modal>
<ng-template>
You can't see me... yet!
</ng-template>
</app-modal>

The general idea with contentChild is that it allows us to access the template content that is supplied within the component’s tags. Here we are using app-modal which is the child component we want to pass a TemplateRef to, and we do that by defining an ng-template within <app-modal> and </app-modal>. The ng-template will not be visible. As we discussed, anything we add inside of an ng-template is not visible by default.

However, it will make the TemplateRef for this <ng-template> available to our app-modal component to use however it wants. In our case, our app-modal component accesses this TemplateRef via contentChild. This is in contrast to viewChild which would be used to grab parts of the app-modal’s own template, not a template that is being passed into it.

If we do this:

template = contentChild.required(TemplateRef);

We can get a reference to a TemplateRef (this is what an <ng-template> is) that is supplied inside of the <app-modal> selector. This means that the template class member we are setting up with contentChild here will be whatever template we supplied inside of <app-modal>:

<app-modal>
<ng-template>
You can't see me... yet!
</ng-template>
</app-modal>

That is the whole point of this wrapper component we have created. Rather than having to inject the Dialog wherever we want to use it and imperatively calling this.dialog.open we just supply the template we want to use as above. We have also set up the isOpen input such that we can just toggle that between true and false and it will automatically open and close the dialog for us.

The ability to supply an input to toggle behaviour like this is often a great help in keeping things declarative. Eventually, we will supply a signal as the input for isOpen and — just as is usually the case with the declarative style — whenever that isOpen signal changes, the dialog will just automatically react to that.

Let’s try using it now.

import { Component } from '@angular/core';
import { ModalComponent } from '../shared/ui/modal.component';
@Component({
selector: 'app-home',
template: `
<header>
<h1>Quicklists</h1>
<button>Add Checklist</button>
</header>
<app-modal [isOpen]="false">
<ng-template> You can't see me... yet </ng-template>
</app-modal>
`,
imports: [ModalComponent],
})
export default class HomeComponent {}

Perhaps you can see how we now have this nice “declarative” way to display a modal. We just add app-modal to the template, and we can toggle its open/closed state with its isOpen input.

After all that work, it might be disappointing to see that nothing has actually changed if we serve the application. We can’t see the content of our <ng-template> and clicking the add button does nothing. We’ll deal with that in just a moment.

We will also eventually replace the contents of this modal with an actual form to create a checklist, but we will just use a placeholder for now.

Allow the modal to be opened and closed

Now we need to implement a mechanism to control opening and closing the modal.

export default class HomeComponent {
checklistBeingEdited = signal<Partial<Checklist> | null>(null);
}

In general, I will not be telling you where to import things from, but I will for this first example. See if you can figure it out by yourself first, and if not the import will be listed below.

Make sure to add these imports at the top of the HomeComponent file:

import { Component, signal } from '@angular/core';
import { ModalComponent } from '../shared/ui/modal.component';
import { Checklist } from '../shared/interfaces/checklist';
<button (click)="checklistBeingEdited.set({})">Add Checklist</button>
<app-modal [isOpen]="!!checklistBeingEdited()">

What we have done here might be a little bit surprising. Perhaps you would expect an isOpen signal that, we just switch between true and false and use that as our isOpen input.

This would work, but it is a bit more of an imperative way to do things — we would need to imperatively call:

this.isOpen.set(true);

Our application isn’t just automatically reacting, we are telling it how to do things still. Instead, we should consider more carefully: when should the modal be open?

It should be open when either the user has clicked the add button, or if they are currently editing one of their existing checklists.

So, we create this checklistBeingEdited signal:

checklistBeingEdited = signal<Partial<Checklist> | null>(null);

If we are editing a checklist we can set checklistBeingEdited to be whatever Checklist is being edited. If we want to create a new checklist then we can just set it to an empty object {} (which is what we are already doing). That is why we use Partial for the type here — this is a utility type that will allow us to supply any or none of the properties for the Checklist. If we didn’t do this, TypeScript would complain about us supplying an empty object {}.

If we are not adding or editing a checklist, we can set this to null. Our modal will react to these changes appropriately by opening or closing the dialog:

<app-modal [isOpen]="!!checklistBeingEdited()">

The reason we use the “NOT NOT” syntax here: !! is to convert our signal value to a boolean. If we supply a Checklist or an empty checklist {} it will result in a true value, if we supply a null it will result in a false value. This will result in the behaviour we want:

  • Open for Checklist or {}
  • Closed for null

Try clicking the button in the application now and you should see that the modal opens.

You should see that the modal opens, but currently there is no way to close the modal.

We are actually going to handle closing/dismissing the modal using another component! We are going to create a generic form-modal component that we will use in multiple places in the application. We will have that component emit a close event when it wants the modal to close, and we will react to that event by setting our signal to null.

In the next lesson, we are going to tackle building this form-modal component.