Why State Management?State management involves organizing and maintaining the application’s data (state) to ensure components have access to consistent, up-to-date information. In Angular, state includes user data, UI states (e.g., loading indicators), form inputs, or API responses.Why It Matters
Service-Based State vs. External LibrariesAngular offers multiple approaches to state management, primarily service-based and external libraries.Service-Based State ManagementUsing Angular services with RxJS (e.g., BehaviorSubject) to manage state is simple and built into the framework.Pros:
Intro to RxJS BehaviorSubjectA BehaviorSubject is a type of RxJS Subject that:Explanation:
Hands-On Project: Small Shared State DemoLet’s extend your angular-routing-demo project to manage the selected user state using a BehaviorSubject in a new StateService. The AdminComponent will select a user, update the state, and persist it in local storage. The UserComponent will display the selected user reactively, building on the Module 8 setup with the freeapi.miniprojectideas.com API.Step 1: Set Up the ProjectEnsure the project is set up (from Module 8):Step 2: Create StateServiceExplanation:Step 4: Update StorageServiceStep 5: Update UserServiceStep 6: Update app.routes.tsStep 7: Update main.tsStep 8: Update AppComponentStep 9: Update AdminComponentUse StateService to manage the selected user state.Step 10: Update UserComponentUse StateService to display the selected user reactively.Step 11: Update UserInfoComponentStep 12: Update UserDetailsComponentStep 13: Update RegisterComponentStep 14: Update LoginComponentStep 15: Update Child ComponentsStep 16: Update NotFoundComponentStep 17: Update FilterByRolePipeStep 18: Update no-spaces.validator.tsStep 19: Test the Application
Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: Incorrect import in main.ts.Fix:Steps:
Best Practices for State Management
- Consistency: Ensures all components reflect the same data (e.g., a selected user).
- Scalability: Simplifies data management as the app grows.
- Reactivity: Updates UI automatically when data changes.
- Testability: Centralizes state logic for easier testing.
- User Experience: Prevents inconsistencies (e.g., outdated user lists).
- Sharing user selections across components (e.g., selecting a user in AdminComponent and displaying it in UserComponent).
- Managing loading/error states for API calls.
- Persisting form data or user preferences.
Service-Based State vs. External LibrariesAngular offers multiple approaches to state management, primarily service-based and external libraries.Service-Based State ManagementUsing Angular services with RxJS (e.g., BehaviorSubject) to manage state is simple and built into the framework.Pros:
- Lightweight: No external dependencies.
- Familiar: Leverages Angular services and RxJS, which you’re already using.
- Flexible: Suitable for small to medium-sized apps.
- Easy to Test: Services are injectable and testable.
- Limited Scalability: Can become complex in large apps with many states.
- Manual Management: Requires careful handling of subscriptions and updates.
- Scalability: Handles complex state in large apps.
- Predictability: Enforces patterns like Redux (e.g., actions, reducers).
- Tooling: Includes dev tools for debugging state changes.
- Immutability: Ensures predictable state updates.
- Learning Curve: Requires understanding new concepts (e.g., actions, selectors).
- Boilerplate: More code for simple tasks.
- Overhead: Adds dependencies and complexity for small apps.
Intro to RxJS BehaviorSubjectA BehaviorSubject is a type of RxJS Subject that:
- Holds a single value (or initial value) and emits it to new subscribers.
- Updates all subscribers when the value changes.
- Is ideal for managing shared state reactively.
- Initial Value: Requires an initial value (e.g., null or an empty object).
- Current Value: Accessible via getValue() or subscriptions.
- Reactive Updates: Automatically notifies subscribers of changes.
typescript
// src/app/services/state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class StateService {
private userState = new BehaviorSubject<string | null>(null);
userState$ = this.userState.asObservable();
setUser(user: string | null) {
this.userState.next(user);
}
getCurrentUser() {
return this.userState.getValue();
}
}
typescript
// src/app/components/example/example.component.ts
import { Component, OnInit } from '@angular/core';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-example',
template: `
p: Current User: {{ user$ | async }}
button: (click)="updateUser()": Update User
`,
standalone: true
})
export class ExampleComponent implements OnInit {
user$ = this.stateService.userState$;
constructor(private stateService: StateService) {}
ngOnInit() {
console.log('Current user:', this.stateService.getCurrentUser());
}
updateUser() {
this.stateService.setUser('Alice');
}
}
- BehaviorSubject: Initializes with null and emits updates via next().
- asObservable(): Exposes the read-only Observable for subscriptions.
- async pipe: Renders the latest value in the template.
Hands-On Project: Small Shared State DemoLet’s extend your angular-routing-demo project to manage the selected user state using a BehaviorSubject in a new StateService. The AdminComponent will select a user, update the state, and persist it in local storage. The UserComponent will display the selected user reactively, building on the Module 8 setup with the freeapi.miniprojectideas.com API.Step 1: Set Up the ProjectEnsure the project is set up (from Module 8):
bash
ng new angular-routing-demo
cd angular-routing-demo
ng g component components/user
ng g component components/admin
ng g component components/user-details
ng g component components/not-found
ng g component components/admin-dashboard
ng g component components/admin-settings
ng g component components/register
ng g component components/login
ng g component components/user-info
ng g pipe pipes/filter-by-role
ng g service services/user
ng g service services/storage
ng g service services/state
typescript
// src/app/services/state.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { User } from '../models/user.model';
import { StorageService } from './storage.service';
@Injectable({
providedIn: 'root'
})
export class StateService {
private selectedUserSubject = new BehaviorSubject<User | null>(null);
selectedUser$ = this.selectedUserSubject.asObservable();
constructor(private storageService: StorageService) {
const storedUser = this.storageService.getLocalStorage('selectedUser');
if (storedUser) {
this.selectedUserSubject.next(storedUser);
}
}
setSelectedUser(user: User | null) {
this.selectedUserSubject.next(user);
if (user) {
this.storageService.setLocalStorage('selectedUser', user);
} else {
this.storageService.clearLocalStorage('selectedUser');
}
}
getCurrentUser() {
return this.selectedUserSubject.getValue();
}
}
- BehaviorSubject<User | null>: Holds the selected user state, initialized as null.
- selectedUser$: Exposes the state as an Observable.
- Integrates with StorageService to persist state in local storage.
- Loads stored user on initialization.
typescript
// src/app/models/user.model.ts
export interface User {
id: number;
userName: string;
emailId: string;
role: string;
}
typescript
// src/app/services/storage.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StorageService {
setLocalStorage(key: string, value: any) {
localStorage.setItem(key, JSON.stringify(value));
}
getLocalStorage(key: string) {
const data = localStorage.getItem(key);
return data ? JSON.parse(data) : null;
}
clearLocalStorage(key: string) {
localStorage.removeItem(key);
}
}
typescript
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { User } from '../models/user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private apiUrl = 'https://freeapi.miniprojectideas.com/api/User/GetAllUsers';
private localUsers: User[] = [
{ id: 1, userName: 'Alice', emailId: 'alice@example.com', role: 'admin' },
{ id: 2, userName: 'Bob', emailId: 'bob@example.com', role: 'user' },
{ id: 3, userName: 'Charlie', emailId: 'charlie@example.com', role: 'admin' },
{ id: 4, userName: 'David', emailId: 'david@example.com', role: 'user' }
];
private admins = [
{ username: 'admin1', password: 'admin123' },
{ username: 'admin2', password: 'admin456' }
];
constructor(private http: HttpClient) {}
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.apiUrl).pipe(
catchError(error => {
console.error('Error fetching users from API:', error);
return of(this.localUsers);
})
);
}
getUserById(id: number): Observable<User | undefined> {
return this.http.get<User>(`${this.apiUrl}/${id}`).pipe(
catchError(error => {
console.error('Error fetching user by ID:', error);
const localUser = this.localUsers.find(user => user.id === id);
return of(localUser);
})
);
}
addUser(user: User): Observable<any> {
return this.http.post(this.apiUrl, user).pipe(
catchError(error => {
console.error('Error adding user:', error);
this.localUsers.push(user);
return of(user);
})
);
}
validateAdmin(username: string, password: string): boolean {
return this.admins.some(admin => admin.username === username && admin.password === password);
}
}
typescript
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { UserComponent } from './components/user/user.component';
import { AdminComponent } from './components/admin/admin.component';
import { UserDetailsComponent } from './components/user-details/user-details.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import { AdminDashboardComponent } from './components/admin-dashboard/admin-dashboard.component';
import { AdminSettingsComponent } from './components/admin-settings/admin-settings.component';
import { RegisterComponent } from './components/register/register.component';
import { LoginComponent } from './components/login/login.component';
export const routes: Routes = [
{
path: 'user',
component: UserComponent,
children: [
{ path: ':id', component: UserDetailsComponent }
]
},
{ path: 'register', component: RegisterComponent },
{ path: 'login', component: LoginComponent },
{
path: 'admin',
component: AdminComponent,
children: [
{ path: 'dashboard', component: AdminDashboardComponent },
{ path: 'settings', component: AdminSettingsComponent },
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' }
]
},
{ path: '', redirectTo: '/user', pathMatch: 'full' },
{ path: '**', component: NotFoundComponent }
];
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes), provideHttpClient()]
}).catch(err => console.error(err));
typescript
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, RouterOutlet, RouterLink } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
standalone: true,
imports: [RouterOutlet, RouterLink]
})
export class AppComponent implements OnInit {
constructor(private router: Router) {}
ngOnInit() {
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
console.log('Navigation started:', event.url);
} else if (event instanceof NavigationEnd) {
console.log('Navigation ended:', event.url);
}
});
}
}
typescript
// src/app/app.component.html
nav:
a: routerLink="/user" routerLinkActive="active": User Dashboard
a: routerLink="/register" routerLinkActive="active": Register
a: routerLink="/login" routerLinkActive="active": Login
a: routerLink="/admin" routerLinkActive="active": Admin Panel
router-outlet
css
// src/app/app.component.css
nav { margin: 20px; }
a { margin-right: 10px; text-decoration: none; }
.active { font-weight: bold; color: #007bff; }
typescript
// src/app/components/admin/admin.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterOutlet, RouterLink, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { FilterByRolePipe } from '../../pipes/filter-by-role.pipe';
import { UserService } from '../../services/user.service';
import { StateService } from '../../services/state.service';
import { User } from '../../models/user.model';
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.css'],
standalone: true,
imports: [FormsModule, RouterOutlet, RouterLink, FilterByRolePipe]
})
export class AdminComponent implements OnInit, OnDestroy {
users$: Observable<User[]>;
filterRole = 'admin';
isHighlighted = false;
isLoading = false;
error: string | null = null;
selectedUser$ = this.stateService.selectedUser$;
constructor(
private userService: UserService,
private stateService: StateService,
private router: Router
) {}
ngOnInit() {
this.isLoading = true;
this.users$ = this.userService.getUsers().pipe(
catchError(err => {
this.error = 'Failed to load users: ' + err.message;
this.isLoading = false;
return of([]);
}),
finalize(() => this.isLoading = false)
);
}
ngOnDestroy() {
console.log('AdminComponent: Destroyed');
}
toggleHighlight() {
this.isHighlighted = !this.isHighlighted;
}
setFilterRole(role: string) {
this.filterRole = role;
}
logSearch(value: string) {
console.log('Search:', value);
}
selectUser(user: User) {
this.stateService.setSelectedUser(user);
this.router.navigate(['/user']);
}
clearSelectedUser() {
this.stateService.setSelectedUser(null);
}
}
typescript
// src/app/components/admin/admin.component.html
h2: Admin Panel
nav:
a: routerLink="dashboard" routerLinkActive="active": Dashboard
a: routerLink="settings" routerLinkActive="active": Settings
router-outlet
h3: Admin Users List
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Loading users...
div: *ngIf="users$ | async as users; else loading":
div:
label: Filter by Role:
select: [(ngModel)]="filterRole" (change)="setFilterRole(filterRole)":
option: value="all": All
option: value="admin": Admin
option: value="user": User
button: (click)="toggleHighlight()": Toggle Highlight
ul:
li: *ngFor="let user of users | filterByRole:filterRole" [ngClass]="{'highlight': isHighlighted && user.role === 'admin'}":
{{ user.userName | uppercase }} ({{ user.role }}) - {{ user.emailId }}
button: (click)="selectUser(user)": Select
div: *ngIf="selectedUser$ | async as selectedUser":
p: Selected: {{ selectedUser.userName }}
button: (click)="clearSelectedUser()": Clear Selection
input: #searchInput placeholder="Search users"
button: (click)="logSearch(searchInput.value)": Search
ng-template: #loading: Loading users...
css
// src/app/components/admin/admin.component.css
ul { list-style-type: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ccc; }
.highlight { background-color: #e0f7fa; }
nav { margin: 10px 0; }
a { margin-right: 10px; text-decoration: none; }
.active { font-weight: bold; color: #007bff; }
.loading { text-align: center; color: #007bff; }
.error { text-align: center; color: red; }
button { margin-left: 10px; }
typescript
// src/app/components/user/user.component.ts
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { StateService } from '../../services/state.service';
import { UserInfoComponent } from '../user-info/user-info.component';
import { User } from '../../models/user.model';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css'],
standalone: true,
imports: [FormsModule, RouterLink, UserInfoComponent]
})
export class UserComponent implements OnInit {
searchQuery = '';
users$: Observable<User[]>;
isLoading = false;
error: string | null = null;
selectedUser$ = this.stateService.selectedUser$;
constructor(
private route: ActivatedRoute,
private router: Router,
private userService: UserService,
private stateService: StateService
) {}
ngOnInit() {
this.isLoading = true;
this.users$ = this.userService.getUsers().pipe(
catchError(err => {
this.error = 'Failed to load users: ' + err.message;
this.isLoading = false;
return of([]);
}),
finalize(() => this.isLoading = false)
);
this.route.queryParamMap.subscribe(params => {
this.searchQuery = params.get('search') || '';
});
}
updateQuery() {
this.router.navigate(['/user'], { queryParams: { search: this.searchQuery } });
}
clearSelectedUser() {
this.stateService.setSelectedUser(null);
}
}
typescript
// src/app/components/user/user.component.html
h2: User Dashboard
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Loading users...
div:
label: Search users:
input: [(ngModel)]="searchQuery" placeholder="Search users"
button: (click)="updateQuery()": Search
p: Search Query: {{ searchQuery }}
app-user-info: [user]="selectedUser$ | async" (clearUser)="clearSelectedUser()"
router-outlet
ul: *ngIf="users$ | async as users; else loading":
li: *ngFor="let user of users":
a: routerLink="/user/{{ user.id }}": {{ user.userName }} ({{ user.role }}) - {{ user.emailId }}
ng-template: #loading: Loading users...
css
// src/app/components/user/user.component.css
ul { list-style-type: none; padding: 0; }
li { padding: 10px; margin: 5px 0; border: 1px solid #ccc; }
.loading { text-align: center; color: #007bff; }
.error { text-align: center; color: red; }
typescript
// src/app/components/user-info/user-info.component.ts
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { User } from '../../models/user.model';
import { StateService } from '../../services/state.service';
@Component({
selector: 'app-user-info',
template: `
div: *ngIf="user":
p: Selected User: {{ user.userName }} ({{ user.role }})
button: (click)="clearSelection()": Clear Selection
`,
standalone: true
})
export class UserInfoComponent {
@Input() user: User | null = null;
@Output() clearUser = new EventEmitter<void>();
constructor(private stateService: StateService) {}
clearSelection() {
this.stateService.setSelectedUser(null);
this.clearUser.emit();
}
}
typescript
// src/app/components/user-details/user-details.component.ts
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Observable, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { UserService } from '../../services/user.service';
import { User } from '../../models/user.model';
@Component({
selector: 'app-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.css'],
standalone: true,
imports: [RouterLink]
})
export class UserDetailsComponent implements OnInit {
user$: Observable<User | undefined>;
isLoading = false;
error: string | null = null;
constructor(
private route: ActivatedRoute,
private userService: UserService
) {}
ngOnInit() {
this.isLoading = true;
this.route.paramMap.subscribe(params => {
const id = Number(params.get('id'));
this.user$ = this.userService.getUserById(id).pipe(
catchError(err => {
this.error = 'Failed to load user: ' + err.message;
this.isLoading = false;
return of(undefined);
}),
finalize(() => this.isLoading = false)
);
});
}
}
typescript
// src/app/components/user-details/user-details.component.html
h2: User Details
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Loading user...
div: *ngIf="user$ | async as user; else noUser":
p: Name: {{ user.userName }}
p: Email: {{ user.emailId }}
p: Role: {{ user.role | uppercase }}
p: else noUser: User not found
p: Back to User Dashboard
css
// src/app/components/user-details/user-details.component.css
.loading { text-align: center; color: #007bff; }
.error { text-align: center; color: red; }
typescript
// src/app/components/register/register.component.ts
import { Component } from '@angular/core';
import { ReactiveFormsModule, FormGroup, FormControl, Validators } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';
import { noSpacesValidator } from '../../validators/no-spaces.validator';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.css'],
standalone: true,
imports: [ReactiveFormsModule, RouterLink]
})
export class RegisterComponent {
registerForm = new FormGroup({
userName: new FormControl('', [Validators.required, Validators.minLength(3), noSpacesValidator()]),
emailId: new FormControl('', [Validators.required, Validators.email]),
role: new FormControl('user', [Validators.required])
});
isLoading = false;
error: string | null = null;
constructor(private userService: UserService, private router: Router) {}
onSubmit() {
if (this.registerForm.valid) {
this.isLoading = true;
const formValue = this.registerForm.value;
const newUser = {
id: this.userService.getUsers().length + 1,
userName: formValue.userName || '',
emailId: formValue.emailId || '',
role: formValue.role || 'user'
};
this.userService.addUser(newUser).subscribe({
next: () => {
this.registerForm.reset();
this.router.navigate(['/user']);
},
error: err => {
this.error = 'Failed to register user: ' + err.message;
this.isLoading = false;
},
complete: () => {
this.isLoading = false;
}
});
}
}
}
typescript
// src/app/components/register/register.component.html
h2: User Registration
div: *ngIf="error" class="error": {{ error }}
div: *ngIf="isLoading" class="loading": Registering user...
form: [formGroup]="registerForm" (ngSubmit)="onSubmit()":
div:
label: Username:
input: formControlName="userName"
div: *ngIf="registerForm.get('userName')?.hasError('required') && registerForm.get('userName')?.touched":
Username is required
div: *ngIf="registerForm.get('userName')?.hasError('minlength') && registerForm.get('userName')?.touched":
Username must be at least 3 characters
div: *ngIf="registerForm.get('userName')?.hasError('noSpaces') && registerForm.get('userName')?.touched":
Username cannot contain spaces
div:
label: Email:
input: formControlName="emailId"
div: *ngIf="registerForm.get('emailId')?.hasError('required') && registerForm.get('emailId')?.touched":
Email is required
div: *ngIf="registerForm.get('emailId')?.hasError('email') && registerForm.get('emailId')?.touched":
Invalid email format
div:
label: Role:
select: formControlName="role":
option: value="user": User
option: value="admin": Admin
button: type="submit" [disabled]="registerForm.invalid || isLoading": Register
p: Back to User Dashboard
css
// src/app/components/register/register.component.css
form { margin: 20px; padding: 20px; border: 1px solid #ccc; }
input, select { padding: 5px; margin: 5px; width: 200px; }
div { margin-bottom: 10px; }
.error { color: red; font-size: 0.9em; }
.loading { text-align: center; color: #007bff; }
typescript
// src/app/components/login/login.component.ts
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { UserService } from '../../services/user.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css'],
standalone: true,
imports: [FormsModule, RouterLink]
})
export class LoginComponent {
username = '';
password = '';
errorMessage = '';
constructor(private userService: UserService, private router: Router) {}
onSubmit() {
if (this.userService.validateAdmin(this.username, this.password)) {
this.router.navigate(['/admin']);
} else {
this.errorMessage = 'Invalid username or password';
}
}
}
typescript
// src/app/components/login/login.component.html
h2: Admin Login
form: (ngSubmit)="onSubmit()":
div:
label: Username:
input: [(ngModel)]="username" name="username" required
div: *ngIf="form.username?.errors?.['required'] && form.username?.touched": Username is required
div:
label: Password:
input: [(ngModel)]="password" name="password" type="password" required
div: *ngIf="form.password?.errors?.['required'] && form.password?.touched": Password is required
div: *ngIf="errorMessage" class="error": {{ errorMessage }}
button: type="submit" [disabled]="!form.valid": Login
p: Back to User Dashboard
css
// src/app/components/login/login.component.css
form { margin: 20px; padding: 20px; border: 1px solid #ccc; }
input { padding: 5px; margin: 5px; width: 200px; }
div { margin-bottom: 10px; }
.error { color: red; font-size: 0.9em; }
typescript
// src/app/components/admin-dashboard/admin-dashboard.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-admin-dashboard',
template: `
h3: Admin Dashboard
p: Welcome to the Admin Dashboard!
`,
standalone: true
})
export class AdminDashboardComponent {}
typescript
// src/app/components/admin-settings/admin-settings.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-admin-settings',
template: `
h3: Admin Settings
p: Configure settings here.
`,
standalone: true
})
export class AdminSettingsComponent {}
typescript
// src/app/components/not-found/not-found.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-not-found',
template: `
h2: 404 - Page Not Found
p: Go back to User Dashboard
`,
standalone: true,
imports: [RouterLink]
})
export class NotFoundComponent {}
typescript
// src/app/pipes/filter-by-role.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';
import { User } from '../models/user.model';
@Pipe({
name: 'filterByRole',
standalone: true
})
export class FilterByRolePipe implements PipeTransform {
transform(users: User[], role: string): User[] {
if (!users || role === 'all') return users;
return users.filter(user => user.role === role);
}
}
typescript
// src/app/validators/no-spaces.validator.ts
import { AbstractControl, ValidatorFn } from '@angular/forms';
export function noSpacesValidator(): ValidatorFn {
return (control: AbstractControl): { [key: string]: any } | null => {
const hasSpaces = control.value?.includes(' ');
return hasSpaces ? { noSpaces: true } : null;
};
}
- Run ng serve and visit http://localhost:4200.
- Test features:
- Admin Panel (/admin):
- Fetch users from freeapi.miniprojectideas.com.
- Select a user (click “Select”) and verify it updates in UserInfoComponent and local storage (check DevTools > Application > Local Storage).
- Clear the selection and verify the state updates reactively.
- User Dashboard (/user):
- View the selected user in UserInfoComponent reactively via selectedUser$ | async.
- Clear the selection and confirm it updates across components.
- Register (/register):
- Add a new user and verify it appears in User and Admin lists.
- Login (/login):
- Log in with admin1/admin123.
- User Details (/user/:id):
- View user details.
- 404 Page (/invalid):
- Confirm the Not Found page appears.
- Admin Panel (/admin):
Troubleshooting Common ErrorsTS2305: Module '"./app/app"' has no exported member 'App'Cause: Incorrect import in main.ts.Fix:
typescript
// src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter, provideHttpClient } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
bootstrapApplication(AppComponent, {
providers: [provideRouter(routes), provideHttpClient()]
}).catch(err => console.error(err));
- Ensure AppComponent is exported in app.component.ts.
- Verify file paths.
- Run ng serve.
- Ensure next() is called with the new value in StateService.
- Verify components subscribe to selectedUser$ using async pipe or subscribe().
- Ensure setSelectedUser updates both the BehaviorSubject and local storage.
- Check JSON.stringify and JSON.parse for correct serialization.
Best Practices for State Management
- Use BehaviorSubject for Simple State: Ideal for small, reactive state needs.
- Centralize State: Keep state logic in a single service (e.g., StateService).
- Avoid Over-Subscription: Use async pipe to manage subscriptions automatically.
- Type Safety: Use interfaces (e.g., User) for state data.
- Combine with Storage: Persist critical state in local storage for durability.
- Consider Libraries for Large Apps: Evaluate NgRx or Akita for complex state needs.
0 comments:
Post a Comment