In this blog post, we’ll explore how to create a CRUD (Create, Read, Update, Delete) application in Angular 17. We’ll use standalone components, the latest Angular syntax (@if, @for, @inject), Tailwind CSS for styling, and json-server for our backend. Our application will manage a list of employees with routing to separate pages for listing, creating, and updating employees.

What is a Standalone Component?

A standalone component in Angular is a self-contained unit that doesn’t need a parent module to function. It can declare its own dependencies and be used independently. This makes it easier to manage and reuse components across an application.

Prerequisites

  • Node.js and npm installed
  • Angular version 17 or above (commad: ng version)
  • Angular CLI installed (commad: npm install -g @angular/cli)
  • Tailwind CSS installed (commad: npm install tailwindcss)
  • JSON-Server installed (command: npm install -g json-server)

Step 1: Setting Up the Project

Create a new Angular project and navigate into the project directory:

ng new crud-app
cd crud-app

Open the project in VS Code or any code editor of your choice.

Step 2: Install and Configure Tailwind CSS

Open the terminal in VS Code (Ctrl + Backtick for Windows and Cmd + Backtick for Mac). From now on, all commands will be executed in the VS Code terminal.

Install Tailwind CSS and initialize it:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init

A tailwind.config.js file will be generated in the root directory of the project.

Paste the below code in tailwind.config.js

// tailwind.config.js
module.exports = {
  content: [
    "./src/**/*.{html,ts}", // This tells Tailwind to look at all your HTML and TypeScript files for styles you're using
  ],
  theme: {
    extend: {}, // This is where you can add your custom styles if you want
  },
  plugins: [], // This is for any extra tools or plugins you might want to use with Tailwind
};

Include Tailwind in your styles:

Paste the below code in src/styles.css file.

/* You can add global styles to this file, and also import other style files */

/* src/styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Step 3: Set Up JSON Server

Create a db.json file in the root of your project with some initial data:

{
  "employees": [
    { "id": 1, "name": "John Doe", "position": "Developer" },
    { "id": 2, "name": "Jane Smith", "position": "Designer" }
  ]
}

Start the JSON server:

json-server --watch db.json

By default, this runs on http://localhost:3000.

To run the JSON server on a different port, execute:

json-server --watch db.json --port 8000

Now, our JSON server is up and running on port 8000. This is the base endpoint for my JSON server: http://localhost:8000/employees. For now, we can stop the JSON server and continue with our development. To stop the JSON server, press Ctrl + C in the terminal.

Step 4: Create an Employee Service

Before generating the service let us first create an interface, to which the data will be mapped. This will give us type safety.

Create new folder inside app folder by name “Models”. Inside Models add new file employee.interface.ts.

Update the employee.interface.ts file as below:

export interface employee {
  id: string;
  name: string;
  position: string;
}

Generate a service for handling API calls:

ng generate service services/employee --skip-tests

This will create employee.service.ts file in services folder, and –skip-tests flag will not generate testing files, as testing is out of scope for this tutorial.

Edit the employee.service.ts:

import { inject, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map, Observable, switchMap } from 'rxjs';
import { employee } from '../models/employee.interface';

@Injectable({
  providedIn: 'root',
})
export class EmployeeService {
  private apiUrl = 'http://localhost:8000/employees'; // this endpoint was generated in Step 3
  http = inject(HttpClient);

  getEmployees(): Observable<employee[]> {
    return this.http.get<any[]>(this.apiUrl);
  }

  getEmployee(id: string): Observable<employee> {
    return this.http.get<employee>(`${this.apiUrl}/${id}`);
  }

  createEmployee(employee: employee): Observable<employee> {
    return this.getEmployees().pipe(
      map((employees) => {
        const maxId = employees.reduce((max, emp) => Math.max(max, +emp.id), 0);
        employee.id = (maxId + 1).toString();
        return employee;
      }),
      switchMap((newEmployee) =>
        this.http.post<employee>(this.apiUrl, newEmployee)
      )
    );
  }

  updateEmployee(id: string, employee: employee): Observable<employee> {
    return this.http.put<employee>(`${this.apiUrl}/${id}`, employee);
  }

  deleteEmployee(id: string): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

In this example, HttpClient is injected using the inject function directly inside the class, avoiding the need for constructor parameters.

Simple Breakdown of the Code for createEmployee function:

  • .pipe(): Chains operations on the observable.
  • .map(): Transforms the list of employees to find the highest ID and create a new employee object with a new ID.
  • switchMap(): Takes this new employee object and makes an HTTP POST request to save it.

Note: The RxJS .map() is used to change or transform the data coming from an observable, whereas React’s .map() Loops through arrays to create JSX elements.

Step 5: Create Components

We’ll create two components, one for listing employees and the other for creating or updating an employee.

Employee List Component

Generate a component for listing employees:

ng generate component employee-list --skip-tests

Edit the employee-list.component.ts:

import { Component, OnInit, inject } from '@angular/core';
import { employee } from '../models/employee.interface';
import { EmployeeService } from '../services/employee.service';
import { Router } from '@angular/router';

@Component({
  selector: 'app-employee-list',
  standalone: true,
  imports: [],
  templateUrl: './employee-list.component.html',
  styleUrl: './employee-list.component.css',
})
export class EmployeeListComponent {
  employeeService = inject(EmployeeService);
  router = inject(Router);
  employees: employee[] = [];

  ngOnInit() {
    this.employeeService
      .getEmployees()
      .subscribe((data) => (this.employees = data));
  }

  goToCreate() {
    this.router.navigate(['/create']);
  }

  editEmployee(id: string) {
    this.router.navigate(['/edit', id]);
  }

  deleteEmployee(id: string) {
    if (confirm('Are you sure you want to delete this employee?')) {
      this.employeeService.deleteEmployee(id).subscribe(() => {
        this.employees = this.employees.filter((emp) => emp.id !== id);
      });
    }
  }
}

Services are injected using the “inject” function to access employee data and handle navigation.
ngOnInit Method Fetches the employee data when the component initializes.
goToCreate Method Navigates to the employee creation page.
editEmployee Method Navigates to the employee edit page with the given employee ID.
deleteEmployee Method Deletes an employee and updates the local employee list.

Edit the employee-list.component.html:

<div class="container mx-auto">
  <h1 class="text-2xl font-bold mb-4">Employee List</h1>
  <button
    (click)="goToCreate()"
    class="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-700 mb-4"
  >
    Add Employee
  </button>
  @if (employees.length > 0) {
  <table class="table-auto w-full">
    <thead>
      <tr>
        <th class="px-4 py-2">Name</th>
        <th class="px-4 py-2">Position</th>
        <th class="px-4 py-2">Actions</th>
      </tr>
    </thead>
    <tbody>
      @for (employee of employees; track employees) {
      <tr>
        <td class="border px-4 py-2">{{ employee.name }}</td>
        <td class="border px-4 py-2">{{ employee.position }}</td>
        <td class="border px-4 py-2">
          <button
            (click)="editEmployee(employee.id)"
            class="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-700 mr-2"
          >
            Edit
          </button>
          <button
            (click)="deleteEmployee(employee.id)"
            class="bg-red-500 text-white p-2 rounded-md hover:bg-red-700"
          >
            Delete
          </button>
        </td>
      </tr>
      }
    </tbody>
  </table>
  } @else {
  <p>No employees found.</p>
  }
</div>

This HTML code leverages the new Angular 17 syntax for conditional rendering and iteration. Using @if for conditionals and @for for looping, it displays a table of employees or a message indicating no employees are found. This new syntax replaces the older ngIf and ngFor directives, offering a more modern and concise way to handle these scenarios in Angular templates.

Why Use track?

The track keyword is used to improve performance when rendering lists. It helps Angular keep track of the items in the list, so it can efficiently update the DOM when the list changes. Without tracking, Angular might re-render the entire list whenever there’s a change, which can be inefficient. By using track employees, Angular can identify which items have changed and only update those specific items, making the rendering process faster and more efficient.

Employee Form Component

Generate component for the employee form:

ng generate component employee-form --skip-tests

Edit the employee-form.component.ts:

import { Component, OnInit, inject } from '@angular/core';
import {
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} from '@angular/forms';
import { EmployeeService } from '../services/employee.service';
import { ActivatedRoute, Router } from '@angular/router';

@Component({
  selector: 'app-employee-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './employee-form.component.html',
  styleUrl: './employee-form.component.css',
})
export class EmployeeFormComponent {
  employeeForm: FormGroup;
  isEdit = false;
  employeeId: string | null = null;

  // Dependency Injection
  fb = inject(FormBuilder);
  employeeService = inject(EmployeeService);
  router = inject(Router);
  route = inject(ActivatedRoute);

  constructor() {
    this.employeeForm = this.fb.group({
      name: ['', Validators.required],
      position: ['', Validators.required],
    });
  }

  ngOnInit() {
    this.route.paramMap.subscribe((params) => {
      const id = params.get('id');
      if (id) {
        this.isEdit = true;
        this.employeeId = id;
        this.employeeService.getEmployee(this.employeeId).subscribe((data) => {
          this.employeeForm.patchValue(data);
        });
      }
    });
  }

  onSubmit() {
    if (this.employeeForm.invalid) return;

    if (this.isEdit && this.employeeId) {
      // Edit Employee
      this.employeeService
        .updateEmployee(this.employeeId, this.employeeForm.value)
        .subscribe(() => {
          this.router.navigate(['/']);
        });
    } else {
      // Create Employee
      this.employeeService
        .createEmployee(this.employeeForm.value)
        .subscribe(() => {
          this.router.navigate(['/']);
        });
    }
  }
}

In the EmployeeFormComponent, the imports array is used to include necessary Angular modules that the component depends on. Here, we include ReactiveFormsModule so we can use reactive forms in our component. Reactive forms let us build and manage forms more effectively, with features like form validation and control over form inputs. This setup makes sure that our form functionality works properly within the component.

Edit the employee-form.component.html:

<div class="container mx-auto">
  <h1 class="text-2xl font-bold mb-3 underline">
    {{ isEdit ? "Edit" : "Create" }} Employee
  </h1>
  <form [formGroup]="employeeForm" (ngSubmit)="onSubmit()">
    <div>
      <label>Name</label>
      <input
        formControlName="name"
        type="text"
        class="border border-gray-300 p-2 rounded-md mb-3"
      />
    </div>
    <div>
      <label>Position</label>
      <input
        formControlName="position"
        type="text"
        class="border border-gray-300 p-2 rounded-md mb-4"
      />
    </div>
    <button
      type="submit"
      class="bg-blue-500 text-white p-2 rounded-md hover:bg-blue-700"
    >
      {{ isEdit ? "Update" : "Create" }}
    </button>
  </form>
</div>

This Angular 17 standalone component handles creating and editing employee records using reactive forms. The component initializes a form group with validation, checks if it’s in edit mode by reading route parameters, and fetches the employee data if needed. Upon form submission, it either updates an existing employee or creates a new one and then navigates back to the employee list.

What is formControlName?

formControlName is a directive in Angular used within reactive forms. It binds a form control in the component’s form model to a specific input element in the template. It helps manage the form’s state and validation.

Types of Forms in Angular

Angular provides two types of forms:

  • Template-driven Forms: Simpler, defined in the template, suitable for basic forms.
  • Reactive Forms: More control and scalability, defined in the component, suitable for complex forms.

Step 6: Configure Routing

Update app.routes.ts under the app folder. Navigate to the app.routes.ts file inside the src/app folder, and add the following routes:

import { Routes } from '@angular/router';
import { EmployeeListComponent } from './employee-list/employee-list.component';
import { EmployeeFormComponent } from './employee-form/employee-form.component';

export const routes: Routes = [
  { path: '', component: EmployeeListComponent },
  { path: 'create', component: EmployeeFormComponent },
  { path: 'edit/:id', component: EmployeeFormComponent },
];

Step 7: Update the App Component

Update the app.component.ts and app.component.html to include the router outlet in the imports array.

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

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

In the app.component.html, remove everything and add the below line

<router-outlet></router-outlet>

Step 8: Update app.config.ts

To ensure that the HttpClient service is available throughout your application, add provideHttpClient() to the global providers in your app.config.ts.

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()],
};

Step 9: Run the application

First, start the JSON server by executing the following command in the terminal:

json-server --watch db.json --port 8000

Open another terminal in your VS Code and start the Angular app by executing the following command:

ng serve -o

Conclusion

In this blog post, we explored how to create a CRUD application using Angular 17 with standalone components. We demonstrated setting up a JSON server for backend data, implementing reactive forms for managing employee data, and ensuring efficient ID handling for new records. Hope this tutorial will provide you a solid foundation for building sophisticated applications in Angular.

Don’t forget to checkout: Understand BehaviorSubject in Angular with Real Time Example

Categorized in:

Angular,

Last Update: June 20, 2024

Tagged in: