top of page
  • Writer's pictureSolve It

Products listing and Create Modal using Tailwind CSS with Angular

Updated: Mar 4


Dashboard screenshot
Dashboard screenshot

Tailwind CSS with Angular


In this post, we'll look at products listing page and how to create a new product using a modal.



Create product folder

Create a new folder product in the src/app folder

Create product components

  cd src/app
  ng generate component product/product-list
  ng generate component product/product-details

Create routes for auth module

Create a new file routes.ts in the src/app/product folder

import { Route } from '@angular/router';
import { ProductListComponent } from './product-list/product-list.component';
import { ProductDetailsComponent } from './product-details/product-details.component';

export default [
  { path: '', component: ProductListComponent },
  { path: ':id', component: ProductDetailsComponent },
] satisfies Route[];

Update app.routes.ts file

import { Routes } from '@angular/router';
export const routes: Routes = [
  { path: 'auth', providers: [], loadChildren: () => import('./auth/routes') },
  { path: 'products', providers: [], loadChildren: () => import('./product/routes') },
];

Create Product class

  cd src/app
  ng generate class shared/classes/product

product.ts

export class Product {
  constructor(
    public id: number,
    public name: string,
    public description: string,
    public price: number,
  ) { }
}

Create Product service

  cd src/app
  ng generate service shared/services/product

product.service.ts

import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
import { Observable } from 'rxjs';
import { HttpParams } from '@angular/common/http';
import { Product } from '../classes/product';

@Injectable({
  providedIn: 'root',
})
export class ProductService {
  constructor(private apiService: ApiService) {}

  getProducts(): Observable<Product[]> {
    return this.apiService.get(`products`);
  }
  getProductDetails(id: string): Observable<Product> {
    return this.apiService.get(`products/${id}`);
  }
  createProduct(product: Product): Observable<Product> {
    return this.apiService.post(`products`, product);
  }
  updateProduct(id: string, product: Product): Observable<Product> {
    return this.apiService.put(`products/${id}`, product);
  }
  deleteProduct(id: string): Observable<any> {
    return this.apiService.delete(`products/${id}`);
  }
}

Create an interceptor that will send the token with every request

  cd src/app
  ng generate service shared/interceptors/http-token

http-token.service.ts

import {
  HttpEvent,
  HttpHandler,
  HttpInterceptor,
  HttpRequest,
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, catchError } from 'rxjs';
import { JsonWebToken } from '../utils/json-web-token';

@Injectable({
  providedIn: 'root',
})
export class HttpTokenService implements HttpInterceptor {
  constructor() {}

  intercept(
    request: HttpRequest<unknown>,
    next: HttpHandler
  ): Observable<HttpEvent<unknown>> {
    const headersConfig: any = { 'Access-Control-Allow-Origin': '*' };
    const token = JsonWebToken.getToken();
    if (token) {
      headersConfig.Authorization = `Bearer ${token}`;
    }
    request = request.clone({ setHeaders: headersConfig });

    return next.handle(request).pipe(
      catchError((error) => {
        switch (error.status) {
          case 401:
            //todo if ! auth module
            console.log('todo : refresh token');
            break;
        }
        throw error;
      })
    );
  }
}

Update app.config.ts

Add the interceptor to the providers array

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

import { routes } from './app.routes';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { HttpTokenService } from './shared/interceptors/http-token.service';

export const appConfig: ApplicationConfig = {
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: HttpTokenService, multi: true },
    importProvidersFrom(HttpClientModule),
    provideRouter(routes),
  ],
};

Update product-list.component.ts

import { Component, OnInit } from '@angular/core';
import { Product } from '../../shared/classes/product';
import { ProductService } from '../../shared/services/product.service';
import {
  FormGroup,
  FormControl,
  Validators,
  ReactiveFormsModule,
} from '@angular/forms';

@Component({
  selector: 'app-product-list',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './product-list.component.html',
  styleUrl: './product-list.component.scss',
})
export class ProductListComponent implements OnInit {
  products: Product[] = [];
  isProductModalOpen = false;
  productForm = new FormGroup({
    name: new FormControl('', [Validators.required]),
    description: new FormControl('', Validators.required),
    price: new FormControl(null, Validators.required),
  });

  constructor(private productService: ProductService) {}
  ngOnInit() {
    this.getProducts();
  }
  getProducts() {
    this.productService.getProducts().subscribe((products) => {
      this.products = products;
      console.log(this.products);
    });
  }
  onSubmit() {
    if (this.productForm.valid) {
      this.productService
        .createProduct(this.productForm.value as Partial<Product>)
        .subscribe((response) => {
          this.closeModal();
          this.getProducts();
        });
    }
  }
  closeModal() {
    this.isProductModalOpen = false;
    this.productForm.reset();
  }
}


Update product-list.component.html

<div class="flex justify-between items-center">
  <h1 class="text-2xl font-bold">Products</h1>
  <button
    (click)="isProductModalOpen = true"
    class="flex items-center bg-blue-500 text-white px-4 py-2 rounded"
  >
    Create
  </button>
</div>

<div class="relative overflow-x-auto py-20">
  <table
    class="w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400"
  >
    <thead
      class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400"
    >
      <tr>
        <th scope="col" class="px-6 py-3">Product name</th>
        <th scope="col" class="px-6 py-3">Description</th>

        <th scope="col" class="px-6 py-3">Price</th>
        <th scope="col" class="px-6 py-3">Actions</th>
      </tr>
    </thead>
    <tbody>
      @for (product of products; track $index) {

      <tr class="bg-white border-b dark:bg-gray-800 dark:border-gray-700">
        <th
          scope="row"
          class="px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white"
        >
          {{ product.name }}
        </th>
        <td class="px-6 py-4">{{ product.description }}</td>
        <td class="px-6 py-4">{{ product.price }}</td>
        <td class="px-6 py-4">
          <div class="sm:flex gap-1">
            <button
              class="flex items-center bg-red-500 text-white px-4 py-2 rounded"
            >
              Delete
            </button>
            <button
              class="flex items-center bg-green-500 text-white px-4 py-2 rounded"
            >
              Edit
            </button>
          </div>
        </td>
      </tr>
      }
    </tbody>
  </table>
</div>

@if (isProductModalOpen) {

<div
  class="relative z-50"
  aria-labelledby="modal-title"
  role="dialog"
  aria-modal="true"
>
  <!--
      Background backdrop, show/hide based on modal state.
  
      Entering: "ease-out duration-300"
        From: "opacity-0"
        To: "opacity-100"
      Leaving: "ease-in duration-200"
        From: "opacity-100"
        To: "opacity-0"
    -->
  <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>

  <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
    <div
      class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
    >
      <!--
          Modal panel, show/hide based on modal state.
  
          Entering: "ease-out duration-300"
            From: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
            To: "opacity-100 translate-y-0 sm:scale-100"
          Leaving: "ease-in duration-200"
            From: "opacity-100 translate-y-0 sm:scale-100"
            To: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
        -->
      <div
        class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg"
      >
        <div class="bg-white px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
          <div class="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
            <form
              class="space-y-6"
              [formGroup]="productForm"
              (ngSubmit)="onSubmit()"
            >
              <div>
                <label
                  for="name"
                  class="block text-sm font-medium leading-6 text-gray-900"
                  >Name</label
                >
                <div class="mt-2">
                  <input
                    id="name"
                    name="name"
                    type="text"
                    formControlName="name"
                    class="block w-full rounded-md border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                  />
                </div>
              </div>

              <div>
                <div class="flex items-center justify-between">
                  <label
                    for="description"
                    class="block text-sm font-medium leading-6 text-gray-900"
                    >Description</label
                  >
                </div>
                <div class="mt-2">
                  <input
                    id="description"
                    name="description"
                    type="text"
                    formControlName="description"
                    class="block w-full rounded-md border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                  />
                </div>
              </div>
              <div>
                <div class="flex items-center justify-between">
                  <label
                    for="price"
                    class="block text-sm font-medium leading-6 text-gray-900"
                    >Price</label
                  >
                </div>
                <div class="mt-2">
                  <input
                    id="price"
                    name="price"
                    type="number"
                    formControlName="price"
                    class="block w-full rounded-md border-0 p-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
                  />
                </div>
              </div>
              <div class="sm:flex sm:flex-row-reverse">
                <button
                  [disabled]="!productForm.valid"
                  type="submit"
                  class="inline-flex w-full justify-center rounded-md bg-green-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-green-500 sm:ml-3 sm:w-auto disabled:opacity-50"
                >
                  Save
                </button>
                <button
                  (click)="closeModal()"
                  type="button"
                  class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
                >
                  Cancel
                </button>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
}

In the next blog post, we'll look at how to create a product details page and how to update and delete a product.


Stay tuned!

47 views0 comments

Recent Posts

See All

Comments


bottom of page