In this blog, we will walk you through the process of creating a URL shortener application using Angular for the frontend and Tailwind CSS for styling. A URL shortener is a handy tool that converts long URLs into shorter, more manageable links. This project will help you understand how to build a functional and aesthetically pleasing web application using modern web development technologies.
To follow along with this tutorial, you should have a basic understanding of Angular and some familiarity with Tailwind CSS. Ensure you have Node.js and Angular CLI installed on your machine.
First, create a new Angular project by running the following command in your terminal:
ng new url-shortener-app cd url-shortener-app
Next, set up Tailwind CSS in your Angular project. Install Tailwind CSS and its dependencies via npm:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init
Configure Tailwind CSS by updating the tailwind.config.js file:
module.exports = { content: [ "./src/**/*.{html,ts}", ], theme: { extend: {}, }, plugins: [], }
Add the Tailwind directives to your src/styles.scss file:
@tailwind base; @tailwind components; @tailwind utilities;
Create a URL model to define the structure of the URL data. Add a new file src/app/models/url.model.ts:
export type Urls = Url[]; export interface Url { _id: string; originalUrl: string; shortUrl: string; clicks: number; expirationDate: string; createdAt: string; __v: number; }
Create a service to handle API calls related to URL shortening. Add a new file src/app/services/url.service.ts:
import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Url, Urls } from '../models/url.model'; import { environment } from '../../environments/environment'; @Injectable({ providedIn: 'root', }) export class UrlService { private apiUrl = environment.apiUrl; constructor(private http: HttpClient) {} shortenUrl(originalUrl: string): Observable<Url> { return this.http.post<Url>(`${this.apiUrl}/shorten`, { originalUrl }); } getAllUrls(): Observable<Urls> { return this.http.get<Urls>(`${this.apiUrl}/urls`); } getDetails(id: string): Observable<Url> { return this.http.get<Url>(`${this.apiUrl}/details/${id}`); } deleteUrl(id: string): Observable<Url> { return this.http.delete<Url>(`${this.apiUrl}/delete/${id}`); } }
Generate a new component for shortening URLs:
ng generate component shorten
Update the component's HTML (src/app/shorten/shorten.component.html) to as shown below:
<div class="max-w-md mx-auto p-4 shadow-lg rounded-lg mt-4"> <h2 class="text-2xl font-bold mb-2">URL Shortener</h2> <form [formGroup]="urlForm" (ngSubmit)="shortenUrl()"> <div class="flex items-center mb-2"> <input class="flex-1 p-2 border border-gray-300 rounded mr-4" formControlName="originalUrl" placeholder="Enter your URL" required /> <button class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600" type="submit"> Shorten </button> </div> @if (urlForm.get('originalUrl')?.invalid && (urlForm.get('originalUrl')?.dirty || urlForm.get('originalUrl')?.touched)) { <div class="text-red-500" role="alert" aria-live="assertive"> @if (urlForm.get('originalUrl')?.errors?.['required']) { URL is required. } @if (urlForm.get('originalUrl')?.errors?.['pattern']) { Invalid URL format. Please enter a valid URL starting with http:// or https://. } </div> } </form> @if (errorMsg) { <div class="p-4 bg-red-100 rounded mt-4"> <p class="text-red-500">{{ errorMsg }}</p> </div> } @if (shortUrl) { <div class="p-4 bg-green-100 rounded"> <p>Shortened URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + shortUrl" target="_blank">{{ shortUrl }}</a> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 border border-slate-950 rounded hover:bg-gray-300" (click)="copyUrl(redirectUrl + shortUrl)">Copy</button> @if (copyMessage) { <span class="text-green ml-2">{{ copyMessage }}</span> } </p> </div> } </div> <div class="max-w-md mx-auto mt-4 p-2"> <h2 class="text-2xl font-bold mb-4">All URLs</h2> @if (isloading) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> Loading... </div> </div> } @else if (error) { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> <p class="text-red-500">{{ error }}</p> </div> </div> } @else { @if (urls.length > 0 && !isloading && !error) { <ul> @for (url of urls; track $index) { <li class="p-2 border border-gray-300 rounded mb-2"> <div class="flex justify-between items-center"> <div> URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + url.shortUrl" target="_blank">{{ url.shortUrl }}</a> </div> <div class="flex justify-between items-center"> <button class="px-2 py-1 bg-blue-200 text-blue-800 rounded hover:bg-blue-300" (click)="showDetails(url.shortUrl)">Details</button> <button class="ml-2 px-2 py-1 bg-gray-200 text-gray-800 rounded hover:bg-gray-300" (click)="copyListUrl(redirectUrl + url.shortUrl, $index)">{{ copyIndex === $index ? 'Copied' : 'Copy' }}</button> <button class="ml-2 px-2 py-1 bg-red-200 text-red-800 rounded hover:bg-red-300" (click)="prepareDelete(url.shortUrl)">Delete</button> </div> </div> </li> } </ul> } @else { <div class="max-w-md mx-auto p-4 shadow-lg rounded-lg"> <div class="text-center p-4"> No URLs found. </div> </div> } } </div> @if (showDeleteModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">Confirm Deletion</h3> <p class="mb-4">Are you sure you want to delete this URL?</p> <button class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600" (click)="confirmDelete()">Yes, Delete</button> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400 ml-2" (click)="showDeleteModal = false">Cancel</button> </div> </div> } @if (showDetailsModal) { <div class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"> <div class="bg-white p-4 rounded shadow-lg"> <h3 class="text-xl font-bold mb-2">URL Details</h3> @if (isLoading) { <p class="mb-4">Loading...</p> } @else { <p class="mb-4">Short URL: <a class="text-blue-500 hover:text-blue-600" [href]="redirectUrl + selectedUrl.shortUrl" target="_blank">{{ selectedUrl.shortUrl }}</a></p> <p class="mb-4">Original URL: <a class="text-blue-500 hover:text-blue-600" [href]="selectedUrl.originalUrl" target="_blank">{{ selectedUrl.originalUrl }}</a></p> <p class="mb-4">Clicks: <span class="text-green-500">{{ selectedUrl.clicks }}</span></p> <p class="mb-4">Created At: {{ selectedUrl.createdAt | date: 'medium' }}</p> <p class="mb-4">Expires At: {{ selectedUrl.expirationDate | date: 'medium' }}</p> <button class="px-4 py-2 bg-gray-300 text-gray-800 rounded hover:bg-gray-400" (click)="showDetailsModal = false">Close</button> } </div> </div> }
Update the component's TypeScript file (src/app/shorten/shorten.component.ts) to handle form submissions and API interactions:
import { Component, inject, OnInit } from '@angular/core'; import { UrlService } from '../services/url.service'; import { FormControl, FormGroup, ReactiveFormsModule, Validators, } from '@angular/forms'; import { Url } from '../models/url.model'; import { environment } from '../../environments/environment'; import { DatePipe } from '@angular/common'; import { Subject, takeUntil } from 'rxjs'; @Component({ selector: 'app-shorten', standalone: true, imports: [DatePipe, ReactiveFormsModule], templateUrl: './shorten.component.html', styleUrl: './shorten.component.scss', }) export class ShortenComponent implements OnInit { shortUrl: string = ''; redirectUrl = environment.apiUrl + '/'; copyMessage: string = ''; copyListMessage: string = ''; urls: Url[] = []; showDeleteModal = false; showDetailsModal = false; urlToDelete = ''; copyIndex: number = -1; selectedUrl: Url = {} as Url; isLoading = false; isloading = false; error: string = ''; errorMsg: string = ''; urlForm: FormGroup = new FormGroup({}); private unsubscribe$: Subject<void> = new Subject<void>(); urlService = inject(UrlService); ngOnInit() { this.urlForm = new FormGroup({ originalUrl: new FormControl('', [ Validators.required, Validators.pattern('^(http|https)://.*$'), ]), }); this.getAllUrls(); } shortenUrl() { if (this.urlForm.valid) { this.urlService.shortenUrl(this.urlForm.value.originalUrl).pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('Shortened URL: ', response); this.shortUrl = response.shortUrl; this.getAllUrls(); }, error: (error) => { console.error('Error shortening URL: ', error); this.errorMsg = error?.error?.message || 'An error occurred!'; }, }); } } getAllUrls() { this.isloading = true; this.urlService.getAllUrls().pipe(takeUntil(this.unsubscribe$)).subscribe({ next: (response) => { // console.log('All URLs: ', response); this.urls = response; this.isloading = false; }, error: (error) => { console.error('Error getting all URLs: ', error); this.isloading = false; this.error = error?.error?.message || 'An error occurred!'; }, }); } showDetails(id: string) { this.showDetailsModal = true; this.getDetails(id); } getDetails(id: string) { this.isLoading = true; this.urlService.getDetails(id).subscribe({ next: (response) => { // console.log('URL Details: ', response); this.selectedUrl = response; this.isLoading = false; }, error: (error) => { console.error('Error getting URL details: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } copyUrl(url: string) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyMessage = 'Copied!'; setTimeout(() => { this.copyMessage = ''; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyMessage = 'Failed to copy URL'; }); } copyListUrl(url: string, index: number) { navigator.clipboard .writeText(url) .then(() => { // Optional: Display a message or perform an action after successful copy console.log('URL copied to clipboard!'); this.copyListMessage = 'Copied!'; this.copyIndex = index; setTimeout(() => { this.copyListMessage = ''; this.copyIndex = -1; }, 2000); }) .catch((err) => { console.error('Failed to copy URL: ', err); this.copyListMessage = 'Failed to copy URL'; }); } prepareDelete(url: string) { this.urlToDelete = url; this.showDeleteModal = true; } confirmDelete() { // Close the modal this.showDeleteModal = false; // Delete the URL this.deleteUrl(this.urlToDelete); } deleteUrl(id: string) { this.urlService.deleteUrl(id).subscribe({ next: (response) => { // console.log('Deleted URL: ', response); this.getAllUrls(); }, error: (error) => { console.error('Error deleting URL: ', error); this.error = error?.error?.message || 'An error occurred!'; }, }); } ngOnDestroy() { this.unsubscribe$.next(); this.unsubscribe$.complete(); } }
<router-outlet></router-outlet>
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { providers: [ provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient(), ], };
import { Routes } from '@angular/router'; export const routes: Routes = [ { path: '', loadComponent: () => import('./shorten/shorten.component').then((m) => m.ShortenComponent), }, ];
You have successfully built a URL shortener application using Angular and Tailwind CSS. This project demonstrates how to integrate modern frontend technologies to create a functional and stylish web application. With Angular's powerful features and Tailwind CSS's utility-first approach, you can build responsive and efficient web applications with ease.
Feel free to extend this application by adding features like user authentication, etc. Happy coding!
Visit the GitHub repository to explore the code in detail.
The above is the detailed content of Building a URL Shortener App with Angular and Tailwind CSS. For more information, please follow other related articles on the PHP Chinese website!