Building a Basic To-Do List App with Angular v17


Simplicity for Understanding

In this blog post, I’ll dive into the basics of Angular v17 by building a straightforward to-do list application. Whether you’re a beginner or simply looking for a refresher, this guide will provide a solid starting point.

To keep the focus on core Angular concepts, we’ll build our entire app directly within the app.component.ts file. While real-world Angular projects typically utilize a more structured component architecture, our approach simplifies the learning process.

What You’ll Need

This post assumes you have a basic grasp of: HTML, CSS, TypeScript.

Project Setup

To get started, use the Angular CLI to create a new project. In this case I will use CSS for styling.

Bash
ng new my-todo-app 

Once your project is generated, change into the project directory, and start the VS Code.

Bash
cd my-todo-app && code .

Coding the To-Do List in app.component.ts

For this post, I’ll contain our to-do list logic within the app.component.ts file. The initial code looks like:

JavaScript
// app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  templateUrl: './app.component.html',
  styleUrl: './app.component.css'
})
export class AppComponent {
  title = 'to-do';
}

First let’s modify the app.component.ts file to include a title, mockup to-do list Array “allItems”.

Update “templateUrl: ‘./app.component.html’, styles: Styles” to “template: Template, styles: Styles”.

Create two consts: “Template” and “Styles” where I will add more HTML and CSS into it later.

Add a very basic HTML to the Template. It simply displays an <h1> heading with the title of your to-do application. Now, the code looks like:

JavaScript
// app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

const Template = `
<div>
  <h1>{{ title }}</h1>
</div>
`;

const Styles = ``;

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  template: Template,
  styles: Styles
})
export class AppComponent {
  title = 'Angular To-Do List';

  allItems = [
    { id: 1, description: 'eat', done: true },
    { id: 2, description: 'sleep', done: false },
    { id: 3, description: 'play', done: false },
    { id: 4, description: 'laugh', done: false },
  ];
}

Open terminal (or the command prompt). Ensure you are still in the project directory (my-todo-app or whatever you named it).
Run “ng serve” to test that everything so far is working. When you open your app (usually at http://localhost:4200/), you should see a page with the heading “Angular To-Do List” at the top.

Next, let’s add a get items() method to dynamic generate the list, with filters and sort by id method. I also edit the Template and include filter buttons, check box, and description of the to-do.

Filtering Logic:
– “all”: Returns all the items without filtering.
– “active”: Returns only the items where item.done is false (not completed tasks).
– “done”: Returns only the items where item.done is true (completed tasks).

The sort by id method sort the list by the id in descendent order.

Now the the app.component.ts looks like following: (Note: I am using Angular V17 new template syntax)

JavaScript
// app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

const Template = `
<div>
  <h1>{{ title }}</h1>
  <div>
    <button (click)="filter = 'all'">All</button>
    <button (click)="filter = 'active'">Active</button>
    <button (click)="filter = 'done'">Done</button>
  </div>
  <div>
    <ul>
      @for (item of items; track item.id) {
        <li>
          <input type="checkbox" [checked] = "item.done" (change)="toggleItem(item.id)">
          {{ item.description }}
        </li>
      }
    </ul>
  </div>
</div>
`;

const Styles = ``;

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  template: Template,
  styles: Styles
})
export class AppComponent {
  title = 'Angular To-Do List';
  filter: "all" | "active" | "done" = "all";

  allItems = [
    { id: 1, description: 'eat', done: true },
    { id: 2, description: 'sleep', done: false },
    { id: 3, description: 'play', done: false },
    { id: 4, description: 'laugh', done: false },
  ];

  get items() {
    return this.allItems
      .filter(
        this.filter === "all" ? item => item :
          this.filter === "active" ? item => !item.done :
            item => item.done,
      )
      .sort(
        this.sortById
      );
  }

  sortById(a: { id: number }, b: { id: number }) {
    return b.id - a.id;
  }
}

The app page will automatically refresh and show the changes.

Next, I will add two methods to our TypeScript logic: addItem() and removeItem(). The addItem() method lets us add a new task to the beginning of our list, while removeItem() filters out a task based on its ID. To use these methods, I’ve updated the HTML template. I’ve included an input field () with a “Add” button to capture new tasks, and a “Delete” button next to each task so we can remove them if needed.

JavaScript
// app.component.ts
...

const Template = `
<div>
...
  <div>
    <input #newItem placeholder="Add an to-do item" type="text" (keyup.enter)="addItem(newItem.value); newItem.value=''"
      id="addItemInput" />
    <button (click)="addItem(newItem.value); newItem.value=''">Add</button>
  </div>
  <div>
    <ul>
      @for (item of items; track item.id) {
        <li>
          <input type="checkbox" [checked] = "item.done" (change)="toggleItem(item.id)">
          {{ item.description }}
          <button (click)="removeItem(item.id)">Delete</button>
        </li>
      }
    </ul>
  </div>
</div>
`;

...

export class AppComponent {
...

  addItem(description: string) {
    if(description.length > 0) {
      this.allItems.unshift({
        id: this.allItems.length +1,
        description,
        done: false,
      })
    }
  }

  removeItem(id: number) {
    this.allItems = this.allItems.filter((item) => item.id !== id);
  }
}

To edit existing items directly, I’ve introduced a few new TypeScript methods to help achieve this. The toggleEditable() method handles switching an item into or out of edit mode, the toggleItem() method marks a task’s completion status, and saveItem() updates the description of an item after editing.

To display the editing interface, I’ve updated the HTML template with some conditional logic. If a task’s ID matches the currentlyEditedId, it show an input field and a ‘Save’ button. Otherwise, it display the task description as a span. Notice how the (change), (click), and (keyup.enter) events tie into the editing process, allowing user to toggle the done status, enter edit mode, and save changes respectively.

JavaScript
// app.component.ts
...

const Template = `
<div>
...
  <div>
    <ul>
      @for (item of items; track item.id) {
        <li>
          <input type="checkbox" [checked] = "item.done" (change)="toggleItem(item.id)">
          @if (currentlyEditedId == item.id) {
            <input type="text" #editItem [value]="item.description" (keyup.enter)="saveItem(item.id, editItem.value)"/>
            <button (click)="saveItem(item.id, editItem.value)">Save</button>
          }
          @else {
            <span (click)="toggleEditable(item.id)">
              {{ item.description }}
            </span>
            <button (click)="removeItem(item.id)">Delete</button>
          }
        </li>
      }
    </ul>
  </div>
</div>
`;

...

export class AppComponent {
...
  currentlyEditedId: number | null = null;
  
...
  toggleItem(id: number) {
    this.allItems.filter((item) => {
      item.id === id ? item.done = !item.done : item.done;
    })
  }

  saveItem(id:number, description: string) {
    const item = this.allItems.find((item) => item.id === id);
    if (item) {
      item.description = description;
    }
    this.currentlyEditedId = null; 
  }

  toggleEditable (id: number) {
    this.currentlyEditedId = this.currentlyEditedId === id ? null : id;
  }
}

See It in Action

Try it out! Now you can click on any to-do list item to edit it. Press ‘Enter’ on the keyboard or click the ‘Save’ button to save your changes.

Recap

  • We started with a simple Angular to-do list app, focusing on the core structure within the app.component.ts file.
  • We’ve built out the essential functionality to add, delete, and filter to-do items (all, active, done).
  • We implemented the ability to edit existing to-do items directly within the list, making the app more interactive and user-friendly.
  • You can find the complete code for this project so far on GitHub

The completed app.component.ts file will be as following:

JavaScript
// app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';

const Template = `
<div class="container">
  <div class="to-do-wrapper">
    <h1 class="title">Angular To Do List</h1>
    <br>
    <div class="row">
      <span>Filter:</span>
      <button (click)="filter = 'all'">All</button>
      <button (click)="filter = 'active'">Active</button>
      <button (click)="filter = 'done'">Done</button> 
    </div>
    <div class="row">
      <input #newItem placeholder="Add an to-do item" type="text" (keyup.enter)="addItem(newItem.value); newItem.value=''"
        id="addItemInput" />
      <button (click)="addItem(newItem.value); newItem.value=''">Add</button>
    </div>

    <ul>
      @for (item of items; track item.id){
      <li>
        <div class="row">
          <input type="checkbox" [checked]="item.done" (change)="toggleItem(item.id)">
          @if ( currentlyEditedId == item.id ) {
            <input type="text" #editItem [value]="item.description" (keyup.enter)="saveItem(item.id, editItem.value)"/>
            <button (click)="saveItem(item.id, editItem.value)">Save</button>
          }
          @else {
            <div class="task-text" (click)="toggleEditable(item.id)">{{ item.description }}</div>
            <button (click)="removeItem(item.id)">Delete</button>
          }
        </div>
      </li>
      }
    </ul>
  </div>
</div>
`

const Styles = `
.container {
  min-height: 100vh;
  background-color: #f5f5f5;
  padding: 1rem;
  display:flex;
  justify-content: center;
}
.to-do-wrapper {
  width: 500px;
  display: flex;
  flex-direction: column;
  gap: 15px;
}
.title {
  text-align: center;
}
.row {
  display: flex;
  gap:10px;
  margin: 1rem 0;
  justify-content: space-between;
  align-items: center;
}
ul{
  padding: 0;
}
li{
  width: 100%;
  height: 2rem;
  list-style: none;
}
input[type="checkbox"] {
  width: 1.2rem;
  height: 1.2rem;
}
.task-text,
input[type="text"] {
  width: 100%;
  min-width:300px;
  padding: 0.5rem;
}
button {
  width: 5rem;
  padding: 0.5rem;
}
`

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, RouterOutlet],
  template: Template,
  styles: Styles
})
export class AppComponent {
  title = 'Todo';
  filter: "all" | "active" | "done" = "all";
  currentlyEditedId: number | null = null;

  allItems = [
    { id: 1, description: 'eat', done: true },
    { id: 2, description: 'sleep', done: false },
    { id: 3, description: 'play', done: false },
    { id: 4, description: 'laugh', done: false },
  ]

  get items() {
    return this.allItems
      .filter(
        this.filter === "all" ? item => item :
          this.filter === "active" ? item => !item.done :
            item => item.done,
      )
      .sort(
        this.sortById
      );
  }

  sortById(a: { id: number }, b: { id: number }) {
    return b.id - a.id;
  }

  addItem(description: string) {
    if(description.length > 0) {
      this.allItems.unshift({
        id: this.allItems.length +1,
        description,
        done: false,
      })
    }
  }

  saveItem(id:number, description: string) {
    const item = this.allItems.find((item) => item.id === id);
    if (item) {
      item.description = description;
    }
    this.currentlyEditedId = null; 
  }

  removeItem(id: number) {
    this.allItems = this.allItems.filter((item) => item.id !== id);
  }

  toggleItem(id: number) {
    this.allItems.filter((item) => {
      item.id === id ? item.done = !item.done : item.done;
    })
  }

  toggleEditable (id: number) {
    this.currentlyEditedId = this.currentlyEditedId === id ? null : id;
  }

}

What’s Next?

  • Styling: Adding CSS to make the to-do list visually appealing.
  • Organizing: Moving the template (Template) into the app.component.html file and styles (Styles) into the app.component.css file. This helps keep the code cleaner.