Angular + CSS/SCSS StructureAngular supports CSS and SCSS (Sass) for styling components, leveraging Angular’s component-based architecture. SCSS is a superset of CSS, offering features like variables, nesting, and mixins for more maintainable styles.CSS/SCSS in AngularExplanation:
Component-Level vs. Global StylesComponent-Level StylesExplanation:
Angular Material (Basic Components)Angular Material is a UI component library for Angular, providing pre-built, accessible, and customizable components like buttons, cards, toolbars, and dialogs. It follows Material Design principles for a polished look.Setting Up Angular MaterialExplanation:
Reusable Header/Footer ComponentsReusable header and footer components provide consistent navigation and branding across the app.Example: Header ComponentExample: Footer Component
Hands-On Project: Enhance angular-routing-demo with Angular Material and Reusable ComponentsLet’s enhance your angular-routing-demo project by:Explanation:Step 4: Update StateServiceStep 5: Update StorageServiceStep 6: Update UserServiceStep 7: Update app.routes.tsStep 8: Update main.tsExplanation:Explanation:Step 13: Update UserComponentStep 14: Update UserInfoComponentStep 15: Update UserDetailsComponentStep 16: Update RegisterComponentStep 17: Update LoginComponentStep 18: Update Child ComponentsStep 19: Update NotFoundComponentStep 20: Update FilterByRolePipeStep 21: Update no-spaces.validator.tsStep 22: 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 Styling & UI
- Component Styles: Defined in a component’s styleUrls or styles metadata, scoped to the component by default.
- SCSS Setup: Configure Angular to use SCSS by selecting it during project creation (ng new --style=scss) or updating angular.json.
- Encapsulation: Angular uses ViewEncapsulation to scope styles:
- Emulated (default): Styles are scoped to the component using unique attributes.
- None: Styles are global, affecting the entire app.
- ShadowDom: Uses native Shadow DOM for true encapsulation.
scss
// src/app/components/example/example.component.scss
.example-container {
padding: 20px;
background-color: #f5f5f5;
h2 {
color: #007bff;
}
button {
@include button-mixin;
}
}
@mixin button-mixin {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
&:hover {
background-color: #0056b3;
}
}
typescript
// src/app/components/example/example.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-example',
template: `
<div class="example-container">
<h2>Example Component</h2>
<button>Click Me</button>
</div>
`,
styleUrls: ['./example.component.scss'],
standalone: true
})
export class ExampleComponent {}
- SCSS file is linked via styleUrls.
- Nesting and mixins improve readability and reusability.
- Styles are scoped to the component, preventing conflicts.
- Modify angular.json:json
"projects": { "angular-routing-demo": { "architect": { "build": { "options": { "styles": [ "src/styles.scss" ] } } } } }
- Rename styles.css to styles.scss:bash
mv src/styles.css src/styles.scss
- Update component files to use .scss extensions (e.g., example.component.css to example.component.scss).
- Install Sass if needed:bash
npm install sass --save-dev
Component-Level vs. Global StylesComponent-Level Styles
- Defined in a component’s styleUrls or styles metadata.
- Scoped to the component using Angular’s Emulated encapsulation.
- Ideal for component-specific styles (e.g., unique button styles in UserComponent).
- Encapsulation prevents style leaks.
- Modular and maintainable.
- Easy to override with :host or ::ng-deep for specific cases.
- Limited to the component, requiring duplication for shared styles.
- Can complicate theming across components.
- Defined in src/styles.scss or other global files in angular.json.
- Apply to the entire application, useful for themes, resets, or shared utilities.
- Centralizes common styles (e.g., typography, colors).
- Simplifies theming and global resets.
- Reusable across components.
- Risk of style conflicts without careful naming.
- Harder to maintain in large apps without conventions (e.g., BEM).
scss
// src/styles.scss (Global)
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
}
body {
margin: 0;
font-family: Arial, sans-serif;
}
.global-button {
background-color: var(--primary-color);
color: white;
padding: 8px 16px;
border: none;
border-radius: 4px;
}
scss
// src/app/components/example/example.component.scss (Component-Level)
:host {
display: block;
padding: 20px;
}
.local-button {
background-color: var(--secondary-color);
color: white;
padding: 10px 20px;
border-radius: 6px;
}
- Global styles define app-wide variables and resets.
- Component styles use :host to style the component’s root element and define local styles.
Angular Material (Basic Components)Angular Material is a UI component library for Angular, providing pre-built, accessible, and customizable components like buttons, cards, toolbars, and dialogs. It follows Material Design principles for a polished look.Setting Up Angular Material
- Install Angular Material and CDK:bash
ng add @angular/material
- Select a theme (e.g., Indigo/Pink).
- Enable typography and animations as prompted.
- Import Material modules in components or a shared module.
- MatButtonModule: Buttons with Material styling.
- MatCardModule: Cards for content organization.
- MatToolbarModule: Toolbars for headers.
- MatListModule: Lists for displaying items.
typescript
// src/app/components/example/example.component.ts
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-example',
template: `
<mat-card>
<mat-card-header>
<mat-card-title>Example Card</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>This is a Material card.</p>
<button mat-raised-button color="primary">Click Me</button>
</mat-card-content>
</mat-card>
`,
styleUrls: ['./example.component.scss'],
standalone: true,
imports: [MatButtonModule, MatCardModule]
})
export class ExampleComponent {}
- Import Material modules (MatButtonModule, MatCardModule) in the component.
- Use mat-raised-button and mat-card for styled UI components.
- color="primary" applies the theme’s primary color.
Reusable Header/Footer ComponentsReusable header and footer components provide consistent navigation and branding across the app.Example: Header Component
typescript
// src/app/components/header/header.component.ts
import { Component } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-header',
template: `
<mat-toolbar color="primary">
<span>Angular Routing Demo</span>
<nav>
<a mat-button routerLink="/user" routerLinkActive="active">User Dashboard</a>
<a mat-button routerLink="/register" routerLinkActive="active">Register</a>
<a mat-button routerLink="/login" routerLinkActive="active">Login</a>
<a mat-button routerLink="/admin" routerLinkActive="active">Admin Panel</a>
</nav>
</mat-toolbar>
`,
styleUrls: ['./header.component.scss'],
standalone: true,
imports: [RouterLink, RouterLinkActive, MatToolbarModule, MatButtonModule]
})
export class HeaderComponent {}
scss
// src/app/components/header/header.component.scss
mat-toolbar {
display: flex;
justify-content: space-between;
padding: 0 20px;
nav {
a {
margin-left: 10px;
&.active {
font-weight: bold;
border-bottom: 2px solid white;
}
}
}
}
typescript
// src/app/components/footer/footer.component.ts
import { Component } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
@Component({
selector: 'app-footer',
template: `
<mat-toolbar color="primary">
<span>© 2025 Angular Routing Demo</span>
</mat-toolbar>
`,
styleUrls: ['./footer.component.scss'],
standalone: true,
imports: [MatToolbarModule]
})
export class FooterComponent {}
scss
// src/app/components/footer/footer.component.scss
mat-toolbar {
justify-content: center;
padding: 10px 0;
}
Hands-On Project: Enhance angular-routing-demo with Angular Material and Reusable ComponentsLet’s enhance your angular-routing-demo project by:
- Adding Angular Material for buttons, cards, and toolbars.
- Creating reusable header and footer components.
- Styling components with SCSS for a polished UI.
- Maintaining integration with freeapi.miniprojectideas.com and the StateService from Module 9.
bash
ng new angular-routing-demo --style=scss
cd angular-routing-demo
ng add @angular/material
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 component components/header
ng g component components/footer
ng g pipe pipes/filter-by-role
ng g service services/user
ng g service services/storage
ng g service services/state
- Select the Indigo/Pink theme during ng add @angular/material.
- Enable typography and animations.
scss
// src/styles.scss
@import '@angular/material/theming';
@include mat-core();
$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$theme: mat-light-theme($primary, $accent);
@include angular-material-theme($theme);
:root {
--primary-color: #3f51b5;
--accent-color: #ff4081;
--background-color: #f5f5f5;
--text-color: #333;
}
body {
margin: 0;
font-family: Roboto, 'Helvetica Neue', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
- Imports Angular Material’s theming.
- Defines a custom theme with Indigo/Pink.
- Sets global variables and resets.
typescript
// src/app/models/user.model.ts
export interface User {
id: number;
userName: string;
emailId: string;
role: string;
}
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();
}
}
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';
import { provideAnimations } from '@angular/platform-browser/animations';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
provideAnimations()
]
}).catch(err => console.error(err));
- Added provideAnimations() for Angular Material animations.
typescript
// src/app/app.component.ts
import { Component, OnInit } from '@angular/core';
import { Router, NavigationStart, NavigationEnd, RouterOutlet } from '@angular/router';
import { HeaderComponent } from './components/header/header.component';
import { FooterComponent } from './components/footer/footer.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
standalone: true,
imports: [RouterOutlet, HeaderComponent, FooterComponent]
})
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
<app-header></app-header>
<main class="container">
<router-outlet></router-outlet>
</main>
<app-footer></app-footer>
scss
// src/app/app.component.scss
main.container {
min-height: calc(100vh - 128px); /* Adjust for header/footer height */
}
- Adds HeaderComponent and FooterComponent.
- Uses .container class from global styles for layout.
- Adjusts min-height to prevent footer overlap.
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';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
@Component({
selector: 'app-admin',
templateUrl: './admin.component.html',
styleUrls: ['./admin.component.scss'],
standalone: true,
imports: [
FormsModule,
RouterOutlet,
RouterLink,
FilterByRolePipe,
MatCardModule,
MatButtonModule,
MatSelectModule,
MatInputModule,
MatFormFieldModule
]
})
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
) {
this.users$ = of([]);
}
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
<mat-card class="container">
<mat-card-header>
<mat-card-title>Admin Panel</mat-card-title>
</mat-card-header>
<mat-card-content>
<nav>
<a mat-button routerLink="dashboard" routerLinkActive="active">Dashboard</a>
<a mat-button routerLink="settings" routerLinkActive="active">Settings</a>
</nav>
<router-outlet></router-outlet>
<h3>Admin Users List</h3>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="isLoading" class="loading">Loading users...</div>
<div *ngIf="users$ | async as users; else loading">
<mat-form-field appearance="fill">
<mat-label>Filter by Role</mat-label>
<mat-select [(ngModel)]="filterRole" (ngModelChange)="setFilterRole($event)">
<mat-option value="all">All</mat-option>
<mat-option value="admin">Admin</mat-option>
<mat-option value="user">User</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" (click)="toggleHighlight()">Toggle Highlight</button>
<mat-card *ngFor="let user of users | filterByRole:filterRole" [ngClass]="{'highlight': isHighlighted && user.role === 'admin'}">
<mat-card-content>
{{ user.userName | uppercase }} ({{ user.role }}) - {{ user.emailId }}
<button mat-raised-button color="accent" (click)="selectUser(user)">Select</button>
</mat-card-content>
</mat-card>
<div *ngIf="selectedUser$ | async as selectedUser">
<p>Selected: {{ selectedUser.userName }}</p>
<button mat-raised-button color="warn" (click)="clearSelectedUser()">Clear Selection</button>
</div>
<mat-form-field appearance="fill">
<mat-label>Search users</mat-label>
<input matInput #searchInput>
<button mat-button (click)="logSearch(searchInput.value)">Search</button>
</mat-form-field>
</div>
<ng-template #loading>Loading users...</ng-template>
</mat-card-content>
</mat-card>
scss
// src/app/components/admin/admin.component.scss
.container {
margin: 20px;
}
mat-card {
margin: 10px 0;
padding: 10px;
}
.highlight {
background-color: #e0f7fa;
}
nav {
margin-bottom: 20px;
a {
margin-right: 10px;
&.active {
font-weight: bold;
border-bottom: 2px solid var(--primary-color);
}
}
}
.loading {
text-align: center;
color: var(--primary-color);
}
.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';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatListModule } from '@angular/material/list';
@Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.scss'],
standalone: true,
imports: [
FormsModule,
RouterLink,
UserInfoComponent,
MatCardModule,
MatButtonModule,
MatInputModule,
MatFormFieldModule,
MatListModule
]
})
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
) {
this.users$ = of([]);
}
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
<mat-card class="container">
<mat-card-header>
<mat-card-title>User Dashboard</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="isLoading" class="loading">Loading users...</div>
<mat-form-field appearance="fill">
<mat-label>Search users</mat-label>
<input matInput [(ngModel)]="searchQuery" (ngModelChange)="updateQuery()">
</mat-form-field>
<p>Search Query: {{ searchQuery }}</p>
<app-user-info [user]="selectedUser$ | async" (clearUser)="clearSelectedUser()"></app-user-info>
<router-outlet></router-outlet>
<mat-list *ngIf="users$ | async as users; else loading">
<mat-list-item *ngFor="let user of users">
<a mat-button routerLink="/user/{{ user.id }}">{{ user.userName }} ({{ user.role }}) - {{ user.emailId }}</a>
</mat-list-item>
</mat-list>
<ng-template #loading>Loading users...</ng-template>
</mat-card-content>
</mat-card>
scss
// src/app/components/user/user.component.scss
.container {
margin: 20px;
}
mat-list {
margin-top: 20px;
}
mat-list-item {
margin: 5px 0;
a {
text-decoration: none;
color: var(--primary-color);
&:hover {
text-decoration: underline;
}
}
}
.loading {
text-align: center;
color: var(--primary-color);
}
.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';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-user-info',
template: `
<mat-card *ngIf="user">
<mat-card-content>
<p>Selected User: {{ user.userName }} ({{ user.role }})</p>
<button mat-raised-button color="warn" (click)="clearSelection()">Clear Selection</button>
</mat-card-content>
</mat-card>
`,
styleUrls: ['./user-info.component.scss'],
standalone: true,
imports: [MatCardModule, MatButtonModule]
})
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();
}
}
scss
// src/app/components/user-info/user-info.component.scss
mat-card {
margin: 10px 0;
padding: 10px;
}
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';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-user-details',
templateUrl: './user-details.component.html',
styleUrls: ['./user-details.component.scss'],
standalone: true,
imports: [RouterLink, MatCardModule, MatButtonModule]
})
export class UserDetailsComponent implements OnInit {
user$: Observable<User | undefined>;
isLoading = false;
error: string | null = null;
constructor(
private route: ActivatedRoute,
private userService: UserService
) {
this.user$ = of(undefined);
}
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
<mat-card class="container">
<mat-card-header>
<mat-card-title>User Details</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="isLoading" class="loading">Loading user...</div>
<div *ngIf="user$ | async as user; else noUser">
<p>Name: {{ user.userName }}</p>
<p>Email: {{ user.emailId }}</p>
<p>Role: {{ user.role | uppercase }}</p>
</div>
<ng-template #noUser><p>User not found</p></ng-template>
<a mat-button routerLink="/user">Back to User Dashboard</a>
</mat-card-content>
</mat-card>
scss
// src/app/components/user-details/user-details.component.scss
.container {
margin: 20px;
}
.loading {
text-align: center;
color: var(--primary-color);
}
.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';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
@Component({
selector: 'app-register',
templateUrl: './register.component.html',
styleUrls: ['./register.component.scss'],
standalone: true,
imports: [
ReactiveFormsModule,
RouterLink,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule
]
})
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
<mat-card class="container">
<mat-card-header>
<mat-card-title>User Registration</mat-card-title>
</mat-card-header>
<mat-card-content>
<div *ngIf="error" class="error">{{ error }}</div>
<div *ngIf="isLoading" class="loading">Registering user...</div>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill">
<mat-label>Username</mat-label>
<input matInput formControlName="userName">
<mat-error *ngIf="registerForm.get('userName')?.hasError('required') && registerForm.get('userName')?.touched">
Username is required
</mat-error>
<mat-error *ngIf="registerForm.get('userName')?.hasError('minlength') && registerForm.get('userName')?.touched">
Username must be at least 3 characters
</mat-error>
<mat-error *ngIf="registerForm.get('userName')?.hasError('noSpaces') && registerForm.get('userName')?.touched">
Username cannot contain spaces
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Email</mat-label>
<input matInput formControlName="emailId">
<mat-error *ngIf="registerForm.get('emailId')?.hasError('required') && registerForm.get('emailId')?.touched">
Email is required
</mat-error>
<mat-error *ngIf="registerForm.get('emailId')?.hasError('email') && registerForm.get('emailId')?.touched">
Invalid email format
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Role</mat-label>
<mat-select formControlName="role">
<mat-option value="user">User</mat-option>
<mat-option value="admin">Admin</mat-option>
</mat-select>
</mat-form-field>
<button mat-raised-button color="primary" type="submit" [disabled]="registerForm.invalid || isLoading">Register</button>
</form>
<a mat-button routerLink="/user">Back to User Dashboard</a>
</mat-card-content>
</mat-card>
scss
// src/app/components/register/register.component.scss
.container {
margin: 20px;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
}
mat-form-field {
width: 100%;
max-width: 400px;
}
.error {
color: red;
font-size: 0.9em;
}
.loading {
text-align: center;
color: var(--primary-color);
}
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';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
standalone: true,
imports: [
FormsModule,
RouterLink,
MatCardModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule
]
})
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
<mat-card class="container">
<mat-card-header>
<mat-card-title>Admin Login</mat-card-title>
</mat-card-header>
<mat-card-content>
<form (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill">
<mat-label>Username</mat-label>
<input matInput [(ngModel)]="username" name="username" required>
<mat-error *ngIf="form.username?.errors?.['required'] && form.username?.touched">
Username is required
</mat-error>
</mat-form-field>
<mat-form-field appearance="fill">
<mat-label>Password</mat-label>
<input matInput [(ngModel)]="password" name="password" type="password" required>
<mat-error *ngIf="form.password?.errors?.['required'] && form.password?.touched">
Password is required
</mat-error>
</mat-form-field>
<div *ngIf="errorMessage" class="error">{{ errorMessage }}</div>
<button mat-raised-button color="primary" type="submit" [disabled]="!form.valid">Login</button>
</form>
<a mat-button routerLink="/user">Back to User Dashboard</a>
</mat-card-content>
</mat-card>
scss
// src/app/components/login/login.component.scss
.container {
margin: 20px;
}
form {
display: flex;
flex-direction: column;
gap: 10px;
}
mat-form-field {
width: 100%;
max-width: 400px;
}
.error {
color: red;
font-size: 0.9em;
}
typescript
// src/app/components/admin-dashboard/admin-dashboard.component.ts
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-admin-dashboard',
template: `
<mat-card class="container">
<mat-card-header>
<mat-card-title>Admin Dashboard</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Welcome to the Admin Dashboard!</p>
</mat-card-content>
</mat-card>
`,
styleUrls: ['./admin-dashboard.component.scss'],
standalone: true,
imports: [MatCardModule]
})
export class AdminDashboardComponent {}
scss
// src/app/components/admin-dashboard/admin-dashboard.component.scss
.container {
margin: 20px;
}
typescript
// src/app/components/admin-settings/admin-settings.component.ts
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
@Component({
selector: 'app-admin-settings',
template: `
<mat-card class="container">
<mat-card-header>
<mat-card-title>Admin Settings</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>Configure settings here.</p>
</mat-card-content>
</mat-card>
`,
styleUrls: ['./admin-settings.component.scss'],
standalone: true,
imports: [MatCardModule]
})
export class AdminSettingsComponent {}
scss
// src/app/components/admin-settings/admin-settings.component.scss
.container {
margin: 20px;
}
typescript
// src/app/components/not-found/not-found.component.ts
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-not-found',
template: `
<mat-card class="container">
<mat-card-header>
<mat-card-title>404 - Page Not Found</mat-card-title>
</mat-card-header>
<mat-card-content>
<a mat-button routerLink="/user">Go back to User Dashboard</a>
</mat-card-content>
</mat-card>
`,
styleUrls: ['./not-found.component.scss'],
standalone: true,
imports: [RouterLink, MatCardModule, MatButtonModule]
})
export class NotFoundComponent {}
scss
// src/app/components/not-found/not-found.component.scss
.container {
margin: 20px;
text-align: center;
}
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:
- Header/Footer: Verify consistent navigation and footer across routes.
- Admin Panel (/admin):
- Fetch users from freeapi.miniprojectideas.com.
- Select a user and verify it updates in UserInfoComponent and local storage.
- Check Material Design components (cards, buttons, select).
- User Dashboard (/user):
- View the selected user in UserInfoComponent.
- Test search functionality and Material list styling.
- Register (/register):
- Add a new user with Material form fields.
- Login (/login):
- Log in with admin1/admin123.
- User Details (/user/:id):
- View user details in a Material card.
- 404 Page (/invalid):
- Confirm the Not Found page uses Material styling.
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';
import { provideAnimations } from '@angular/platform-browser/animations';
bootstrapApplication(AppComponent, {
providers: [
provideRouter(routes),
provideHttpClient(),
provideAnimations()
]
}).catch(err => console.error(err));
- Ensure AppComponent is exported in app.component.ts.
- Verify file paths.
- Run ng serve.
- Ensure @angular/material/theming is imported in styles.scss.
- Verify provideAnimations() is included in main.ts.
- Check that Material modules are imported in components.
- Install Sass: npm install sass --save-dev.
- Check SCSS syntax (e.g., missing semicolons, incorrect nesting).
- Ensure angular.json references .scss files.
Best Practices for Styling & UI
- Use SCSS for Maintainability: Leverage variables, nesting, and mixins.
- Prefer Component-Level Styles: Keep styles modular unless shared.
- Use Angular Material for Consistency: Adopt Material Design for professional UI.
- Centralize Themes: Define colors and typography in styles.scss.
- Responsive Design: Use flexbox or CSS Grid for layouts.
- Accessibility: Ensure Material components meet ARIA standards.
0 comments:
Post a Comment