Saturday, August 16, 2025
0 comments

Master Angular 20 Styling & UI Enhancements: A Beginner’s Guide to CSS/SCSS, Angular Material, and Reusable Header/Footer Components

 



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 Angular
  • 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.
Example: Component SCSS
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 {}
Explanation:
  • SCSS file is linked via styleUrls.
  • Nesting and mixins improve readability and reusability.
  • Styles are scoped to the component, preventing conflicts.
Configuring SCSSIf your project uses CSS, update to SCSS:
  1. Modify angular.json:
    json
    "projects": {
      "angular-routing-demo": {
        "architect": {
          "build": {
            "options": {
              "styles": [
                "src/styles.scss"
              ]
            }
          }
        }
      }
    }
  2. Rename styles.css to styles.scss:
    bash
    mv src/styles.css src/styles.scss
  3. Update component files to use .scss extensions (e.g., example.component.css to example.component.scss).
  4. 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).
Pros:
  • Encapsulation prevents style leaks.
  • Modular and maintainable.
  • Easy to override with :host or ::ng-deep for specific cases.
Cons:
  • Limited to the component, requiring duplication for shared styles.
  • Can complicate theming across components.
Global Styles
  • Defined in src/styles.scss or other global files in angular.json.
  • Apply to the entire application, useful for themes, resets, or shared utilities.
Pros:
  • Centralizes common styles (e.g., typography, colors).
  • Simplifies theming and global resets.
  • Reusable across components.
Cons:
  • Risk of style conflicts without careful naming.
  • Harder to maintain in large apps without conventions (e.g., BEM).
Example: Global vs. Component Styles
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;
}
Explanation:
  • 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
  1. Install Angular Material and CDK:
    bash
    ng add @angular/material
    • Select a theme (e.g., Indigo/Pink).
    • Enable typography and animations as prompted.
  2. Import Material modules in components or a shared module.
Basic Components
  • MatButtonModule: Buttons with Material styling.
  • MatCardModule: Cards for content organization.
  • MatToolbarModule: Toolbars for headers.
  • MatListModule: Lists for displaying items.
Example: Using Angular Material
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 {}
Explanation:
  • 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;
      }
    }
  }
}
Example: Footer Component
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>&copy; 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:
  1. Adding Angular Material for buttons, cards, and toolbars.
  2. Creating reusable header and footer components.
  3. Styling components with SCSS for a polished UI.
  4. Maintaining integration with freeapi.miniprojectideas.com and the StateService from Module 9.
Step 1: Set Up the ProjectEnsure the project is set up (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.
Step 2: Update Global Styles
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;
}
Explanation:
  • Imports Angular Material’s theming.
  • Defines a custom theme with Indigo/Pink.
  • Sets global variables and resets.
Step 3: Update User Model
typescript
// src/app/models/user.model.ts
export interface User {
  id: number;
  userName: string;
  emailId: string;
  role: string;
}
Step 4: Update StateService
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();
  }
}
Step 5: Update StorageService
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);
  }
}
Step 6: Update UserService
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);
  }
}
Step 7: Update app.routes.ts
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 }
];
Step 8: Update main.ts
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));
Explanation:
  • Added provideAnimations() for Angular Material animations.
Step 9: Update AppComponent
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 */
}
Explanation:
  • Adds HeaderComponent and FooterComponent.
  • Uses .container class from global styles for layout.
  • Adjusts min-height to prevent footer overlap.
Step 10: Update HeaderComponentAlready created above, ensuring it uses MatToolbarModule and MatButtonModule.Step 11: Update FooterComponentAlready created above, using MatToolbarModule.Step 12: Update AdminComponentUse Angular Material components for a polished UI.
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;
}
Step 13: Update UserComponent
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;
}
Step 14: Update UserInfoComponent
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;
}
Step 15: Update UserDetailsComponent
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;
}
Step 16: Update RegisterComponent
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);
}
Step 17: Update LoginComponent
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;
}
Step 18: Update Child Components
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;
}
Step 19: Update NotFoundComponent
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;
}
Step 20: Update FilterByRolePipe
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);
  }
}
Step 21: Update no-spaces.validator.ts
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;
  };
}
Step 22: Test the Application
  1. Run ng serve and visit http://localhost:4200.
  2. 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));
Steps:
  1. Ensure AppComponent is exported in app.component.ts.
  2. Verify file paths.
  3. Run ng serve.
Angular Material Styles Not AppliedCause: Missing theme or animations.Fix:
  • Ensure @angular/material/theming is imported in styles.scss.
  • Verify provideAnimations() is included in main.ts.
  • Check that Material modules are imported in components.
SCSS Compilation ErrorsCause: Syntax errors or missing sass dependency.Fix:
  • 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:

Featured Post

Master Angular 20 Basics: A Complete Beginner’s Guide with Examples and Best Practices

Welcome to the complete Angular 20 learning roadmap ! This series takes you step by step from basics to intermediate concepts , with hands...

Subscribe

 
Toggle Footer
Top