Angular: Building a To-Do Project with Essential Basics

Angular: Building a To-Do Project with Essential Basics

Crafting a Robust To-Do Application using Tailwind CSS and Fundamental Angular 2 Concepts

Initializing and Integrating Tailwind CSS with Your Angular Application

Introduction

Tailwind CSS, known for its utility-first approach, offers a rapid and efficient way to style your applications. In this guide, we'll walk you through the process of seamlessly integrating Tailwind CSS into your Angular project.

Step 1: Set Up Your Project To get started, ensure you have Angular CLI installed. If not, you can install it with the following commands in your terminal:

npm install -g @angular/cli

Now, let's create your Angular project and navigate into its directory:

ng new my-project
cd my-project

Step 2: Install and Configure Tailwind CSS, I find Tailwind CSS to be a game-changer. It boosts your development speed by leaps and bounds compared to traditional CSS 10x.

Start by installing Tailwind CSS and its dependencies via npm:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

This initializes a tailwind.config.js file where you can customize the configuration according to your project's needs. To ensure Tailwind CSS works seamlessly with your Angular templates, update the tailwind.config.js file as follows:

// tailwind.config.js

module.exports = {
  content: [
    "./src/**/*.{html,ts}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

Step 3: Integrate Tailwind Directives Now, let's integrate Tailwind CSS directives into your project's styles. Open the styles.css file located in the src folder and add the following directives:

/* styles.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

Step 4: Build and Preview Your Project With everything in place, you're almost ready to see Tailwind CSS in action. Start your development server using the following command:

ng serve

During this process, it's possible to encounter an error stating, "Error: This command is not available when running the Angular CLI outside a workspace." If you encounter this, refer to our other blog post for a solution.

This will compile your Angular project and make it available for preview. Now, as you navigate to your browser and visit http://localhost:4200, you'll witness the magic of Tailwind CSS enhancing your application's styling.

Step 5: Utilize Tailwind CSS Utility Classes Tailwind CSS's real power lies in its utility classes. These classes enable you to rapidly style your content. As an example, consider the following code snippet in your app.component.html file:

<!-- app.component.html -->

<h1 class="text-7xl font-semibold">
  Hello world!
</h1>

With this, you've successfully integrated Tailwind CSS into your Angular project, unlocking its potential to expedite your styling process.

For a more comprehensive understanding, you can refer to the official Tailwind CSS documentation for Angular: Tailwind CSS Documentation for Angular

Explanation of the app.component.html

// app.component.html
<div class="max-w-[1140px] m-auto text-center mt-10 p-4">
  <h3 class="text-7xl mb-10 text-center">{{ title }}</h3>
  <div class="flex flex-col gap-4">
    <input
      type="text"
      class="max-w-[500px] p-4 border"
      placeholder="Todo item..."
      [(ngModel)]="todoItemName.name"
    />
    <button
      class="bg-black text-white py-5 px-10 rounded-md"
      (click)="onClick(index)"
    >
      Add item to the todo list
    </button>
    <button
      class="bg-black text-white py-5 px-10 rounded-md"
      (click)="clearAll()"
    >
      Clear all items
    </button>
  </div>
  <div class="text-left mt-10 flex flex-col gap-4">
    <div *ngFor="let item of items">
      <div
        class="p-4 flex justify-between rounded-md"
        [ngClass]="{
          not_done: item.status === 'Not done',
          done: item.status === 'Done',
          do_it_later: item.status === 'Do it later',
          maybe_never: item.status === 'Maybe never'
        }"
      >
        {{ item.name }}
        <mat-icon (click)="deleteTodo(item.index)" class="cursor-pointer"
          >close</mat-icon
        >
      </div>
      <div class="flex gap-4 items-center mt-2">
        <div
          (click)="changeColor('green', item.index)"
          class="bg-green-500 py-2 px-6 cursor-pointer rounded-full"
        >
          Done
        </div>
        <div
          (click)="changeColor('yellow', item.index)"
          class="bg-yellow-500 py-2 px-6 cursor-pointer rounded-full"
        >
          Do it later
        </div>
        <div
          (click)="changeColor('red', item.index)"
          class="bg-red-500 py-2 px-6 cursor-pointer text-white rounded-full"
        >
          Not urgent
        </div>
      </div>
    </div>
  </div>
</div>

The provided code snippet begins by structuring the HTML layout for a todo list application. Within a container element, a series of UI elements are organized to facilitate the addition, management, and visualization of tasks. The code employs Tailwind CSS utility classes to achieve a visually appealing and responsive design.

Starting with a container having a maximum width of 1140 pixels and centered alignment, the code establishes the core layout for the todo list app. A large heading (formatted with text size and margin) displays the title, which is dynamically set using Angular's data binding ({{ title }}).

The subsequent part of the code focuses on user interaction elements. An input field is provided to enter todo items, and the entered value is two-way bound to the Angular variable todoItemName.name using [(ngModel)]. This allows real-time synchronization between the input field and the data variable.

Two buttons follow the input field: one for adding an item to the todo list and another for clearing all items.

The todo list items are displayed in a vertical column using the flex and gap utility classes to manage spacing between items. Each item is presented within a styled container that changes its appearance based on the status of the task. The ngClass directive dynamically applies different classes (not_done, done, do_it_later, maybe_never) to represent different statuses.

Within each task container, the task name is displayed, and a delete icon (represented by mat-icon) is included for removing the task. The icon's click event is bound to the deleteTodo function.

Following the task name and delete icon, a row of buttons appears. These buttons allow users to change the status of the task, with each button representing a different status. Clicking a button triggers the corresponding changeColor function to update the status and visual appearance of the task container. The buttons are color-coded using Tailwind CSS classes (bg-green-500, bg-yellow-500, bg-red-500), and their rounded design and cursor pointer enhance interactivity.

// app.component.ts
// Import necessary components from Angular core
import { Component } from '@angular/core';

// Declare the component's metadata
@Component({
  selector: 'app-root', // The component will be selected using this tag
  templateUrl: './app.component.html', // HTML template file for the component
  styles: [
    // Inline styles for the component (defining classes for different status styles)
    `
      .not_done {
        background-color: Gainsboro;
      }
      .done {
        background-color: Green;
      }
      .do_it_later {
        background-color: GoldenRod;
      }
      .maybe_never {
        background-color: Red;
      }
    `,
  ],
})
export class AppComponent {
  // Component class definition

  // Title displayed in the HTML template
  title = 'The todo list project!';

  // Index to keep track of the todo item index
  index = 0;

  // Template for the initial todo item
  generatedTodoItem = {
    name: 'First Todo',
    index: 10000, // This is for learning purposes only
    status: 'Not done',
  };

  // Array to store todo items
  items = [this.generatedTodoItem];

  // Variable to store the name of the new todo item
  todoItemName = '';

  // Function to handle the click event when adding a todo item
  onClick(i: number) {
    if (this.todoItemName.trim() !== '') {
      // Create a new todo item and add it to the items array
      this.items.push({
        name: this.todoItemName,
        index: i,
        status: 'Not done',
      });

      // Reset the input field and increment the index
      this.todoItemName = '';
      this.index++;
    }
  }

  // Function to delete a todo item
  deleteTodo(ind: number) {
    // Filter out the item with the specified index
    this.items = this.items.filter((item) => item.index !== ind);
  }

  // Function to clear all todo items
  clearAll() {
    this.items = [];
  }

  // Function to change the color (and status) of a todo item
  changeColor(color: string, index: number) {
    // Find the item in the items array based on index
    const item = this.items.find((item) => item.index === index);

    // If the item doesn't exist, return
    if (!item) {
      return;
    }

    // Update the status based on the selected color
    if (color === 'green') {
      item.status = 'Done';
    } else if (color === 'yellow') {
      item.status = 'Do it later';
    } else {
      item.status = 'Maybe never';
    }
  }
}

This TypeScript code defines the behavior of the Angular component responsible for managing the todo list application. It includes functions for adding, deleting, and updating todo items, as well as maintaining their statuses. Additionally, the code provides inline styles for different todo item statuses, defining how they appear visually in the HTML template.

Splitting the App into Editing and Display Components

As our application grows, it's essential to maintain a clean and organized codebase. Leveraging Angular's component-based architecture, we can enhance the modularity of our todo list app by breaking it down into smaller, focused components. In this section, we'll introduce two new components: "Editing" and "Display", each serving distinct purposes.

Make new components using Angular CLI

n g display
n g editing

The Editing Component

The Editing component takes care of adding and managing new todo items. By encapsulating this functionality, we achieve a clearer separation of concerns and make our codebase more maintainable. Let's take a closer look at the key aspects of this component:

// editing.component.ts
// Import necessary components from Angular core
import { Component, Input, Output, EventEmitter } from '@angular/core';

// Declare the component metadata
@Component({
  selector: 'app-editing', // The component selector used in templates
  templateUrl: './editing.component.html', // HTML template file for the component
  styleUrls: ['./editing.component.css'], // External stylesheet for the component
})
export class EditingComponent {
  // Declare the component class

  // Output decorator to emit events when a new todo item is created or all items are cleared
  @Output() createdNewTodoItem = new EventEmitter<string>();
  @Output() clearAllItems = new EventEmitter<void>();

  // Variable to store the name of a new todo item
  newTodoItemName = '';

  // Function to handle clearing all todo items
  clearAll() {
    // Emit an event to signal clearing all items to the parent component
    this.clearAllItems.emit();
  }

  // Function to handle adding a new todo item
  onAddItem() {
    // Emit the name of the new todo item to the parent component
    this.createdNewTodoItem.emit(this.newTodoItemName);

    // Reset the variable for the next todo item
    this.newTodoItemName = '';
  }
}
<!-- editing.component.html -->
<div class="flex flex-col gap-4">
    <input
      type="text"
      class="max-w-[500px] p-4 border"
      placeholder="Todo item..."
      [(ngModel)]="newTodoItemName"
    />
    <button
      class="bg-emerald-500 text-white py-5 px-10 rounded-md"
      (click)="onAddItem()"
    >
      Add item to the todo list
    </button>
    <button
      class="bg-black text-white py-5 px-10 rounded-md"
      (click)="clearAll()"
    >
      Clear all items
    </button>
  </div>

The Display Component

The Display component is responsible for rendering and visualizing the list of todo items, along with their associated functionalities. This separation ensures that the UI rendering is distinct from the logic governing the editing and status changes. Here's a glimpse of how this component is structured:

<!-- display.component.html -->
<div class="text-left mt-10 flex flex-col gap-4">
  <div *ngFor="let item of items">
    <div
      class="p-4 flex justify-between rounded-md"
      [ngClass]="{
        not_done: item.status === 'Not done',
        done: item.status === 'Done',
        do_it_later: item.status === 'Do it later',
        maybe_never: item.status === 'Maybe never'
      }"
    >
      {{ item.name }}
      <mat-icon (click)="deleteTodo(item.index)" class="cursor-pointer"
        >close</mat-icon
      >
    </div>
    <div class="flex gap-4 items-center mt-2">
      <div
        (click)="changeColor('green', item.index)"
        class="bg-green-500 py-2 px-6 cursor-pointer rounded-full"
      >
        Done
      </div>
      <div
        (click)="changeColor('yellow', item.index)"
        class="bg-yellow-500 py-2 px-6 cursor-pointer rounded-full"
      >
        Do it later
      </div>
      <div
        (click)="changeColor('red', item.index)"
        class="bg-red-500 py-2 px-6 cursor-pointer text-white rounded-full"
      >
        Not urgent
      </div>
    </div>
  </div>
</div>
// display.component.ts
// Import necessary components from Angular core
import { Component, Input, Output, EventEmitter } from '@angular/core';

// Declare the component metadata
@Component({
  selector: 'app-display', // The component selector used in templates
  templateUrl: './display.component.html', // HTML template file for the component
  styles: [
    // Inline styles for the component (defining classes for different status styles)
    `
      .not_done {
        background-color: Gainsboro;
      }
      .done {
        background-color: Green;
      }
      .do_it_later {
        background-color: GoldenRod;
      }
      .maybe_never {
        background-color: Red;
      }
    `,
  ],
})
export class DisplayComponent {
  // Declare the component class

  // Input decorator to receive the array of todo items from the parent component
  @Input() items: string[];

  // Output decorator to emit events when the todo item is deleted or its color is changed
  @Output() onDeleteTodo = new EventEmitter<number>();
  @Output() onChangeColor = new EventEmitter<{ color: string; index: number }>();

  // Function to handle the deletion of a todo item
  deleteTodo(index: number) {
    // Emit the index of the item to be deleted to the parent component
    this.onDeleteTodo.emit(index);
  }

  // Function to handle the color change of a todo item
  changeColor(color: string, index: number) {
    // Log the index to the console (for demonstration purposes)
    console.log(index);

    // Emit an object containing the color and index of the item to the parent component
    this.onChangeColor.emit({ color: color, index: index });
  }
}

By dividing our application into these two components, we achieve greater clarity and maintainability. The "Editing" component handles user input and interactions related to adding and clearing items, while the "Display" component showcases the todo list and its associated actions.

Of course, after the changes, you should also update the app.component, try it yourself now, and at the end of the blog I will leave the final app.component

Header Component

As we embark on our journey towards more advanced concepts like routing in future blogs, it's essential to consolidate our understanding of Angular's fundamental features. In the context of this phase, we'll begin by creating a Header component.

Crafting the Header Component

Let's begin by generating a dedicated Header component using the Angular CLI:

ng generate component header

Once you've generated the Header component, you can copy-paste the following code into the relevant files:

header.component.html:

<div class="px-4 py-2 mb-14 w-full border-b z-10 bg-white shadow-md">
  <div class="flex justify-between items-center max-w-[1140px] m-auto">
    <div class="w-[60px]">
      <img
        (click)="showHomePage()"
        src="https://img.freepik.com/premium-vector/green-check-mark-icon-symbol-logo-circle-tick-symbol-green-color-vector-illustration_685751-503.jpg?w=2000"
        alt="Todo Logo"
        class="cursor-pointer"
      />
    </div>
    <div class="flex gap-5">
      <div (click)="showHomePage()" class="font-semibold cursor-pointer">
        Home
      </div>
      <div (click)="showNewPage()" class="font-semibold cursor-pointer">
        New Page
      </div>
    </div>
  </div>
</div>

header.component.ts:

// header.component.ts
import { Component, EventEmitter, Output } from '@angular/core';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {
  @Output() newShowNewPage = new EventEmitter <{showHomeBool:boolean, showNewPageBool:boolean}>();
  @Output() newShowHomePage = new EventEmitter<{showHomeBool:boolean, showNewPageBool:boolean}>();

  showNewPage(){
    this.newShowNewPage.emit();
  }
  showHomePage(){
    this.newShowHomePage.emit();
  }
}

Navigational Improvisation with *ngIf

By employing the *ngIf directive within the Header component, we've introduced a basic form of navigation. When users click on the "Home" or "New Page" options, the component emits events that trigger a change in the display. While this doesn't involve true routing, it serves us to learn the basics.

Of course, after the changes, you should also update the app.component, try it yourself now, and at the end of the blog I will leave the final app.component

Your app should look like this now:

Implementing the Dropdown Menu

To integrate the dropdown menu, we'll make use of Angular's directives and event handling. The following snippets illustrate how the dropdown menu is added and how it functions:

header.component.html:

// header.component.html     
<div (click)="showModal()" class="font-semibold cursor-pointer flex items-center">
  New Page <mat-icon>arrow_drop_down</mat-icon>
</div>
<div *ngIf="openModal" class="absolute z-20 pl-3 bg-white hover:bg-slate-200 cursor-pointer items-center ml-[-40px] mt-[130px] shadow-md flex h-[60px] w-[190px]">
  <div class="flex gap-5 items-center">
    <div class="font-semibold">Go to the new page!</div>
    <mat-icon>play_arrow</mat-icon>
  </div>
</div>

header.component.ts:

// header.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';

@Component({
  selector: 'app-header',
  templateUrl: './header.component.html',
  styleUrls: ['./header.component.css']
})
export class HeaderComponent {
  @Input() openModal: boolean; // Updated the type to boolean
  @Output() newShowModal = new EventEmitter<void>();

  showModal() {
    this.newShowModal.emit();
  }
}

app.component.html changes:

// app.component.html
  <app-header
    (newShowNewPage)="showNewPage()"
    (newShowHomePage)="showHomePage()"
    [openModal]="openModal"
    (newShowModal)="showModal()"
  ></app-header>

app.component.ts changes:

// app.component.ts 
openModal = false;

showModal() {
  this.openModal = !this.openModal;
}

Optimizing Application Logic with Services

As our application scales, keeping the logic organized and reusable becomes paramount. The app.component.ts file can quickly become unwieldy. This is where Angular's Services step in. Services are a way to extract common functionality from components, making the code more modular, maintainable, and reusable.

By leveraging Services, we can create a centralized location for business logic, data management, and communication. Let's see how we can refactor the code to employ a service:

// todo.service.ts
export class TodoService {
  index = 0;
  items = [
    {
      name: 'First Todo',
      index: 10000,
      status: 'Not done',
    },
    {
      name: 'Second Todo',
      index: 10001,
      status: 'Not done',
    },
    {
      name: 'Third Todo',
      index: 10002,
      status: 'Not done',
    },
  ];
  onCreatedNewTodoItem(todoItemName: string) {
    if (todoItemName.trim() !== '') {
      this.items.push({
        name: todoItemName,
        index: this.index,
        status: 'Not done',
      });
      todoItemName = '';
      this.index++;
    }
  }
  onClearAll() {
    this.items = [];
  }
  deleteTodo(ind: number) {
    this.items = this.items.filter((item) => item.index !== ind);
  }
  // changeColor(color: string, index: number) {
  changeColor(color: string, index: number ) {
    const item = this.items.find((item) => item.index === index);
    if (!item) {
      return; // Item not found
    }
    if (color === 'green') {
      item.status = 'Done';
    } else if (color === 'yellow') {
      item.status = 'Do it later';
    } else {
      item.status = 'Maybe never';
    }
  }

  logingFunction(message: string) {
    console.log(message);
  }
}

Benefits of Services

By moving the logic to a service, you achieve several key advantages:

  1. Modularity: The component is less cluttered, focusing on presentation rather than logic.

  2. Reusability: The same service can be used in multiple components, eliminating code duplication.

  3. Testability: Services can be tested independently, improving the overall testing process.

  4. Maintainability: Changes or updates to the logic are confined to the service, making maintenance simpler.

At the end app.component.ts and app.component.html will look like this:

// app.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styles: [],
  providers: [TodoService],
})
export class AppComponent implements OnInit {
  title = 'The todo list project!';
  index = 0;
  items = [];
  showNewPageBool = false;
  showHomeBool = true;
  openModal = false;

  constructor(private todoService: TodoService) {
   // Inform Angular that we need instance of the TodoService class
   // So we are not manualy making instance like this
  //  const todoService = new TodoService();
  }

  ngOnInit() {
    this.items = this.todoService.items;
  }
  onCreatedNewTodoItem(todoItemName: string) {
    this.todoService.onCreatedNewTodoItem(todoItemName);
    this.index = this.todoService.index;
    console.log(this.index);
  }
  onClearAll() {
    this.todoService.onClearAll();
    this.items = this.todoService.items;
  }

  deleteTodo(ind: number) {
    this.todoService.deleteTodo(ind);
    this.items = this.todoService.items;
  }
  changeColor(data: { color: string; index: number }) {
    this.todoService.changeColor(data.color, data.index);
  }
  showNewPage() {
    this.showHomeBool = false;
    this.showNewPageBool = true;
    this.openModal = false;
  }
  showHomePage() {
    this.showNewPageBool = false;
    this.showHomeBool = true;
  }
  showModal() {
    this.openModal = !this.openModal;
  }
}
<!-- app.component.ts -->
<div>
  <app-header
    (newShowNewPage)="showNewPage()"
    (newShowHomePage)="showHomePage()"
    [openModal]="openModal"
    (newShowModal)="showModal()"
  ></app-header>
  <div
    *ifNotDirective="!showHomeBool"
    class="max-w-[1140px] m-auto text-center p-4"
  >
    <h3 class="text-7xl mb-10 text-center text-emerald-700">{{ title }}</h3>
    <app-editing
      (createdNewTodoItem)="onCreatedNewTodoItem($event)"
      (clearAllItems)="onClearAll()"
    ></app-editing>
    <app-display
      [items]="items"
      (onDeleteTodo)="deleteTodo($event)"
      (onChangeColor)="changeColor($event)"
    ></app-display>
  </div>
  <div *ifNotDirective="!showNewPageBool">
    <h3 class="text-7xl mb-10 text-center text-emerald-700">New Page!</h3>
  </div>
</div>