Building a URL Shortener App with Angular and Tailwind CSS
Manthan Ankolekar
Posted on July 26, 2024
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.
Prerequisites
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.
Project Setup
1. Creating a New Angular Project
First, create a new Angular project by running the following command in your terminal:
ng new url-shortener-app
cd url-shortener-app
2. Installing Tailwind CSS
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;
Building the URL Shortener
3. Creating the URL Model
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;
}
4. Setting Up the URL Service
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}`);
}
}
5. Creating the Shorten URL Component
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" (click)="refreshData()">{{ 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" (click)="refreshData()">{{
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.set(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.set(false)">Close</button>
}
</div>
</div>
}
6. Adding Logic to the Component
Update the component's TypeScript file (src/app/shorten/shorten.component.ts
) to handle form submissions and API interactions:
import { Component, inject, OnInit, signal } 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 = signal('');
redirectUrl = environment.apiUrl + '/';
copyMessage = signal('');
copyListMessage = signal('');
urls = signal<Url[]>([]);
showDeleteModal = signal(false);
showDetailsModal = signal(false);
urlToDelete = signal('');
copyIndex = signal(-1);
selectedUrl = signal<Url>({} as Url);
isLoading = signal(false);
isloading = signal(false);
error = signal('');
errorMsg = signal('');
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) => {
this.shortUrl.set(response.shortUrl);
this.getAllUrls();
},
error: (error) => {
console.error('Error shortening URL: ', error);
this.errorMsg.set(error?.error?.message || 'An error occurred!');
},
});
}
}
refreshData() {
this.getAllUrls();
}
getAllUrls() {
this.isloading.set(true);
this.urlService.getAllUrls().pipe(takeUntil(this.unsubscribe$)).subscribe({
next: (response) => {
this.urls.set(response);
this.isloading.set(false);
},
error: (error) => {
console.error('Error getting all URLs: ', error);
this.isloading.set(false);
this.error.set(error?.error?.message || 'An error occurred!');
},
});
}
showDetails(id: string) {
this.showDetailsModal.set(true);
this.getDetails(id);
}
getDetails(id: string) {
this.isLoading.set(true);
this.urlService.getDetails(id).pipe(takeUntil(this.unsubscribe$)).subscribe({
next: (response) => {
this.selectedUrl.set(response);
this.isLoading.set(false);
},
error: (error) => {
console.error('Error getting URL details: ', error);
this.isLoading.set(false);
this.error.set(error?.error?.message || 'An error occurred!');
},
});
}
copyUrl(url: string) {
navigator.clipboard
.writeText(url)
.then(() => {
console.log('URL copied to clipboard!');
this.copyMessage.set('Copied!');
setTimeout(() => {
this.copyMessage.set('');
}, 2000);
})
.catch((err) => {
console.error('Failed to copy URL: ', err);
this.copyMessage.set('Failed to copy URL');
});
}
copyListUrl(url: string, index: number) {
navigator.clipboard
.writeText(url)
.then(() => {
console.log('URL copied to clipboard!');
this.copyListMessage.set('Copied!');
this.copyIndex.set(index);
setTimeout(() => {
this.copyListMessage.set('');
this.copyIndex.set(-1);
}, 2000);
})
.catch((err) => {
console.error('Failed to copy URL: ', err);
this.copyListMessage.set('Failed to copy URL');
});
}
prepareDelete(url: string) {
this.urlToDelete.set(url);
this.showDeleteModal.set(true);
}
confirmDelete() {
this.showDeleteModal.set(false);
this.deleteUrl(this.urlToDelete());
}
deleteUrl(id: string) {
this.urlService.deleteUrl(id).pipe(takeUntil(this.unsubscribe$)).subscribe({
next: (response) => {
this.getAllUrls();
},
error: (error) => {
console.error('Error deleting URL: ', error);
this.error.set(error?.error?.message || 'An error occurred!');
},
});
}
ngOnDestroy() {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
7. Update the app component's HTML file (src/app/app.component.html
)
<router-outlet></router-outlet>
8. Update the app config file (src/app/app.config.ts
)
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(),
],
};
9. Update the app routes file (src/app/app.routes.ts
)
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./shorten/shorten.component').then((m) => m.ShortenComponent),
},
];
Conclusion
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!
Exploring the Code
Visit the GitHub repository to explore the code in detail.
Posted on July 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.