layout | title | nav_order | permalink |
---|---|---|---|
default |
Frontend Guide |
6 |
/Frontend |
The frontend of this project is built with Angular 19, leveraging modern Angular features including standalone components, signals for state management, the inject() function for dependency injection, and a powerful combination of Angular Material and TailwindCSS for styling.
-
Angular 19
- Standalone components architecture
- Modern dependency injection with
inject()
- Signal-based state management
- Reactive programming with RxJS
- Lazy-loaded routes for optimized performance
-
Angular Material 19
- Comprehensive UI component library
- Custom theme configuration
- Dark mode support
- Accessibility features
-
TailwindCSS v4
- Utility-first CSS framework
- Integration with Material Design
- Custom color schemes
- Responsive design utilities
-
Additional Libraries
- RxJS for reactive programming
- Angular JWT for token handling
The application uses the standalone component pattern for better modularity and tree-shaking:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
@Component({
selector: 'app-example',
standalone: true,
imports: [
CommonModule,
MatButtonModule,
RouterModule
],
template: `
<button mat-raised-button color="primary" routerLink="/dashboard">
Dashboard
</button>
`
})
export class ExampleComponent {}
Instead of constructor-based dependency injection, this project uses the modern inject()
function for cleaner code and better tree-shaking:
import { Component, inject, OnInit } from '@angular/core';
import { UserService } from '@core/services/user.service';
import { NotificationService } from '@core/services/notification.service';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-user-profile',
standalone: true,
imports: [CommonModule, MatCardModule, MatButtonModule],
template: `
<mat-card class="p-4 max-w-md mx-auto">
<mat-card-header>
<mat-card-title>{{ user()?.name }}</mat-card-title>
<mat-card-subtitle>{{ user()?.email }}</mat-card-subtitle>
</mat-card-header>
<mat-card-content class="mt-4">
<p *ngIf="user()">Role: {{ user()?.role }}</p>
</mat-card-content>
<mat-card-actions>
<button mat-raised-button color="primary" (click)="updateProfile()">
Update Profile
</button>
</mat-card-actions>
</mat-card>
`
})
export class UserProfileComponent implements OnInit {
// Modern dependency injection
private userService = inject(UserService);
private notificationService = inject(NotificationService);
// Signal-based state
user = this.userService.currentUser;
ngOnInit() {
this.loadUserProfile();
}
loadUserProfile() {
this.userService.getUserProfile().subscribe({
next: () => {},
error: (error) => {
this.notificationService.showError('Failed to load user profile');
}
});
}
updateProfile() {
// Profile update logic
}
}
The project uses Angular's signals for state management, providing a reactive and efficient way to handle UI state:
import { Component, signal, computed, effect } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
interface Task {
id: number;
title: string;
completed: boolean;
}
@Component({
selector: 'app-task-manager',
standalone: true,
imports: [CommonModule, MatButtonModule, MatCardModule],
template: `
<mat-card class="p-4">
<h2 class="text-xl font-semibold">Task Manager</h2>
<div class="my-3">
<p>Total Tasks: {{ totalTasks() }}</p>
<p>Completed: {{ completedCount() }}</p>
<p>Progress: {{ progressPercentage() }}%</p>
</div>
<div class="space-y-2">
<div *ngFor="let task of tasks()" class="flex items-center gap-2">
<input type="checkbox" [checked]="task.completed"
(change)="toggleTask(task.id)">
<span [class.line-through]="task.completed">{{ task.title }}</span>
</div>
</div>
<button mat-raised-button color="primary" class="mt-4" (click)="addTask()">
Add Task
</button>
</mat-card>
`
})
export class TaskManagerComponent {
// Primary signal for state
tasks = signal<Task[]>([
{ id: 1, title: 'Learn Angular Signals', completed: true },
{ id: 2, title: 'Create a task manager', completed: false },
{ id: 3, title: 'Share with the world', completed: false }
]);
// Computed signals for derived state
totalTasks = computed(() => this.tasks().length);
completedCount = computed(() =>
this.tasks().filter(task => task.completed).length
);
progressPercentage = computed(() => {
if (this.totalTasks() === 0) return 0;
return Math.round((this.completedCount() / this.totalTasks()) * 100);
});
// Effect for logging (side effects)
taskChangeLogger = effect(() => {
console.log(`Tasks updated. Completed: ${this.completedCount()} of ${this.totalTasks()}`);
});
toggleTask(id: number) {
this.tasks.update(tasks =>
tasks.map(task =>
task.id === id ? {...task, completed: !task.completed} : task
)
);
}
addTask() {
const newId = this.tasks().length > 0
? Math.max(...this.tasks().map(t => t.id)) + 1
: 1;
this.tasks.update(tasks => [
...tasks,
{ id: newId, title: `New Task ${newId}`, completed: false }
]);
}
}
The application integrates Angular Material's theming system with TailwindCSS for a cohesive design experience that supports both light and dark modes:
/* styles.scss */
@use "@angular/material" as mat;
// Custom theme configuration
$primary-palette: mat.define-palette(mat.$indigo-palette, 500);
$accent-palette: mat.define-palette(mat.$pink-palette, A200, A100, A400);
$warn-palette: mat.define-palette(mat.$red-palette);
// Light theme
$light-theme: mat.define-light-theme((
color: (
primary: $primary-palette,
accent: $accent-palette,
warn: $warn-palette,
),
typography: mat.define-typography-config(),
density: 0,
));
// Dark theme
$dark-theme: mat.define-dark-theme((
color: (
primary: $primary-palette,
accent: $accent-palette,
warn: $warn-palette,
),
typography: mat.define-typography-config(),
density: 0,
));
// Apply the light theme by default
@include mat.all-component-themes($light-theme);
// Apply the dark theme when .dark class is present
.dark {
@include mat.all-component-colors($dark-theme);
}
// TailwindCSS variables that integrate with Material
:root {
--primary: #{mat.get-color-from-palette($primary-palette, 500)};
--accent: #{mat.get-color-from-palette($accent-palette, A200)};
--warn: #{mat.get-color-from-palette($warn-palette, 500)};
}
.dark {
--primary: #{mat.get-color-from-palette($primary-palette, 300)};
--accent: #{mat.get-color-from-palette($accent-palette, A100)};
--warn: #{mat.get-color-from-palette($warn-palette, 300)};
}
// theme.service.ts
import { Injectable, signal, effect } from '@angular/core';
export type ThemeName = 'light' | 'dark';
@Injectable({
providedIn: 'root'
})
export class ThemeService {
// Theme state as a signal
theme = signal<ThemeName>('light');
constructor() {
this.initTheme();
// Apply theme changes automatically with an effect
effect(() => {
const currentTheme = this.theme();
if (typeof document !== 'undefined') {
document.documentElement.classList.toggle('dark', currentTheme === 'dark');
document.documentElement.style.colorScheme = currentTheme;
}
});
}
private initTheme() {
// Check user preference from localStorage
const savedTheme = localStorage.getItem('theme') as ThemeName;
if (savedTheme) {
this.theme.set(savedTheme);
return;
}
// Check system preference
if (typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
this.theme.set('dark');
}
}
toggleTheme() {
const newTheme = this.theme() === 'dark' ? 'light' : 'dark';
this.theme.set(newTheme);
localStorage.setItem('theme', newTheme);
return newTheme;
}
}
Authentication is implemented using JWT tokens with automatic refresh capability:
// auth.service.ts
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { BehaviorSubject, Observable, of, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { JwtHelperService } from '@auth0/angular-jwt';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private http = inject(HttpClient);
private router = inject(Router);
private jwtHelper = inject(JwtHelperService);
private apiUrl = '/api/auth';
// Signal for authentication state
isAuthenticated = signal<boolean>(false);
// Signal for current user's role
userRole = signal<string | null>(null);
login(credentials: { email: string; password: string }): Observable<any> {
return this.http.post<any>(`${this.apiUrl}/login`, credentials).pipe(
tap(response => {
if (response.token) {
// Store token
localStorage.setItem('token', response.token);
// Update authentication state using signals
this.isAuthenticated.set(true);
// Extract and set user role
const decodedToken = this.jwtHelper.decodeToken(response.token);
this.userRole.set(decodedToken.role);
}
}),
catchError(err => {
return throwError(() => new Error('Login failed'));
})
);
}
logout(): void {
// Clear storage
localStorage.removeItem('token');
// Update signals
this.isAuthenticated.set(false);
this.userRole.set(null);
// Navigate to login
this.router.navigate(['/login']);
}
checkAuthStatus(): Observable<boolean> {
const token = localStorage.getItem('token');
if (!token) {
this.isAuthenticated.set(false);
return of(false);
}
const isTokenExpired = this.jwtHelper.isTokenExpired(token);
if (isTokenExpired) {
this.logout();
return of(false);
}
// Token is valid
const decodedToken = this.jwtHelper.decodeToken(token);
this.userRole.set(decodedToken.role);
this.isAuthenticated.set(true);
return of(true);
}
}
/frontend
├── src/
│ ├── app/
│ │ ├── @core/ # Core functionality
│ │ │ ├── components/ # Shared components
│ │ │ ├── directives/ # Custom directives
│ │ │ ├── guards/ # Route guards
│ │ │ ├── interceptors/# HTTP interceptors
│ │ │ ├── layout/ # Layout components
│ │ │ ├── models/ # Interfaces and types
│ │ │ ├── pipes/ # Custom pipes
│ │ │ └── services/ # Global services
│ │ ├── feature/ # Feature modules
│ │ │ ├── contact/ # Contact management
│ │ │ ├── user/ # User management
│ │ │ ├── role/ # Role management
│ │ │ ├── permission/ # Permission management
│ │ │ └── settings/ # Application settings
│ │ ├── environments/ # Environment config
│ │ └── styles/ # Global styles
│ ├── assets/ # Static assets
│ └── index.html # Main HTML entry
├── Dockerfile # Production container
├── debug.dockerfile # Development container
└── package.json # Dependencies
The application includes a complete authentication system with:
- Login with JWT token
- Registration with validation
- Role-based permissions
- Profile management
Security is implemented using a comprehensive permission system:
// Permission guard
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';
import { PermissionService } from '@core/services/permission.service';
import { NotificationService } from '@core/services/notification.service';
export const PermissionGuard = (pageName: string, operation: string = 'Read'): CanActivateFn => {
return () => {
const permissionService = inject(PermissionService);
const router = inject(Router);
const notificationService = inject(NotificationService);
// Check if user has required permission
if (permissionService.hasPermission(pageName, operation)) {
return true;
}
// Display notification and redirect to home
notificationService.showError('You do not have permission to access this page');
router.navigate(['/home']);
return false;
};
};
// Permission directive for UI elements
import { Directive, Input, TemplateRef, ViewContainerRef, inject } from '@angular/core';
import { PermissionService } from '@core/services/permission.service';
@Directive({
selector: '[hasPermission]',
standalone: true
})
export class HasPermissionDirective {
private permissionService = inject(PermissionService);
private viewContainer = inject(ViewContainerRef);
private templateRef = inject(TemplateRef<any>);
@Input() set hasPermission(permission: { page: string; operation: string }) {
if (this.permissionService.hasPermission(permission.page, permission.operation)) {
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
}
When working with the frontend codebase, follow these guidelines:
- Create standalone components for better modularity
- Use signals for component state management
- Use computed signals for derived state
- Use the inject() function for dependency injection
- Leverage TailwindCSS utilities instead of custom CSS where possible
- Implement proper error handling for API calls
- Follow the permission model for security
- Keep components small and focused
- Use lazy loading for feature modules
- Maintain accessibility standards
Using Docker (recommended):
docker-compose up frontend
Locally:
cd frontend
npm install
npm start
npm run build
Or using Docker:
docker build -f Dockerfile -t contact-frontend .
npm test