# Deployment and DevOps Standards for Angular Material
This document outlines the deployment and DevOps standards for Angular Material applications. Adhering to these standards ensures maintainable, performant, and secure deployments.
## 1. Build Processes and CI/CD
### 1.1. Standard: Use Angular CLI for Builds
**Do This:** Employ Angular CLI commands (e.g., "ng build") for production builds.
**Don't Do This:** Manually configure Webpack or other bundlers unless strictly necessary and justified by specific, advanced optimization requirements.
**Why:** The CLI provides a standardized, optimized build process tailored for Angular applications, including tree-shaking, minification, and AoT compilation, which significantly improve performance with Angular Material.
**Example:**
"""bash
# Development build
ng build
# Production build
ng build --configuration production --base-href /my-app/
"""
**Anti-Pattern:** Manually modifying the "webpack.config.js" file without a deep understanding of the implications. Angular CLI abstracts away much of the complexity and provides hooks for customization when needed via configuration files like "angular.json".
### 1.2. Standard: Configure Environments Accurately
**Do This:** Use environment-specific configuration files (e.g., "environment.ts", "environment.prod.ts") to manage API endpoints, feature flags, and logging levels.
**Don't Do This:** Hardcode environment-specific values directly in the component code.
**Why:** Separating configuration from code enables easier deployment across different environments (development, staging, production) without requiring code changes.
**Example:**
"""typescript
// environment.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api'
};
// environment.prod.ts
export const environment = {
production: true,
apiUrl: 'https://api.example.com/api'
};
"""
"""typescript
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { environment } from '../environments/environment';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
apiUrl: string = environment.apiUrl;
ngOnInit(): void {
console.log("API URL:", this.apiUrl);
}
}
"""
**Anti-Pattern:** Using "if (window.location.hostname === 'localhost')" to determine environment. This is unreliable and bypasses the purpose of well-defined environment configurations.
### 1.3. Standard: Implement CI/CD Pipelines
**Do This:** Automate build, test, and deployment processes using CI/CD tools like Jenkins, GitLab CI, GitHub Actions, or Azure DevOps.
**Don't Do This:** Deploy manually or rely on ad-hoc scripts.
**Why:** CI/CD pipelines ensure consistent and repeatable deployments, reduce the risk of human error, and enable faster release cycles.
**Example (GitHub Actions):**
"""yaml
# .github/workflows/deploy.yml
name: Deploy to Firebase Hosting
on:
push:
branches:
- main
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm install
- run: npm run build -- --prod
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT }}'
channelId: live
projectId: your-firebase-project-id
"""
**Anti-Pattern:** Checking "node_modules" into source control or deploying those folders. Pipelines should install fresh dependencies and build artifacts.
### 1.4. Standard: Utilize AOT Compilation and Tree Shaking
**Do This:** Ensure Ahead-of-Time (AOT) compilation and tree-shaking are enabled during production builds. They can be enable by doing "ng build --prod" or "ng build --configuration production"
**Don't Do This:** Rely on Just-in-Time (JIT) compilation in production.
**Why:** AoT compilation pre-compiles the Angular application during the build process, resulting in faster startup times and reduced payload sizes. Tree-shaking eliminates unused code from the final bundle, further optimizing performance and reducing total application size, which is especially important when using Angular Material, as only the modules and components that are used are shipped.
**Example:**
The Angular CLI automatically enables AoT compilation in production mode. Verify that "aot" is set to "true" within the "angular.json"'s production configuration section.
"""json
// angular.json
{
"projects": {
"my-app": {
"architect": {
"build": {
"configurations": {
"production": {
"aot": true,
"optimization": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
}
}
}
}
}
"""
**Anti-Pattern:** Disabling AoT compilation to workaround build issues. Investigate and address the root cause of any AoT-related errors. Also, not setting up budgets for the maximum allowed bundle sizes can lead to performance degradation over time as the application grows.
## 2. Production Considerations
### 2.1. Standard: Optimize Images and Assets
**Do This:** Compress images, use appropriate image formats (WebP when possible), and leverage lazy loading for non-critical assets.
**Don't Do This:** Serve large, unoptimized images or load all assets upfront.
**Why:** Optimized assets reduce page load times and improve user experience, especially on mobile devices.
**Example:**
Using the "imagemin-webpack-plugin" to optimize images during the build process:
"""javascript
// webpack.config.js
const ImageminPlugin = require('imagemin-webpack-plugin').default;
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
plugins: [
new ImageminPlugin({
test: /\.(jpe?g|png|gif|svg)$/i,
pngquant: {
quality: [.7, .9]
}
})
]
};
"""
**Anti-Pattern:** Directly referencing images from external sources without considering caching strategies.
### 2.2. Standard: Implement Caching Strategies
**Do This:** Utilize browser caching and service workers to cache static assets and API responses.
**Don't Do This:** Rely solely on server-side caching.
**Why:** Caching reduces network requests and improves the perceived performance of the application, particularly for returning users.
**Example:**
Using "@angular/service-worker" to implement a service worker:
1. Install "@angular/service-worker":
"""bash
ng add @angular/pwa
"""
2. Configure the service worker in "ngsw-config.json":
"""json
{
"index": "/index.html",
"assetGroups": [
{
"name": "app",
"installMode": "prefetch",
"resources": {
"files": [
"/favicon.ico",
"/index.html",
"/manifest.webmanifest",
"/assets/**",
"/*.css",
"/*.js"
],
"versionedFiles": [
"/*.bundle.*.js",
"/*.chunk.*.js",
"/*.map",
"/styles.*.css"
]
}
},
],
"dataGroups": [
{
"name": "api-performance",
"urls": ["/api/**"],
"cacheConfig": {
"strategy": "performance", //get quickest response, update cache later
"maxSize": 100,
"maxAge": "1h"
}
},
]
}
"""
**Anti-Pattern:** Failing to invalidate the cache when deploying new versions, resulting in users seeing outdated content. Hash assets and leverage versioned URLs to avoid stale caches.
### 2.3. Standard: Monitor Application Performance
**Do This:** Implement real-time monitoring using tools like New Relic, Datadog, or Sentry to track performance metrics, errors, and user behavior.
**Don't Do This:** Wait for users to report issues.
**Why:** Monitoring helps identify and resolve performance bottlenecks and errors proactively, ensuring a stable and performant user experience.
**Example:**
Integrating Sentry for error tracking:
1. Install "@sentry/angular":
"""bash
npm install @sentry/angular --save
"""
2. Configure Sentry in "app.module.ts":
"""typescript
import { BrowserModule } from '@angular/platform-browser';
import { NgModule, ErrorHandler } from '@angular/core';
import { AppComponent } from './app.component';
import * as Sentry from "@sentry/angular";
import { Router } from "@angular/router";
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
integrations: [
new Sentry.BrowserTracing({
tracePropagationTargets: ["localhost", "yourdomain.com", /^https:\/\/yourserver\.io\/api/],
routingInstrumentation: Sentry.routingInstrumentation,
}),
new Sentry.Replay(),
],
// Performance Monitoring
tracesSampleRate: 0.1, // Capture 10% of transactions for performance monitoring.
// Session Replay
replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production.
replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur.
});
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [
{
provide: ErrorHandler,
useValue: Sentry.createErrorHandler({
showDialog: true,
}),
},
],
bootstrap: [AppComponent]
})
export class AppModule {
constructor(router: Router) {
// Trace route changes for performance monitoring
Sentry.init({
dsn: "YOUR_SENTRY_DSN",
integrations: [Sentry.browserTracingIntegration({ router: router })],
tracesSampleRate: 0.2,
});
}
}
"""
**Anti-Pattern:** Ignoring or disabling error reporting in production. Ensure that you have a system in place to capture uncaught exceptions.
### 2.4 Standard: Implement Proper Lazy Loading Strategies
**Do This:** Lazy load Angular Material modules and components that are not immediately required upon application load. This is especially useful for large applications with many features.
**Don't Do This:** Load every Angular Material Module eagerly on initial load.
**Why:** Lazy loading reduces the initial bundle size, which results in faster startup times and improved responsiveness, thereby enhancing user experience.
**Example:**
"""typescript
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: 'my-feature',
loadChildren: () => import('./my-feature/my-feature.module').then(m => m.MyFeatureModule)
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full'
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
"""
"""typescript
// my-feature.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';
import { MatButtonModule } from '@angular/material/button';
import { MyFeatureComponent } from './my-feature.component';
const routes: Routes = [{ path: '', component: MyFeatureComponent }];
@NgModule({
declarations: [MyFeatureComponent],
imports: [CommonModule, RouterModule.forChild(routes), MatButtonModule], // Import Angular Material modules here
exports: [MyFeatureComponent]
})
export class MyFeatureModule { }
"""
**Anti-Pattern:** Lazy loading core modules or components critical for initial rendering. Carefully identify modules suitable for lazy loading based on usage patterns.
## 3. Security Best Practices
### 3.1. Standard: Sanitize User Input
**Do This:** Sanitize all user input to prevent cross-site scripting (XSS) attacks. Use Angular's built-in "DomSanitizer" for rendering potentially unsafe data.
**Don't Do This:** Directly render user-provided HTML without sanitization.
**Why:** XSS vulnerabilities can allow attackers to inject malicious scripts into your application, compromising user data and security.
**Example:**
"""typescript
import { Component } from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
@Component({
selector: 'app-safe-html',
template: ""
})
export class SafeHtmlComponent {
safeHtml: SafeHtml;
constructor(private sanitizer: DomSanitizer) {
const dangerousHtml = '<p>Hello, world!</p>';
this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(dangerousHtml);
}
}
"""
**Anti-Pattern:** "bypassSecurityTrustHtml" should not be called directly on user input. The data should go through a proper sanitization process first.
### 3.2. Standard: Protect Against CSRF Attacks
**Do This:** Implement Cross-Site Request Forgery (CSRF) protection using tokens or other mechanisms. On the server ensure you are validating "Origin" and "Referer" HTTP headers.
**Don't Do This:** Rely solely on cookies for authentication without CSRF protection.
**Why:** CSRF attacks can allow attackers to perform unauthorized actions on behalf of authenticated users.
**Example:**
Using Angular's "HttpClient" with CSRF tokens:
1. Server-side: Set a CSRF cookie ("XSRF-TOKEN") and expect a header ("X-XSRF-TOKEN") containing the token on POST, PUT, DELETE requests.
2. Angular: "HttpClient" automatically intercepts requests and adds the "X-XSRF-TOKEN" header if the "XSRF-TOKEN" cookie is present.
**Anti-Pattern:** Disabling CSRF protection or using weak CSRF token generation methods.
### 3.3. Standard: Implement Secure Authentication and Authorization
**Do This:** Use secure authentication protocols like OAuth 2.0 or OpenID Connect. Store sensitive data securely using encryption and hashing. Implement role-based access control (RBAC).
**Don't Do This:** Store passwords in plain text or expose sensitive data in the client-side code.
**Why:** Secure authentication and authorization protect user data and prevent unauthorized access to sensitive resources.
**Example:**
Integrating with Auth0:
1. Install "auth0-js":
"""bash
npm install auth0-js
"""
2. Configure Auth0 in your Angular application:
"""typescript
// auth.service.ts
import { Injectable } from '@angular/core';
import * as auth0 from 'auth0-js';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class AuthService {
auth0 = new auth0.WebAuth({
clientID: environment.auth0.clientId,
domain: environment.auth0.domain,
redirectUri: environment.auth0.callbackURL,
responseType: 'token id_token',
scope: 'openid profile email'
});
constructor() {}
login() {
this.auth0.authorize();
}
// ... other authentication methods
}
"""
**Anti-Pattern:** Implementing custom authentication schemes without a thorough understanding of security best practices. Leverage well-established and vetted authentication libraries.
### 3.4 Standard: Keep Dependencies Up-to-Date
**Do This:** Regularly update Angular Material, Angular, and all other dependencies to their latest stable versions to patch security vulnerabilities and take advantage of performance improvements.
**Don't Do This:** Delay updates indefinitely due to fear of breaking changes.
**Why:** Keeping dependencies up-to-date ensures that you are protected against known security vulnerabilities and are taking advantage of the latest performance optimizations. Angular Material frequently releases updates that address reported issues.
**Example:**
Use "ng update" for safe updating:
"""bash
ng update @angular/core @angular/cli
ng update @angular/material
"""
Review breaking changes and follow the Angular Material update guide.
**Anti-Pattern:** Ignoring dependency update notifications from package managers or CI systems. Establish a regular update schedule.
This comprehensive guide provides a robust foundation for creating and maintaining Angular Material applications with high standards for deployment and DevOps. Regularly review and update these standards as the Angular Material ecosystem evolves.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Performance Optimization Standards for Angular Material This document outlines the performance optimization standards for Angular Material applications. These guidelines are designed to improve application speed, responsiveness, and resource usage, specifically within the context of the Angular Material component library. Adhering to these standards will ensure efficient rendering, minimal overhead, and a smooth user experience. ## 1. Change Detection Strategies ### 1.1. OnPush Change Detection **Standard:** Utilize "ChangeDetectionStrategy.OnPush" for components that rely solely on "@Input()" properties for rendering. **Do This:** """typescript import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-display-data', templateUrl: './display-data.component.html', styleUrls: ['./display-data.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class DisplayDataComponent { @Input() data: any; } """ **Don't Do This:** """typescript import { Component, Input } from '@angular/core'; @Component({ selector: 'app-display-data', templateUrl: './display-data.component.html', styleUrls: ['./display-data.component.scss'] }) export class DisplayDataComponent { @Input() data: any; } """ **Why:** "OnPush" change detection tells Angular to only check for changes when the input properties of the component change. This drastically reduces unnecessary change detection cycles, especially in large applications with many components. Without "OnPush", Angular runs change detection on every component irrespective of whether the input properties changed or not. **Anti-Pattern:** Avoid using "OnPush" if the component relies on mutable data or services that change outside the input properties. This requires careful management of data immutability. ### 1.2. Immutability **Standard:** Ensure that data passed to components using "OnPush" is immutable. Use immutable data structures (e.g., from libraries like Immutable.js) or employ techniques like creating new objects/arrays when data changes. **Do This:** """typescript import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-display-items', templateUrl: './display-items.component.html', styleUrls: ['./display-items.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class DisplayItemsComponent { @Input() items: readonly any[]; // Use readonly array } // In the parent component: this.items = [...this.items, newItem]; // Create a new array when updating items """ **Don't Do This:** """typescript import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; @Component({ selector: 'app-display-items', templateUrl: './display-items.component.html', styleUrls: ['./display-items.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush }) export class DisplayItemsComponent { @Input() items: any[]; } // In the parent component: this.items.push(newItem); // Mutating the existing array - BAD! """ **Why:** Mutable data passed to "OnPush" components can cause the component to not update, as Angular will not detect changes if the object reference remains the same. Immutability ensures that updates trigger change detection correctly. **Anti-Pattern:** Directly modifying arrays or objects passed as "@Input()" to components configured with "OnPush". ## 2. Virtualization and Pagination ### 2.1. "cdk-virtual-scroll" for Large Data Lists **Standard:** Employ "cdk-virtual-scroll" (from "@angular/cdk/scrolling") for rendering large lists of data in components like "MatList", "MatTable", or custom data display components. **Do This:** """html <cdk-virtual-scroll-viewport itemSize="50" class="example-viewport"> <mat-list-item *cdkVirtualFor="let item of items">{{item.name}}</mat-list-item> </cdk-virtual-scroll-viewport> """ """typescript import { Component, OnInit } from '@angular/core'; import { Observable, of } from 'rxjs'; @Component({ selector: 'app-virtual-scroll-example', templateUrl: './virtual-scroll-example.component.html', styleUrls: ['./virtual-scroll-example.component.scss'] }) export class VirtualScrollExampleComponent implements OnInit { items: any[] = Array.from({length: 100000}).map((_, i) => ({id: i, name: "Item #${i}"})); ngOnInit(): void { } } """ """css .example-viewport { height: 500px; width: 300px; border: 1px solid black; } mat-list-item { height: 49px; } """ **Don't Do This:** """html <mat-list> <mat-list-item *ngFor="let item of items">{{item.name}}</mat-list-item> </mat-list> """ **Why:** Without virtualization, the browser has to render every single element in the list at once, which can be extremely slow for large datasets. Virtual scrolling only renders the items that are currently visible in the viewport, improving initial load time and scrolling performance. **Anti-Pattern:** Rendering large lists directly using "*ngFor" without virtualization, especially in Angular Material components designed for lists. ### 2.2. Pagination for Large Datasets with "MatPaginator" **Standard:** Implement pagination using "MatPaginator" for tables and data displays that handle large datasets. **Do This:** """html <mat-table [dataSource]="dataSource"> <!-- Columns --> <ng-container matColumnDef="id"> <th mat-header-cell *matHeaderCellDef> ID </th> <td mat-cell *matCellDef="let element"> {{element.id}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </mat-table> <mat-paginator [pageSizeOptions]="[5, 10, 25, 100]" aria-label="Select page of users"></mat-paginator> """ """typescript import { Component, AfterViewInit, ViewChild } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatTableDataSource } from '@angular/material/table'; interface UserData { id: string; name: string; progress: string; fruit: string; } const ELEMENT_DATA: UserData[] = Array.from({length: 100}, (_, k) => createUser(k)); function createUser(id: number): UserData { const name = "User ${id + 1}"; return { id: id.toString(), name: name, progress: Math.round(Math.random() * 100).toString(), fruit: ['apple', 'banana', 'orange', 'grape'][Math.floor(Math.random() * 4)] }; } @Component({ selector: 'app-pagination-example', styleUrls: ['pagination-example.component.scss'], templateUrl: 'pagination-example.component.html', }) export class PaginationExampleComponent implements AfterViewInit { displayedColumns: string[] = ['id', 'name', 'progress', 'fruit']; dataSource = new MatTableDataSource<UserData>(ELEMENT_DATA); @ViewChild(MatPaginator) paginator: MatPaginator; ngAfterViewInit() { this.dataSource.paginator = this.paginator; } } """ **Don't Do This:** """html <mat-table [dataSource]="largeDataSource"> <!-- Rendering all data without pagination --> <ng-container matColumnDef="id"> <th mat-header-cell *matHeaderCellDef> ID </th> <td mat-cell *matCellDef="let element"> {{element.id}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </mat-table> """ **Why:** Pagination divides large datasets into smaller, more manageable chunks, reducing the amount of data loaded and rendered at any given time. It improves initial page load time and allows users to navigate through the data more efficiently. Directly rendering large tables will impact responsiveness and overall speed. **Anti-Pattern:** Loading and rendering all data in a "MatTable" or similar component without pagination when dealing with large datasets. ### 2.3 Server-Side Pagination **Standard**: Implement pagination logic on the server-side wherever possible. The client should only request the set of data needed for the current page and no more. **Do This**: The client requests data: """typescript // In Angular service: getPagedData(pageIndex: number, pageSize: number): Observable<BackendResponse> { const requestParams = new HttpParams() .set('page', pageIndex.toString()) .set('size', pageSize.toString()); return this.http.get<BackendResponse>('/api/data', { params: requestParams }); } // Backend response should include data and total count: interface BackendResponse { data: any[]; total: number; } """ The server returns something like: """json { "data": [ {"id": 1, "name": "Item 1"}, {"id": 2, "name": "Item 2"}, ... ], "total": 1000 } """ **Don't Do This:** Loading all data to the client and performing client-side pagination. **Why**: Server-side pagination significantly reduces the amount of data transferred over the network, leading to faster load times and reduced bandwidth consumption. It's especially important for very large datasets. **Anti-Pattern**: Loading an entire dataset from the server and then paginating on the client side. This wastes bandwidth and processing power. ## 3. Lazy Loading Modules and Components ### 3.1. Route-Based Lazy Loading **Standard:** Implement route-based lazy loading for modules that are not immediately required when the application loads. **Do This:** """typescript // app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: 'feature', loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule) } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } """ **Don't Do This:** """typescript // app.module.ts import { FeatureModule } from './feature/feature.module'; // Eager loading @NgModule({ imports: [ //... FeatureModule // BAD, eager loading! ], //... }) export class AppModule { } """ **Why:** Lazy loading allows you to load modules on demand, reducing the initial bundle size and improving the application's startup time. Users don't have to download code for features they don't immediately need. **Anti-Pattern:** Eagerly loading modules that are not essential for the initial rendering of the application. ### 3.2. Lazy Loading Components with "*ngIf" and "import()" **Standard:** Dynamically import components using "*ngIf" and the "import()" syntax to load them only when needed. **Do This:** """typescript import { Component, ViewContainerRef, ComponentFactoryResolver, ViewChild } from '@angular/core'; @Component({ selector: 'app-lazy-component-wrapper', template: "<ng-container #container></ng-container> <button (click)="loadComponent()">Load Component</button>" }) export class LazyComponentWrapperComponent { @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef; componentRef: any; constructor(private resolver: ComponentFactoryResolver) {} async loadComponent() { this.container.clear(); const { LazyLoadedComponent } = await import('./lazy-loaded.component'); const factory = this.resolver.resolveComponentFactory(LazyLoadedComponent); this.componentRef = this.container.createComponent(factory); } } """ """typescript // lazy-loaded.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-lazy-loaded', template: "<p>This component has been lazily loaded!</p>" }) export class LazyLoadedComponent {} """ **Don't Do This:** Eagerly importing and using components that are only conditionally rendered. **Why:** By loading components only when a specific condition is met (e.g., user interaction, route activation), you reduce the initial bundle size and improve the initial loading speed of the parent component. **Anti-Pattern:** Including conditionally rendered components directly in the template without lazy loading. ## 4. Optimizing Template Rendering ### 4.1. TrackBy Function in "*ngFor" **Standard:** Use a "trackBy" function in "*ngFor" to help Angular efficiently update the DOM when iterating over collections. **Do This:** """html <mat-list> <mat-list-item *ngFor="let item of items; trackBy: trackById">{{item.name}}</mat-list-item> </mat-list> """ """typescript export class MyComponent { items = [{id: 1, name: 'Item 1'}, {id: 2, name: 'Item 2'}]; trackById(index: number, item: any): any { return item.id; } } """ **Don't Do This:** """html <mat-list> <mat-list-item *ngFor="let item of items">{{item.name}}</mat-list-item> </mat-list> """ **Why:** Without "trackBy", Angular re-renders the entire DOM element whenever the array changes, even if the underlying item hasn't changed visibly. The "trackBy" function provides a unique identifier for each item, allowing Angular to only update items that have actually changed, avoiding unnecessary DOM manipulations. **Anti-Pattern:** Omission of "trackBy" in "*ngFor" when iterating over data that might change frequently. ### 4.2. Pure Pipes **Standard:** Use pure pipes for transforming data in templates that doesn't involve side effects. **Do This:** """typescript import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'myPurePipe', pure: true // Explicitly set to true (default) }) export class MyPurePipe implements PipeTransform { transform(value: string): string { return value.toUpperCase(); } } """ """html <p>{{ data | myPurePipe }}</p> """ **Don't Do This:** Using impure pipes for complex or expensive transformations. **Why:** Pure pipes are only re-evaluated when the input value changes, making them efficient for data transformations. Impure pipes are re-evaluated on every change detection cycle, which can significantly impact performance. **Anti-Pattern:** Using impure pipes ("pure: false") for expensive calculations or operations that don't need to be recalculated on every change detection cycle. ### 4.3. Avoid Function Calls in Templates **Standard:** Avoid calling functions directly in templates, especially those that perform calculations or manipulate data. **Do This:** """typescript export class MyComponent { formattedData: string; data = "some data"; ngOnInit() { this.formattedData = this.formatData(this.data); } formatData(data: string): string { return data.toUpperCase(); } } """ """html <p>{{ formattedData }}</p> """ **Don't Do This:** """html <p>{{ formatData(data) }}</p> """ """typescript export class MyComponent { data = "some data"; formatData(data: string): string { return data.toUpperCase(); } } """ **Why:** Function calls within templates are re-executed on every change detection cycle, even if the result is the same. This can lead to significant performance overhead, especially for complex functions. Calculate values in the component class and bind them to the template. **Anti-Pattern:** Calling functions directly in templates, especially when the function performs complex calculations or data transformations. ## 5. Optimizing Angular Material Component Usage ### 5.1. "MatAutocomplete" Optimization **Standard:** When using "MatAutocomplete", debounce input events and minimize the number of results returned. **Do This:** """typescript import { Component, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { Observable } from 'rxjs'; import { startWith, map, debounceTime, distinctUntilChanged } from 'rxjs/operators'; @Component({ selector: 'app-autocomplete-example', templateUrl: './autocomplete-example.component.html', styleUrls: ['./autocomplete-example.component.scss'] }) export class AutocompleteExampleComponent implements OnInit { myControl = new FormControl(); options: string[] = ['One', 'Two', 'Three']; filteredOptions: Observable<string[]>; ngOnInit() { this.filteredOptions = this.myControl.valueChanges.pipe( startWith(''), debounceTime(300), // Debounce for 300ms distinctUntilChanged(), // Only emit when the current value is different than the last map(value => this._filter(value)), ); } private _filter(value: string): string[] { const filterValue = value.toLowerCase(); return this.options.filter(option => option.toLowerCase().includes(filterValue)).slice(0, 10); // Limit to 10 results } } """ """html <mat-form-field> <input type="text" placeholder="Pick one" aria-label="Number" matInput [formControl]="myControl" [matAutocomplete]="auto"> <mat-autocomplete #auto="matAutocomplete"> <mat-option *ngFor="let option of filteredOptions | async" [value]="option"> {{option}} </mat-option> </mat-autocomplete> </mat-form-field> """ **Don't Do This:** """typescript import { Component, OnInit } from '@angular/core'; import { FormControl } from '@angular/forms'; import { Observable } from 'rxjs'; import { startWith, map } from 'rxjs/operators'; @Component({ selector: 'app-autocomplete-example', templateUrl: './autocomplete-example.component.html', styleUrls: ['./autocomplete-example.component.scss'] }) export class AutocompleteExampleComponent implements OnInit { myControl = new FormControl(); options: string[] = ['One', 'Two', 'Three']; filteredOptions: Observable<string[]>; ngOnInit() { this.filteredOptions = this.myControl.valueChanges.pipe( startWith(''), map(value => this._filter(value)), ); } private _filter(value: string): string[] { const filterValue = value.toLowerCase(); return this.options.filter(option => option.toLowerCase().includes(filterValue)); } } """ **Why:** Autocomplete can become slow if the filter function is computationally expensive or if the list of options is very long. Debouncing reduces the frequency of filtering, and limiting the results ensures that the DOM doesn't get overloaded with too many options. **Anti-Pattern:** Performing expensive filtering operations on every keystroke without debouncing or limiting results in "MatAutocomplete". ### 5.2. Minimize DOM Updates in "MatTable" **Standard:** Use "DataSource" with proper change detection strategies and optimized data retrieval to minimize DOM updates when using "MatTable". **Do this:** Use "CollectionViewer" from CDK to efficiently load table data: """typescript import { DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, Observable, of } from 'rxjs'; export interface PeriodicElement { name: string; position: number; weight: number; symbol: string; } const EXAMPLE_DATA: PeriodicElement[] = [ {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'}, {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}, ]; export class ExampleDataSource extends DataSource<PeriodicElement> { private dataStream = new BehaviorSubject<PeriodicElement[]>(EXAMPLE_DATA); connect(): Observable<PeriodicElement[]> { return this.dataStream; } disconnect(): void {} } """ And make sure the data you put into the "dataStream" respects immutability so that components using "OnPush" change detection will update correctly. **Don't do this**: Directly mutating the data array used by "MatTable". **Why**: "MatTable" is highly optimized itself, but improper data handling can introduce performance bottlenecks. Using immutable data with the "DataSource" ensures that "MatTable" only updates the rows that have actually changed. **Anti-Pattern:** Directly mutating the array used as the "MatTable"'s datasource. This forces the table to re-render every row, even if the data hasn't changed visually. ## 6. Asynchronous Operations ### 6.1. Using "async" Pipe **Standard:** Use the "async" pipe in templates to automatically subscribe and unsubscribe from Observables. **Do This:** """typescript import { Component, OnInit } from '@angular/core'; import { Observable, interval } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-async-example', templateUrl: './async-example.component.html', styleUrls: ['./async-example.component.scss'] }) export class AsyncExampleComponent implements OnInit { time$: Observable<Date>; ngOnInit() { this.time$ = interval(1000).pipe(map(() => new Date())); } } """ """html <p>Current Time: {{ time$ | async | date:'mediumTime' }}</p> """ **Don't Do This:** """typescript import { Component, OnInit, OnDestroy } from '@angular/core'; import { Observable, interval } from 'rxjs'; import { map } from 'rxjs/operators'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-async-example', templateUrl: './async-example.component.html', styleUrls: ['./async-example.component.scss'] }) export class AsyncExampleComponent implements OnInit, OnDestroy { time: Date; timeSubscription: Subscription; ngOnInit() { this.timeSubscription = interval(1000).pipe(map(() => new Date())).subscribe(time => this.time = time); } ngOnDestroy() { this.timeSubscription.unsubscribe(); } } """ """html <p>Current Time: {{ time | date:'mediumTime' }}</p> """ **Why:** The "async" pipe handles subscription and unsubscription automatically, preventing memory leaks and simplifying component logic. Manual subscriptions require careful management to avoid memory leaks. **Anti-Pattern:** Subscribing to Observables manually in components and forgetting to unsubscribe in "ngOnDestroy". ### 6.2. Debounce Input Events **Standard**: Use "debounceTime" to reduce the number of events that trigger expensive operations. **Why**: Many interactive features are sensitive to user input. Debouncing these events prevents code from running needlessly. **Do This**: """typescript import { fromEvent } from 'rxjs'; import { debounceTime, map } from 'rxjs/operators'; const searchBox = document.getElementById('search-box'); fromEvent(searchBox, 'keyup').pipe( map((i: any) => i.currentTarget.value), debounceTime(500) ).subscribe((value) => { // Make API call here console.log(value); }); """ **Don't Do This**: Executing code on every single event without debouncing. **Anti-Pattern**: Performing API calls or complex calculations on every keystroke. ## 7. Tree Shaking Optimization ### 7.1. Import Only Necessary Modules **Standard:** Import only the specific Angular Material modules and components that are used in a particular module or component. **Do This:** """typescript import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; @NgModule({ imports: [ MatButtonModule, MatInputModule ], // ... }) export class MyModule { } """ **Don't Do This:** """typescript import { MatModule } from '@angular/material'; // There is no MatModule @NgModule({ imports: [ MatModule // This is incorrect; import specific modules ], // ... }) export class MyModule { } """ **Why:** Importing only the modules you need allows the Angular compiler to tree-shake unused code, reducing the final bundle size. Importing entire libraries (if they're even structured that way) prevents efficient optimization. **Anti-Pattern:** Importing entire Angular Material library or large, unnecessary modules. ## 8. Profiling & Auditing ### 8.1. Using Angular DevTools **Standard:** Regularly use the Angular DevTools extension (Chrome, Edge) to profile the application, identify performance bottlenecks, and analyze change detection cycles. **Why:** Angular DevTools provides insights into component rendering times, change detection behavior, and other performance-related metrics. This helps identify areas that require optimization. **Anti-Pattern:** Developing without regularly profiling the application using Angular DevTools or similar tools. ### 8.2. Auditing with Lighthouse **Standard:** Utilize Google Lighthouse to audit the application for performance, accessibility, and best practices. **Why:** Lighthouse provides a comprehensive audit of the application, highlighting potential performance issues and areas for improvement. **Anti-Pattern:** Deploying applications without performing a Lighthouse audit.
# Core Architecture Standards for Angular Material This document outlines the core architectural coding standards for developing Angular Material applications. Its goal is to provide clear, actionable guidance for developers, ensuring consistency, maintainability, performance, and security in Angular Material projects. This is written assuming the utilization of the latest Angular Material version available at the time of creation. ## 1. Project Structure and Organization A well-defined project structure is crucial for the scalability and maintainability of Angular Material applications. ### 1.1 Standard: Feature-Based Modules **Do This:** Organize your application into feature-based modules. Each module should encapsulate a specific functionality or feature of your application. **Don't Do This:** Lump all components, services, and modules into a single, massive "app.module.ts". **Why:** Feature-based modules promote separation of concerns, improve code reusability, and simplify testing. They also enable lazy loading, which can significantly improve application startup time. **Example:** """typescript // src/app/dashboard/dashboard.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { DashboardComponent } from './dashboard.component'; import { MatCardModule } from '@angular/material/card'; import { MatButtonModule } from '@angular/material/button'; @NgModule({ declarations: [DashboardComponent], imports: [ CommonModule, MatCardModule, MatButtonModule ], exports: [DashboardComponent] }) export class DashboardModule { } // src/app/app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { DashboardModule } from './dashboard/dashboard.module'; @NgModule({ declarations: [ AppComponent, ], imports: [ BrowserModule, BrowserAnimationsModule, // Required for Angular Material animations DashboardModule // Import the feature module ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } """ **Anti-Pattern:** Creating a large, monolithic "app.module.ts" that contains all components and services leads to tight coupling and makes the application difficult to maintain. ### 1.2 Standard: Core and Shared Modules **Do This:** Create "CoreModule" and "SharedModule" to encapsulate application-wide services and reusable components/directives/pipes, respectively. **Don't Do This:** Import services or components directly into multiple feature modules without using "CoreModule" or "SharedModule". **Why:** "CoreModule" ensures that singleton services are only instantiated once. "SharedModule" prevents code duplication and promotes consistency across the application. **Example:** """typescript // src/app/core/core.module.ts import { NgModule, Optional, SkipSelf } from '@angular/core'; import { ApiService } from './api.service'; @NgModule({ providers: [ ApiService ] }) export class CoreModule { constructor(@Optional() @SkipSelf() parentModule: CoreModule) { if (parentModule) { throw new Error('CoreModule is already loaded. Import it in the AppModule only.'); } } } // src/app/shared/shared.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatInputModule } from '@angular/material/input'; import { MyCustomPipe } from './my-custom.pipe'; @NgModule({ declarations: [MyCustomPipe], imports: [ CommonModule, MatButtonModule, MatInputModule ], exports: [ CommonModule, // Re-export CommonModule for use in feature modules MatButtonModule, MatInputModule, MyCustomPipe ] }) export class SharedModule { } // src/app/app.module.ts import { CoreModule } from './core/core.module'; import { SharedModule } from './shared/shared.module'; @NgModule({ imports: [ CoreModule, SharedModule // Import SharedModule ], }) export class AppModule { } //src/app/dashboard/dashboard.module.ts import { SharedModule } from '../shared/shared.module'; @NgModule({ imports: [ SharedModule // Import SharedModule instead of MatButtonModule directly ], }) export class DashboardModule { } """ **Anti-Pattern:** Importing "CoreModule" into feature modules leads to multiple instances of singleton services. Forgetting to re-export "CommonModule" from "SharedModule" can cause unexpected errors in feature modules. ### 1.3 Standard: Folder Structure **Do This:** Employ a consistent folder structure: """ src/ app/ core/ // Singleton services shared/ // Reusable components, directives, pipes feature1/ // Feature module 1 components/ services/ feature1.module.ts feature2/ // Feature module 2 app.component.ts app.module.ts app-routing.module.ts assets/ // Static assets (images, fonts, etc.) environments/ // Environment-specific configurations """ **Don't Do This:** Randomly scatter files across the "src/app" directory. Place environment-specific configurations directly in the "app" folder. **Why:** A clear folder structure improves code discoverability and simplifies navigation within the project. **Example:** (See above file structure) **Anti-Pattern:** Storing components directly in the feature module's folder instead of creating a "components" subfolder. ## 2. Component Design Components are the building blocks of Angular Material applications. ### 2.1 Standard: Smart vs. Dumb Components **Do This:** Distinguish between smart (container) and dumb (presentational) components. Smart components handle data fetching and business logic, while dumb components focus on rendering data and emitting events. **Don't Do This:** Mix data fetching, business logic, and UI rendering within a single component. **Why:** This pattern promotes separation of concerns, improves testability, and enhances code reusability. **Example:** """typescript // src/app/dashboard/dashboard.component.ts (Smart Component) import { Component, OnInit } from '@angular/core'; import { DataService } from '../core/data.service'; @Component({ selector: 'app-dashboard', template: "<app-dashboard-list [items]="data"></app-dashboard-list>" }) export class DashboardComponent implements OnInit { data: any[]; constructor(private dataService: DataService) { } ngOnInit(): void { this.dataService.getData().subscribe(data => this.data = data); } } // src/app/dashboard/components/dashboard-list.component.ts (Dumb Component) import { Component, Input, Output, EventEmitter } from '@angular/core'; @Component({ selector: 'app-dashboard-list', template: " <mat-card *ngFor="let item of items"> {{ item.name }} <button mat-button (click)="itemClicked.emit(item)">View</button> </mat-card> " }) export class DashboardListComponent { @Input() items: any[]; @Output() itemClicked = new EventEmitter<any>(); } """ **Anti-Pattern:** Performing API calls directly within a presentational component. Failing to use "@Input" and "@Output" for data flow between components. ### 2.2 Standard: Angular Material Component Usage **Do This:** Utilize Angular Material components whenever possible for a consistent and accessible UI. Customize components using Angular Material's theming system. **Don't Do This:** Reinventing the wheel by creating custom UI elements that duplicate the functionality of existing Angular Material components. Hardcoding styles directly in component templates. **Why:** Angular Material provides pre-built, accessible, and well-tested UI components. Theming ensures a consistent look and feel across the application. **Example:** """typescript // src/app/my-component/my-component.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-my-component', template: " <mat-form-field appearance="outline"> <mat-label>Enter your name</mat-label> <input matInput placeholder="Placeholder"> <mat-hint>Here's the validation tip</mat-hint> </mat-form-field> <button mat-raised-button color="primary">Save</button> " }) export class MyComponent { } """ **Anti-Pattern:** Using standard HTML input elements instead of "mat-form-field" and "matInput". Inline styling instead of using CSS classes and Angular Material themes. ### 2.3 Standard: Immutability in Components **Do This:** Treat "@Input" properties as immutable within dumb components. If modifications are needed, create a copy before processing. **Don't Do This:** Directly modify "@Input" properties, as this can lead to unexpected side effects and make debugging difficult. **Why:** Immutability makes components more predictable and easier to reason about. It also simplifies change detection. **Example:** """typescript // src/app/my-component/my-component.component.ts import { Component, Input, OnChanges } from '@angular/core'; @Component({ selector: 'app-my-component', template: " <p>{{ processedName }}</p> " }) export class MyComponent implements OnChanges{ @Input() name: string; processedName: string; ngOnChanges(): void { // Create a copy to avoid modifying the original input this.processedName = this.name ? this.name.toUpperCase() : ''; } } """ **Anti-Pattern:** Directly modifying the "name" property within the component, potentially affecting the parent component's data. ## 3. Service Layer Services are responsible for handling business logic, data access, and communication with external APIs. ### 3.1 Standard: Separation of Concerns in Services **Do This:** Create separate services for different functionalities. For example, have one service for user authentication, another for data fetching, and another for managing application state. **Don't Do This:** Combining multiple unrelated responsibilities within a single service. **Why:** Separation of concerns improves code maintainability, testability, and reusability. **Example:** """typescript // src/app/core/auth.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { BehaviorSubject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class AuthService { private isLoggedInSubject = new BehaviorSubject<boolean>(false); isLoggedIn$: Observable<boolean> = this.isLoggedInSubject.asObservable(); constructor(private http: HttpClient) { // Check local storage or API to determine initial login state. } login(credentials: any): Observable<any> { return this.http.post('/api/login', credentials).pipe( tap(() => this.isLoggedInSubject.next(true)) ); } logout(): void { // Perform logout API call or clear local storage this.isLoggedInSubject.next(false); } } // src/app/core/data.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; @Injectable({ providedIn: 'root' }) export class DataService { constructor(private http: HttpClient) { } getData(): Observable<any[]> { return this.http.get<any[]>('/api/data'); } } """ **Anti-Pattern:** Placing authentication logic directly within a data service. ### 3.2 Standard: Dependency Injection **Do This:** Utilize Angular's dependency injection (DI) system to inject dependencies into services and components. **Don't Do This:** Manually creating instances of dependencies using the "new" keyword. **Why:** DI promotes loose coupling, improves testability, and simplifies configuration. **Example:** """typescript // src/app/my-component/my-component.component.ts import { Component } from '@angular/core'; import { DataService } from '../core/data.service'; @Component({ selector: 'app-my-component', template: " <p>{{ data }}</p> " }) export class MyComponent { data: any[]; constructor(private dataService: DataService) { this.dataService.getData().subscribe(data => this.data = data); } } """ **Anti-Pattern:** Creating a new instance of "DataService" within the component's constructor using "new DataService()". This makes the component tightly coupled to a specific implementation. ### 3.3 Standard: Observable and RxJS **Do This:** Use Observables from RxJS for handling asynchronous operations, such as API calls and event streams. Use the pipeable operators from RxJS to transform data streams. **Don't Do This:** Relying heavily on Promises or callbacks for asynchronous operations, especially when dealing with complex data streams. Not unsubscribing from Observables, leading to memory leaks. **Why:** Observables provide a powerful and flexible way to manage asynchronous data streams. RxJS provides a rich set of operators for transforming, filtering, and combining these streams. **Example:** """typescript // src/app/core/data.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { map, catchError } from 'rxjs/operators'; import { of } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class DataService { constructor(private http: HttpClient) { } getData(): Observable<any[]> { return this.http.get<any[]>('/api/data').pipe( map(data => { // Process the data return data.map(item => ({ ...item, processed: true })); }), catchError(error => { console.error('Error fetching data:', error); return of([]); // Return an empty array in case of error }) ); } } """ **Anti-Pattern:** Not handling errors in RxJS streams, leading to unhandled exceptions. Not using the "pipe" operator for chaining RxJS operators, resulting in less readable code. Failing to unsubscribe from observables in components leading to memory leaks (use "async" pipe in template if possible). ## 4. State Management Effective state management is essential for complex Angular Material applications. ### 4.1 Standard: Centralized State Management (NgRx or Akita) **Do This:** Use a centralized state management library, such as NgRx or Akita, for managing application state in larger applications. **Don't Do This:** Relying solely on "@Input" and "@Output" bindings for managing state in complex scenarios, leading to prop drilling and difficulty in tracking state changes. **Why:** Centralized state management provides a predictable and maintainable way to manage application state. NgRx provides a Redux-inspired pattern, while Akita offers a simpler, entity-based approach. **Example (NgRx):** (This is a simplified example; a full NgRx implementation requires reducers, actions, effects.) """typescript // src/app/store/data.actions.ts import { createAction, props } from '@ngrx/store'; export const loadData = createAction('[Data] Load Data'); export const loadDataSuccess = createAction('[Data] Load Data Success', props<{ data: any[] }>()); // src/app/store/data.reducer.ts import { createReducer, on } from '@ngrx/store'; import { loadDataSuccess } from './data.actions'; export interface DataState { data: any[]; loading: boolean; } export const initialState: DataState = { data: [], loading: false }; export const dataReducer = createReducer( initialState, on(loadDataSuccess, (state, { data }) => ({ ...state, data: data, loading: false })) ); // src/app/dashboard/dashboard.component.ts import { Component, OnInit } from '@angular/core'; import { Store, select } from '@ngrx/store'; import { loadData } from '../store/data.actions'; import { Observable } from 'rxjs'; interface AppState { data: DataState; } @Component({ selector: 'app-dashboard', template: " <div *ngIf="!(data$ | async)?.loading"> <mat-card *ngFor="let item of (data$ | async)?.data"> {{ item.name }} </mat-card> </div>" }) export class DashboardComponent implements OnInit { data$: Observable<DataState>; constructor(private store: Store<AppState>) { this.data$ = store.pipe(select('data')); } ngOnInit(): void { this.store.dispatch(loadData()); } } """ **Anti-Pattern:** Mutating state directly within components (without using reducers). Dispatching actions directly from components without using selectors to retrieve data. Overusing state management for simple scenarios (consider component-level state for local UI concerns). ### 4.2 Standard: Component-Level State **Do This:** Use component-level state (e.g., using class properties or the "useState" hook in functional components) for managing UI-specific state that doesn't need to be shared across the application. **Don't Do This:** Storing UI-specific state in the global state management store, which can lead to unnecessary re-renders and performance issues. **Why:** Component-level state is simpler to manage than global state and can improve performance by reducing the scope of change detection. **Example:** """typescript // src/app/my-component/my-component.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-my-component', template: " <button mat-raised-button (click)="toggleVisibility()">Toggle</button> <div *ngIf="isVisible">Content</div> " }) export class MyComponent { isVisible = false; toggleVisibility(): void { this.isVisible = !this.isVisible; } } """ **Anti-Pattern:** Storing the "isVisible" flag in the global state management store when it's only relevant to this specific component. ## 5. Angular Material Theming Consistent application of Angular Material's thematic capabilities. ### 5.1 Standard: Custom Themes **Do This:** Create custom Angular Material themes using SCSS to define the application's color palette, typography, and density. **Don't Do This:** Use the default Angular Material theme without any customization, resulting in a generic and unbranded look. Hardcoding styles directly in component templates. **Why:** Custom themes allow you to tailor Angular Material to your application's unique brand and design requirements. **Example:** """scss // src/styles.scss @import '~@angular/material/theming'; @include mat-core(); $primary: mat-palette($mat-indigo); $accent: mat-palette($mat-pink, A200, A100, A400); $warn: mat-palette($mat-red); $theme: mat-light-theme($primary, $accent, $warn); @include angular-material-theme($theme); """ **Anti-Pattern:** Not using SCSS variables for defining theme colors, making it difficult to change the application's theme later. ### 5.2 Standard: Theme Application **Do This:** Apply the custom theme to the entire application by including the "angular-material-theme" mixin in the global styles file (e.g., "styles.scss"). **Don't Do This:** Applying the theme only to specific components, resulting in inconsistent styling across the application. **Why:** Applying the theme globally ensures a consistent look and feel throughout the application. **Example:** (see above) **Anti-Pattern:** Importing the "angular-material-theme" mixin in multiple component style files, leading to duplicate styles and potential conflicts. ### 5.3 Standard: Density **Do This:** Choose an appropriate density setting for Angular Material components based on the target device and user preferences. Use the "comfortable" or "compact" density settings for touch-based devices or users who prefer a more compact UI. **Don't Do This:** Using the default density setting without considering the target audience or device. **Why:** Adjusting the density can improve the user experience on different devices and for users with different preferences. **Example:** """scss // src/styles.scss @import '~@angular/material/theming'; @include mat-core(); // Includes default density. $primary: mat-palette($mat-indigo); $accent: mat-palette($mat-pink, A200, A100, A400); $warn: mat-palette($mat-red); $theme: mat-light-theme($primary, $accent, $warn); @include angular-material-theme($theme); //For compact density $custom-density-config: mat-density-config(-3, -2, -1, 0, 1); @include mat-core($custom-density-config); """ **Anti-Pattern:** Not providing a way for users to customize the density setting, resulting in a less accessible application. This comprehensive document covers core architectural standards for Angular Material development. Adhering to these guidelines will result in more maintainable, performant, and consistent applications. Remember to consult the official Angular Material documentation for the most up-to-date information and best practices.
# API Integration Standards for Angular Material This document outlines the coding standards for integrating Angular Material components with backend services and external APIs. It aims to provide clear guidance, improve code quality, maintainability, performance, and security across Angular Material projects. ## 1. Architectural Principles ### 1.1. Separation of Concerns **Do This:** * Separate the UI layer (Angular Material components) from the data access layer (API services). The components should focus on presentation and user interaction, while services handle data retrieval and manipulation. **Don't Do This:** * Embed API calls directly within Angular Material components. This makes the components harder to test, reuse, and maintain. **Why:** Promotes modularity, testability, and easier maintenance by isolating responsibilities. **Example:** """typescript // api.service.ts (Data Access Layer) import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class ApiService { private apiUrl = '/api/items'; //Adjust the endpoint accordingly constructor(private http: HttpClient) { } getItems(): Observable<Item[]> { return this.http.get<Item[]>(this.apiUrl); } addItem(item: Item): Observable<Item> { return this.http.post<Item>(this.apiUrl, item); } updateItem(id: number, item: Item): Observable<Item> { return this.http.put<Item>("${this.apiUrl}/${id}", item); } deleteItem(id: number): Observable<any> { return this.http.delete("${this.apiUrl}/${id}"); } } // item.model.ts export interface Item { id: number; name: string; description: string; } """ """typescript // my-component.component.ts (UI Layer) import { Component, OnInit } from '@angular/core'; import { ApiService } from './api.service'; import { Item } from './item.model'; @Component({ selector: 'app-my-component', templateUrl: './my-component.component.html', styleUrls: ['./my-component.component.css'] }) export class MyComponentComponent implements OnInit { items: Item[] = []; constructor(private apiService: ApiService) { } ngOnInit(): void { this.loadItems(); } loadItems(): void { this.apiService.getItems().subscribe( (items) => { this.items = items; }, (error) => { console.error('Error loading items:', error); } ); } addItem(item: Item): void { this.apiService.addItem(item).subscribe( (newItem) => { this.items.push(newItem); }, (error) => { console.error('Error adding item:', error); } ); } deleteItem(id: number): void { this.apiService.deleteItem(id).subscribe(() => { this.items = this.items.filter(item => item.id !== id); }, (error) => { console.error('Error deleting item:', error); }); } //Add Item dialog handler and other UI logic can be added here } """ ### 1.2. Single Source of Truth **Do This:** * Maintain a single source of truth for data. Use a centralized data store (e.g., NgRx, Akita, or a simple service-based state management) when dealing with complex application state. **Don't Do This:** * Duplicate data across multiple components or services, leading to inconsistencies and synchronization problems. **Why:** Ensures data consistency and reduces the complexity of managing application state. **Example (Service based Singleton):** """typescript // data.service.ts import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { ApiService } from './api.service'; import { Item } from './item.model'; @Injectable({ providedIn: 'root' }) export class DataService { private _items$ = new BehaviorSubject<Item[]>([]); public items$: Observable<Item[]> = this._items$.asObservable(); private isLoading$ = new BehaviorSubject<boolean>(false); public isLoadingObservable$ = this.isLoading$.asObservable(); constructor(private apiService: ApiService) { this.loadInitialData(); } loadInitialData(): void { this.isLoading$.next(true); this.apiService.getItems().subscribe( (items) => { this._items$.next(items); this.isLoading$.next(false); }, (error) => { console.error('Error loading initial data:', error); this.isLoading$.next(false); } ); } addItem(item: Item): void { // Optimistically update the UI const currentItems = this._items$.getValue(); this._items$.next([...currentItems, item]); this.apiService.addItem(item).subscribe( (newItem) => { // Update the state with the new item's ID from the server const updatedItems = currentItems.map(existingItem => existingItem === item ? newItem : existingItem ); this._items$.next(updatedItems); }, (error) => { console.error('Error adding item:', error); this._items$.next(currentItems) // Revert in case of error } ); } deleteItem(id: number): void { const currentItems = this._items$.getValue(); const updatedItems = currentItems.filter(item => item.id !== id); this._items$.next(updatedItems); //Optimistically update the UI this.apiService.deleteItem(id).subscribe(() => {}, (error) => { console.error('Error deleting item:', error); this._items$.next(currentItems); // Revert optimistic update }); } } """ ### 1.3. Data Transformation **Do This:** * Transform API responses into the format expected by Angular Material components in the service layer. Utilizing RxJS "map" operator is highly recommended for this purpose. **Don't Do This:** * Force Angular Material components to handle raw API data, which may not be in the correct format. **Why:** Decouples components from specific API formats, enabling easier component reuse and API changes. **Example:** """typescript //In api.service.ts import { map } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class ApiService { private apiUrl = '/api/items'; constructor(private http: HttpClient) {} getItems(): Observable<Item[]> { return this.http.get<any[]>(this.apiUrl).pipe( map(data => { return data.map(item => ({ id: item.item_id, //API field name mapping name: item.item_name, description: item.item_description })); }) ); } } """ ## 2. API Service Implementation ### 2.1. Dependency Injection **Do This:** * Inject API services into components and other services using Angular's dependency injection. **Don't Do This:** * Create service instances manually using "new MyService()". **Why:** Facilitates testability, reusability, and maintainability by decoupling components from service implementations. **Example:** """typescript import { Component, OnInit } from '@angular/core'; import { ApiService } from './api.service'; import { Item } from './item.model'; @Component({ selector: 'app-items-list', template: " <mat-list> <mat-list-item *ngFor="let item of items"> {{ item.name }} - {{item.description}} </mat-list-item> </mat-list> ", styleUrls: ['./items-list.component.css'] }) export class ItemsListComponent implements OnInit { items: Item[] = []; constructor(private apiService: ApiService) { } ngOnInit(): void { this.apiService.getItems().subscribe(items => this.items = items); } } """ ### 2.2. RxJS Observables **Do This:** * Use RxJS Observables for handling asynchronous API requests. Leverage operators like "map", "catchError", "tap", "switchMap", and "combineLatest" to transform and manage data streams. Consider using "async" pipe in the templates. **Don't Do This:** * Use Promises directly or rely on callbacks for handling API results. **Why:** Provides a powerful and flexible way to handle asynchronous operations, enabling better error handling, data transformation, and cancellation. **Example:** """typescript import { Injectable } from '@angular/core'; import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Observable, throwError } from 'rxjs'; import { catchError, map } from 'rxjs/operators'; import { Item } from './item.model'; @Injectable({ providedIn: 'root' }) export class ApiService { private apiUrl = '/api/items'; constructor(private http: HttpClient) { } getItems(): Observable<Item[]> { return this.http.get<any[]>(this.apiUrl).pipe( map(items => items.map(item => ({ id: item.item_id, name: item.item_name, description: item.item_description }))), catchError(this.handleError) ); } private handleError(error: HttpErrorResponse) { if (error.status === 0) { console.error('An error occurred:', error.error); } else { console.error( "Backend returned code ${error.status}, body was: ", error.error); } return throwError(() => new Error('Something bad happened; please try again later.')); } } """ In Component Template: """html <div *ngIf="(items$ | async) as items; else loading"> <mat-list> <mat-list-item *ngFor="let item of items"> {{ item.name }} - {{item.description}} </mat-list-item> </mat-list> </div> <ng-template #loading> <mat-spinner></mat-spinner> </ng-template> """ Using "async" pipe allows Angular to automatically subscribe and unsubscribe from the Observable. ### 2.3. Error Handling **Do This:** * Implement robust error handling in API services using RxJS "catchError" operator. Log errors and display user-friendly messages. **Don't Do This:** * Ignore API errors or let them propagate unhandled to the UI. **Why:** Improves the user experience and helps in debugging and diagnosing issues. **Example:** (See "handleError" function in example above) ### 2.4. Data Validation **Do This:** * Validate data received from APIs, both on the client-side and server-side (if possible). Use Angular's reactive forms and custom validators. **Don't Do This:** * Trust API data implicitly without validation. **Why:** Prevents data corruption, improves security, and ensures data integrity. **Example:** """typescript //In component.ts using Reactive Forms import { Component, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ApiService } from './api.service'; import { Item } from './item.model'; @Component({ selector: 'app-item-form', templateUrl: './item-form.component.html', styleUrls: ['./item-form.component.css'] }) export class ItemFormComponent implements OnInit { itemForm: FormGroup; constructor(private fb: FormBuilder, private apiService: ApiService) { this.itemForm = this.fb.group({ name: ['', [Validators.required, Validators.minLength(3)]], description: ['', Validators.maxLength(100)] }); } ngOnInit(): void {} onSubmit(): void { if (this.itemForm.valid) { this.apiService.addItem(this.itemForm.value).subscribe( (newItem) => { console.log('Item added:', newItem); this.itemForm.reset(); // Clear the form on successful submission }, (error) => { console.error('Error adding item:', error); } ); } else { console.log('Form is invalid. Please check the fields.'); } } get name() { return this.itemForm.get('name'); } get description() { return this.itemForm.get('description'); } } """ """html <!-- in item-form.component.html --> <form [formGroup]="itemForm" (ngSubmit)="onSubmit()"> <mat-form-field appearance="outline"> <mat-label>Name</mat-label> <input matInput formControlName="name" required> <mat-error *ngIf="name?.invalid">{{getErrorMessage(name)}}</mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Description</mat-label> <textarea matInput formControlName="description"></textarea> <mat-error *ngIf="description?.invalid">{{getErrorMessage(description)}}</mat-error> </mat-form-field> <button mat-raised-button color="primary" type="submit" [disabled]="itemForm.invalid">Add Item</button> </form> """ ### 2.5. Caching API Responses **Do This:** * Implement caching mechanisms (e.g., using "localStorage", "sessionStorage", or a dedicated caching library) to reduce redundant API calls for frequently accessed or static data. The "shareReplay" operator can be used to cache in mememory for a given time. **Don't Do This:** * Cache sensitive or frequently changing data without proper invalidation strategies. **Why:** Improves application performance and responsiveness by minimizing network requests. **Example (using "shareReplay"):** """typescript //api.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { shareReplay } from 'rxjs/operators'; @Injectable({ providedIn: 'root' }) export class ApiService { private apiUrl = '/api/lookups'; //Adjust the endpoint accordingly private cachedLookups$: Observable<any>; constructor(private http: HttpClient) { } getLookupData(): Observable<any> { if (!this.cachedLookups$) { this.cachedLookups$ = this.http.get<any>(this.apiUrl).pipe( shareReplay(1) // Cache the last emitted value ); } return this.cachedLookups$; } } """ ## 3. Angular Material Component Integration ### 3.1. Data Binding **Do This:** * Use Angular's data binding features (e.g., "{{ }}", "[]", "()", "ngModel", "*ngFor") to connect Angular Material components with data from API responses. **Don't Do This:** * Manipulate DOM elements directly to display or update data. **Why:** Enables declarative UI development, improves code readability, and simplifies data synchronization. **Example:** """html <mat-list> <mat-list-item *ngFor="let item of items"> <mat-icon matListItemIcon>folder</mat-icon> <div matListItemTitle>{{ item.name }}</div> <div matListItemLine>{{ item.description }}</div> <mat-divider></mat-divider> </mat-list-item> </mat-list> """ ### 3.2. Form Integration **Do This:** * Integrate Angular Material form controls with Angular's reactive forms or template-driven forms to handle user input and submit data to APIs. **Don't Do This:** * Implement custom form handling logic that bypasses Angular's form features. **Why:** Simplifies form validation, data binding, and submission, while leveraging Angular Material's UI components. (See data validation example above) ### 3.3. Material Data Table **Do This:** * Use "<mat-table>" to display tabular data from APIs. Implement sorting, filtering, and pagination using the "MatTableDataSource". **Don't Do This:** * Create custom table implementations using basic HTML elements. **Why:** Provides a standardized and feature-rich table component with built-in support for common data table operations. **Example:** """typescript // item-table.component.ts import { Component, OnInit, ViewChild } from '@angular/core'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { ApiService } from '../api.service'; import { Item } from '../item.model'; @Component({ selector: 'app-item-table', templateUrl: './item-table.component.html', styleUrls: ['./item-table.component.css'] }) export class ItemTableComponent implements OnInit { displayedColumns: string[] = ['id', 'name', 'description']; dataSource: MatTableDataSource<Item>; items: Item[] = []; @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; constructor(private apiService: ApiService) { this.dataSource = new MatTableDataSource(this.items); } ngOnInit() { this.apiService.getItems().subscribe(items => { this.dataSource.data = items; this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } ); } applyFilter(event: Event) { const filterValue = (event.target as HTMLInputElement).value; this.dataSource.filter = filterValue.trim().toLowerCase(); if (this.dataSource.paginator) { this.dataSource.paginator.firstPage(); } } } """ """html <!-- item-table.component.htm --> <mat-form-field appearance="fill"> <mat-label>Filter</mat-label> <input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia" #input> </mat-form-field> <div class="mat-elevation-z8"> <table mat-table [dataSource]="dataSource" matSort> <!-- ID Column --> <ng-container matColumnDef="id"> <th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th> <td mat-cell *matCellDef="let row"> {{row.id}} </td> </ng-container> <!-- Name Column --> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th> <td mat-cell *matCellDef="let row"> {{row.name}} </td> </ng-container> <!-- Description Column --> <ng-container matColumnDef="description"> <th mat-header-cell *matHeaderCellDef mat-sort-header> Description </th> <td mat-cell *matCellDef="let row"> {{row.description}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <!-- Row shown when there is no matching data. --> <tr class="mat-row" *matNoDataRow> <td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td> </tr> </table> <mat-paginator [pageSizeOptions]="[5, 10, 25, 100]" aria-label="Select page of users"></mat-paginator> </div> """ ## 4. Security Considerations ### 4.1. Authentication and Authorization **Do This:** * Implement proper authentication and authorization mechanisms to protect APIs from unauthorized access. Use Angular's "HttpClient" interceptors to add authentication headers to API requests. **Don't Do This:** * Store sensitive credentials (e.g., API keys, passwords) directly in the client-side code. **Why:** Protects sensitive data and prevents unauthorized access to API resources. ### 4.2. Input Sanitization **Do This:** * Sanitize user inputs before sending them to APIs to prevent cross-site scripting (XSS) and other injection attacks. This should be done both on the client and server side. **Don't Do This:** * Trust user inputs implicitly without sanitization. **Why:** Improves the security and integrity of the application by preventing malicious code injection. ### 4.3. Cross-Origin Resource Sharing (CORS) **Do This:** * Configure CORS settings on the server-side to allow requests from the Angular application's origin. **Don't Do This:** * Disable CORS completely, which can expose the API to security vulnerabilities. **Why:** Enables secure communication between the Angular application and the API server by restricting cross-origin requests. ### 4.4. HTTPS **Do This:** * Always use HTTPS for API communication to encrypt data in transit. **Don't Do This:** * Use HTTP for sensitive data transmission. **Why:** Protects data from eavesdropping and man-in-the-middle attacks. ## 5. Performance Optimization ### 5.1. Lazy Loading **Do This:** * Implement lazy loading for modules and components that are not immediately required on application startup. **Don't Do This:** * Load all modules and components upfront, which can slow down the initial load time. **Why:** Reduces the initial load time and improves the application's responsiveness. ### 5.2. Change Detection Strategy **Do This:** * Use the "OnPush" change detection strategy for components that rely on immutable data or explicit input changes. **Don't Do This:** * Rely on the default change detection strategy for all components, which can lead to unnecessary change detection cycles. **Why:** Improves the application's performance by reducing the number of change detection cycles. """typescript import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; import { Item } from './item.model'; @Component({ selector: 'app-item-display', template: " <p>{{ item.name }} - {{ item.description }}</p> ", changeDetection: ChangeDetectionStrategy.OnPush }) export class ItemDisplayComponent { @Input() item: Item; } """ ### 5.3. Pagination **Do This:** * Implement pagination for APIs that return large datasets to reduce the amount of data transferred and rendered at once. **Don't Do This:** * Load and render the entire dataset in one go. **Why:** Improves the application's performance and responsiveness by reducing the amount of data processed at once. (See Material Data Table Example above which implements Pagination.) ## 6. Testing ### 6.1. Unit Testing **Do This:** * Write unit tests for API services and components using mocking and stubbing to isolate the code under test. **Don't Do This:** * Skip unit testing or rely solely on end-to-end tests. **Why:** Verifies the functionality of individual units of code and improves code quality. ### 6.2. End-to-End Testing **Do This:** * Write end-to-end tests to verify the integration between Angular Material components and APIs. **Don't Do This:** * Rely solely on manual testing. **Why:** Verifies the overall functionality of the application and ensures that components and APIs work together correctly. ## 7. Code Style ### 7.1. Naming Conventions **Do This:** * Use descriptive and consistent naming conventions for API services, methods, and variables. * Example: "ApiService", "getItems()", "itemDetails" **Don't Do This:** * Use ambiguous or inconsistent names. **Why:** Improves code readability and maintainability. ### 7.2. Formatting **Do This:** * Follow a consistent code formatting style using tools like Prettier or ESLint. **Don't Do This:** * Use inconsistent or unconventional formatting. **Why:** Improves code readability and reduces the risk of errors. ### 7.3. Comments **Do This:** * Add comments to explain complex logic, API interactions, and non-obvious code. **Don't Do This:** * Over-comment or write redundant comments. **Why:** Improves code understanding and maintainability. By adhering to these API integration standards, Angular Material projects can achieve improved code quality, maintainability, performance, and security. This document should serve as the single source of truth for how API integration should be handled within Angular Material applications.
# Code Style and Conventions Standards for Angular Material This document outlines the coding style and conventions to be followed when developing applications using Angular Material. Adhering to these standards will ensure consistency, readability, maintainability, and performance within Angular Material projects. These guidelines are designed to complement general Angular style guides, focusing specifically on aspects relevant to Angular Material. This document leverages the latest Angular Material practices. ## 1. General Formatting ### 1.1. File Structure * **Do This:** Organize components, services, and modules into meaningful directories. Feature-based modules should reside in their own directory. * **Don't Do This:** Lump all components into a single directory or create overly complex directory structures. **Why:** Clear file structure enhances discoverability and maintainability. Feature modules encapsulate related functionality, promoting modularity. **Example:** """ src/app/ ├── feature-a/ │ ├── feature-a.module.ts │ ├── feature-a.component.ts │ ├── feature-a.component.html │ ├── feature-a.component.scss │ └── feature-a.service.ts ├── shared/ │ ├── shared.module.ts │ └── components/ │ ├── custom-button/ │ │ ├── custom-button.component.ts │ │ ├── custom-button.component.html │ │ └── custom-button.component.scss └── app.module.ts """ ### 1.2. Indentation and Whitespace * **Do This:** Use consistent indentation (preferably 2 spaces) for all files. Maintain a maximum line length of 120 characters. * **Don't Do This:** Use tabs for indentation or inconsistent spacing throughout the codebase. Allow lines to exceed the maximum length. **Why:** Consistent indentation and whitespace improves code readability and reduces merge conflicts. **Example:** """typescript import { Component, OnInit } from '@angular/core'; import { MatTableDataSource } from '@angular/material/table'; import { UserService } from './user.service'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.scss'] }) export class UserListComponent implements OnInit { dataSource = new MatTableDataSource<User>(); constructor(private userService: UserService) {} ngOnInit(): void { this.userService.getUsers().subscribe(users => { this.dataSource = new MatTableDataSource(users); }); } } """ ### 1.3. Line Breaks and Operators * **Do This:** Place operators at the beginning of the line when breaking long expressions. * **Don't Do This:** Place operators at the end of the line when breaking long expressions. **Why:** This makes it easier to understand the continuation of the expression and improves readability. **Example:** """typescript // Do This const result = this.userService.getUsers() .pipe( map(users => users.filter(user => user.isActive)), catchError(error => { console.error('Error fetching users', error); return of([]); }) ); // Don't Do This const result = this.userService.getUsers().pipe( map(users => users.filter(user => user.isActive)), catchError(error => { console.error('Error fetching users', error); return of([]); }) ); """ ### 1.4. Comments * **Do This:** Use comments to explain complex logic, non-obvious code segments, and API usage. Use JSDoc-style comments for documenting components, services, and methods. * **Don't Do This:** Over-comment obvious code or provide redundant explanations. **Why:** Comments clarify the intent and functionality of code, aiding understanding and future maintenance. **Example:** """typescript /** * Fetches a list of all active users from the server. * @returns An Observable that emits an array of User objects. */ getUsers(): Observable<User[]> { // Make an HTTP request to fetch the user data. return this.http.get<User[]>('/api/users') .pipe( map(users => users.filter(user => user.isActive)), catchError(this.handleError) // Handle errors gracefully. ); } """ ## 2. Naming Conventions ### 2.1. TypeScript Files * **Do This:** Use PascalCase for class names, camelCase for variables and methods, and UPPER_SNAKE_CASE for constants. Append appropriate suffixes (e.g., "Component", "Service", "Module"). * **Don't Do This:** Use inconsistent naming conventions or abbreviations that are not widely understood. **Why:** Consistent naming conventions facilitate easy identification of different code elements. **Example:** """typescript // Class export class UserService { ... } // Variable let userName: string; // Method getUserName(): string { ... } // Constant const MAX_USERS = 100; """ ### 2.2. Template Files (HTML) * **Do This:** Use kebab-case for component selectors and attributes. Bind properties using "[property]" and event handlers using "(event)". * **Don't Do This:** Use camelCase or inconsistent casing for selectors and attributes. **Why:** Kebab-case is the standard for HTML attributes, promoting consistency between Angular Material components and standard HTML elements. **Example:** """html <app-user-list [users]="userList" (userSelected)="onUserSelected($event)"></app-user-list> <mat-card> <mat-card-title>User Details</mat-card-title> <mat-card-content> <p>Name: {{ selectedUser.name }}</p> <p>Email: {{ selectedUser.email }}</p> </mat-card-content> </mat-card> """ ### 2.3. Style Files (SCSS) * **Do This:** Use kebab-case for class and ID selectors. Use BEM (Block Element Modifier) methodology for styling. Use variables for colors, fonts, and sizes. * **Don't Do This:** Use camelCase or inconsistent casing for selectors. Avoid inline styles. **Why:** BEM improves the scalability and maintainability of stylesheets. Variables promote consistency and ease of modification. **Example:** """scss $primary-color: #3f51b5; $font-stack: Arial, sans-serif; .user-list { &__item { padding: 10px; border-bottom: 1px solid #eee; &--active { background-color: lighten($primary-color, 40%); } } } """ ## 3. Angular Material Specific Style ### 3.1. Component Usage * **Do This:** Use Angular Material components directly from the "Mat" namespace (e.g., "MatButton", "MatInput"). Utilize the Angular Material theming system for consistent styling. Use "ng generate @angular/material:component" to scaffold a new component with Material elements. * **Don't Do This:** Recreate existing Material components or apply custom styles directly to Material elements without leveraging the theming system. **Why:** Using the official components ensures consistency and avoids reinventing the wheel. Theming makes it easy to maintain a consistent look and feel across the application. **Example:** """html <mat-form-field appearance="outline"> <mat-label>Username</mat-label> <input matInput placeholder="Enter your username"> <mat-hint>Username must be at least 5 characters</mat-hint> </mat-form-field> <button mat-raised-button color="primary">Submit</button> """ """scss @use '@angular/material' as mat; @mixin my-component-theme($theme) { $primary: mat.get-theme-color($theme, primary); $accent: mat.get-theme-color($theme, accent); .my-component { background-color: mat.get-color-from-palette($primary, 500); color: mat.get-color-from-palette($accent, A200); } } """ ### 3.2. Data Tables * **Do This:** Utilize "MatTableDataSource" for managing data in "mat-table". Implement sorting, filtering, and pagination using "MatSort", "MatPaginator", and the built-in table features. Consider using virtual scrolling ("@angular/cdk/scrolling") for large datasets. * **Don't Do This:** Manually implement table features like sorting and filtering or load all data into the table at once. **Why:** "MatTableDataSource" simplifies data management. Components like "MatSort" and "MatPaginator" provide consistent and accessible UX. Virtual scrolling optimizes rendering performance for large datasets. **Example:** """typescript import { Component, OnInit, ViewChild } from '@angular/core'; import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; @Component({ selector: 'app-data-table', templateUrl: './data-table.component.html', styleUrls: ['./data-table.component.scss'] }) export class DataTableComponent implements OnInit { displayedColumns: string[] = ['id', 'name', 'email']; dataSource = new MatTableDataSource<User>(); @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; ngOnInit() { this.dataSource.data = this.generateData(); // Replace with actual data fetching this.dataSource.paginator = this.paginator; this.dataSource.sort = this.sort; } applyFilter(event: Event) { const filterValue = (event.target as HTMLInputElement).value; this.dataSource.filter = filterValue.trim().toLowerCase(); if (this.dataSource.paginator) { this.dataSource.paginator.firstPage(); } } generateData(): User[] { const data: User[] = []; for (let i = 1; i <= 100; i++) { data.push({id: i, name: "User ${i}", email: "user${i}@example.com"}); } return data } } """ """html <mat-form-field appearance="fill"> <mat-label>Filter</mat-label> <input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia" #input> </mat-form-field> <div class="mat-elevation-z8"> <table mat-table [dataSource]="dataSource" matSort> <!-- ID Column --> <ng-container matColumnDef="id"> <th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th> <td mat-cell *matCellDef="let row"> {{row.id}} </td> </ng-container> <!-- Name Column --> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th> <td mat-cell *matCellDef="let row"> {{row.name}} </td> </ng-container> <!-- Email Column --> <ng-container matColumnDef="email"> <th mat-header-cell *matHeaderCellDef mat-sort-header> Email </th> <td mat-cell *matCellDef="let row"> {{row.email}} </td> </ng-container> <tr mat-header-row *matHeaderCellDef></tr> <th mat-header-cell *matHeaderCellDef matSortHeader> ID </th*> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> <!-- Row shown when there is no matching data. --> <tr class="mat-row" *matNoDataRow> <td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td> </tr> </table> <mat-paginator [pageSizeOptions]="[5, 10, 25, 100]" aria-label="Select page of users"></mat-paginator> </div> """ ### 3.3. Forms * **Do This:** Use Reactive Forms with Angular Material input components. Leverage "MatFormField" for consistent form styling and error handling. Use validation directives and "MatError" to display validation messages gracefully. * **Don't Do This:** Use Template-driven forms excessively or implement custom form styling without using "MatFormField". Avoid providing clear validation messages. **Why:** Reactive Forms provide better control and testability. "MatFormField" ensures accessible and consistent form layouts. Clear validation messages enhance the user experience. **Example:** """typescript import { Component, OnInit } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; @Component({ selector: 'app-login-form', templateUrl: './login-form.component.html', styleUrls: ['./login-form.component.scss'] }) export class LoginFormComponent implements OnInit { loginForm: FormGroup; ngOnInit() { this.loginForm = new FormGroup({ username: new FormControl('', [Validators.required, Validators.minLength(5)]), password: new FormControl('', [Validators.required]) }); } onSubmit() { if (this.loginForm.valid) { // Handle form submission console.log('Form submitted', this.loginForm.value); } } get username() { return this.loginForm.get('username'); } get password() { return this.loginForm.get('password'); } } """ """html <form [formGroup]="loginForm" (ngSubmit)="onSubmit()"> <mat-form-field appearance="outline"> <mat-label>Username</mat-label> <input matInput formControlName="username" required> <mat-error *ngIf="username.invalid"> <span *ngIf="username.errors?.required">Username is required</span> <span *ngIf="username.errors?.minlength">Username must be at least 5 characters</span> </mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Password</mat-label> <input matInput type="password" formControlName="password" required> <mat-error *ngIf="password.invalid">Password is required</mat-error> </mat-form-field> <button mat-raised-button color="primary" type="submit" [disabled]="loginForm.invalid">Login</button> </form> """ ### 3.4. Dialogs and Overlays * **Do This:** Use "MatDialog" for creating modal dialogs. Pass data to dialogs using the "data" option and receive results using "afterClosed()". Use "MatSnackBar" for displaying non-modal messages. Use proper aria attributes for accessibility * **Don't Do This:** Create custom dialog implementations or use "alert()"/"confirm()". Neglect accessibility best practices when creating dialogs. **Why:** "MatDialog" provides consistent styling and accessibility features. "MatSnackBar" offers a non-intrusive way to display messages. **Example:** """typescript import { Component, Inject } from '@angular/core'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @Component({ selector: 'app-confirmation-dialog', templateUrl: './confirmation-dialog.component.html', }) export class ConfirmationDialogComponent { constructor( public dialogRef: MatDialogRef<ConfirmationDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: any) {} onNoClick(): void { this.dialogRef.close(); } } @Component({ selector: 'app-my-component', template: " <button mat-raised-button (click)="openDialog()">Open Dialog</button> " }) export class MyComponent { constructor(public dialog: MatDialog) {} openDialog(): void { const dialogRef = this.dialog.open(ConfirmationDialogComponent, { width: '250px', data: {message: 'Are you sure you want to delete this item?'} }); dialogRef.afterClosed().subscribe(result => { if (result) { // Handle confirmation console.log('The dialog was closed', result); } else { console.log("Dialog was cancelled") // Handle cancellation } }); } } """ """html <!-- confirmation-dialog.component.html --> <h1 mat-dialog-title>Confirmation</h1> <div mat-dialog-content> {{ data.message }} </div> <div mat-dialog-actions> <button mat-button (click)="onNoClick()">Cancel</button> <button mat-button [mat-dialog-close]="true" cdkFocusInitial>Confirm</button> </div> """ ### 3.5. Theming * **Do This:** Define custom themes using SCSS mixins that extend Angular Material's default themes. Use different themes for different parts of the application, if needed. * **Don't Do This:** Use inline styles or override Angular Material styles without using the theming system. **Why:** Theming ensures consistent styling and easy customization. **Example:** """scss // src/app/core/styles/_custom-theme.scss @use '@angular/material' as mat; @import '~@angular/material/theming'; // Include the common styles for Angular Material. We include this here so that you only // have to load a single css file for Angular Material in your app. @include mat.core(); // Define the palettes for primary, accent, and warn using the Material Design palettes. $candy-app-primary: mat.define-palette(mat.$indigo-palette, 500); $candy-app-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400); // The warn palette is optional (defaults to red). $candy-app-warn: mat.define-palette(mat.$red-palette); // Create the theme object. A theme consists of configurations for individual // components of a UI library. $candy-app-theme: mat.define-light-theme(( color: ( primary: $candy-app-primary, accent: $candy-app-accent, warn: $candy-app-warn, ) )); @include mat.all-component-themes($candy-app-theme); """ * The styles for the theme will be applied inside "styles.scss" """scss @import './app/core/styles/custom-theme'; body { margin: 0; font-family: Roboto, sans-serif; } .unicorn-dark-theme { @include angular-material-theme($dark-unicorn-theme); } """ ### 3.6. Accessibility (A11y) * **Do This:** Always consider accessibility when using Angular Material components. Use ARIA attributes where needed, ensure proper focus management, and provide alternative text for images. Test with screen readers. * **Don't Do This:** Ignore accessibility considerations or rely solely on visual cues. **Why:** Accessibility makes applications usable for everyone, including users with disabilities. **Example:** """html <button mat-button aria-label="Add a new user"> <mat-icon>add</mat-icon> </button> <img src="user-profile.jpg" alt="User profile picture"> """ ## 4. Code Organization and Architecture ### 4.1. Modularity * **Do This:** Divide the application into feature modules and shared modules. Lazy-load feature modules to improve initial load time. * **Don't Do This:** Create monolithic modules containing all application components. **Why:** Modularity improves maintainability, testability, and scalability of the application. Lazy loading reduces initial load time by loading modules on demand. ### 4.2. Services * **Do This:** Use services for managing shared logic, data fetching, and state management. Provide services at the appropriate level (e.g., root, module, or component). * **Don't Do This:** Duplicate logic across components or inject services into components where they are not needed. **Why:** Services promote code reusability and separation of concerns. ### 4.3. State Management * **Do This:** Use a state management library (e.g., NgRx, Akita, or built-in Angular services with RxJS) for managing application state, especially for complex applications. * **Don't Do This:** Rely solely on "@Input()" and "@Output()" for passing data between components in complex scenarios. **Why:** State management libraries provide a central location for managing application state, making it easier to reason about and debug. ## 5. Error Handling * **Do This:** Implement global error handling using an "ErrorHandler". Log errors to the console and/or a remote logging service. Display user-friendly error messages using "MatSnackBar" or a custom error component. * **Don't Do This:** Let errors crash the application silently or display technical error messages to the user. **Why:** Proper error handling improves the stability and user experience of the application. **Example:** """typescript import { ErrorHandler, Injectable } from '@angular/core'; import { MatSnackBar } from '@angular/material/snack-bar'; @Injectable() export class GlobalErrorHandler implements ErrorHandler { constructor(private snackBar: MatSnackBar) {} handleError(error: any) { console.error('An error occurred:', error); this.snackBar.open('An unexpected error occurred. Please try again later.', 'Close', { duration: 5000, }); } } // In app.module.ts import { GlobalErrorHandler } from './global-error-handler'; @NgModule({ ... providers: [ { provide: ErrorHandler, useClass: GlobalErrorHandler } ], ... }) export class AppModule { } """ ## 6. Performance Optimization * **Do This:** Use OnPush change detection strategy where appropriate. Debounce or throttle user input events. Use virtual scrolling for large lists. Optimize images and other assets. * **Don't Do This:** Use default change detection strategy unnecessarily or perform expensive operations in the template. **Why:** Performance optimization improves the responsiveness and scalability of the application. ## 7. Security * **Do This:** Sanitize user input to prevent XSS attacks. Use HTTPS for all network requests. Validate data on the server-side. Avoid storing sensitive information in local storage. Handle authentication and authorization properly. * **Don't Do This:** Trust user input or expose sensitive information. **Why:** Security is crucial for protecting user data and preventing malicious attacks. Sanitize any HTML that comes from untrusted sources using Angular's "DomSanitizer". ## 8. Testing * **Do This:** Write unit tests for components, services, and pipes. Use end-to-end tests to verify the overall application functionality. Use appropriate test doubles (e.g., mocks, stubs, spies). * **Don't Do This:** Skip testing or write tests that are too brittle or superficial. **Why:** Testing ensures the quality and reliability of the application. ## 9. Documentation * **Do This:** Document components, services, and methods using JSDoc-style comments. Provide README files for modules and libraries. Use a documentation generator (e.g., Compodoc) to create API documentation. * **Don't Do This:** Neglect documentation or provide incomplete or outdated documentation. **Why:** Documentation makes it easier for developers to understand, use, and maintain the codebase. By following these coding style and conventions, development teams can create high-quality Angular Material applications that are maintainable, scalable, and performant. These guidelines are designed for the latest versions of Angular Material, and keeping up with best practices is essential for modern applications.
# Security Best Practices Standards for Angular Material This document outlines security best practices for developing Angular applications using Angular Material. It aims to provide developers with clear, actionable guidelines to prevent common vulnerabilities and ensure the creation of secure, robust applications. These coding standards are designed to be used in conjunction with AI coding assistants, ensuring consistency and promoting secure coding practices across teams. ## 1. General Security Principles ### 1.1. Principle of Least Privilege * **Do This:** Grant users and components only the necessary permissions to perform their tasks. * **Don't Do This:** Over-provision permissions, which can lead to unauthorized access and potential security breaches. **Why:** Limiting privileges reduces the impact of compromised accounts or components. Even if an attacker gains access, their actions are restricted by the limited privileges. ### 1.2. Input Validation * **Do This:** Validate all user inputs rigorously, both on the client-side and the server-side. * **Don't Do This:** Trust user inputs without proper validation, as this can lead to injection attacks, cross-site scripting (XSS), and other vulnerabilities. **Why:** Input validation ensures that the application processes only expected and safe data, preventing malicious inputs from causing harm. ### 1.3. Output Encoding * **Do This:** Encode all output data before rendering it in the UI, especially data that includes user-supplied content. * **Don't Do This:** Directly inject user-supplied data into the DOM without proper encoding, which can lead to XSS attacks. **Why:** Output encoding neutralizes potentially harmful characters, preventing them from being interpreted as executable code by the browser. ### 1.4. Keep Up-to-Date * **Do This:** Regularly update Angular, Angular Material, and all other dependencies to the latest versions to patch security vulnerabilities. * **Don't Do This:** Use outdated versions of libraries, which may contain known security flaws that attackers can exploit. **Why:** Staying updated ensures that your application benefits from the latest security patches and improvements. ## 2. Protecting Against Common Vulnerabilities ### 2.1. Cross-Site Scripting (XSS) Prevention #### 2.1.1. Avoiding InnerHTML and other unsafe DOM Manipulation * **Do This:** Use Angular's data binding and templating features to safely render data in the UI. * **Don't Do This:** Use "innerHTML" or other methods that directly manipulate the DOM with user-supplied data. **Why:** Direct DOM manipulation can introduce XSS vulnerabilities if user-supplied data contains malicious scripts. **Example (Anti-Pattern):** """typescript // Anti-pattern: Directly injecting HTML element.innerHTML = userInput; """ **Example (Correct Implementation):** """typescript // Correct implementation: Using Angular's data binding import { Component } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ selector: 'app-safe-html', template: "<div [innerHTML]="safeHtml"></div>" }) export class SafeHtmlComponent { safeHtml: SafeHtml; constructor(private sanitizer: DomSanitizer) { const userInput = '<img src="x" onerror="alert(\'XSS\')">'; this.safeHtml = this.sanitizer.bypassSecurityTrustHtml(userInput); //BE VERY CAREFUL and only use with trusted sources! } } """ **Explanation:** The "DomSanitizer" provides methods to sanitize and explicitly mark values as safe. "bypassSecurityTrustHtml" tells Angular that the provided HTML is safe, *but only use this if you trust the source*. Prefer using data binding when possible and avoid directly injecting user-provided HTML. Always sanitize HTML before using this method, especially when dealing with a CMS or other user-controlled content sources. #### 2.1.2. Sanitizing User Inputs * **Do This:** Use Angular's "DomSanitizer" to sanitize user inputs before rendering them in the UI. * **Don't Do This:** Render user inputs directly without sanitization. **Why:** Sanitization removes potentially harmful code from user inputs, preventing XSS attacks. **Example (Correct Implementation):** """typescript import { Component, SecurityContext } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; @Component({ selector: 'app-sanitization', template: " <p>Unsafe value: {{unsafeValue}}</p> <p>Safe value: {{safeValue}}</p> " }) export class SanitizationComponent { unsafeValue: string = '<img src="x" onerror="alert(\'XSS\')">'; safeValue: string; constructor(private sanitizer: DomSanitizer) { this.safeValue = this.sanitizer.sanitize(SecurityContext.HTML, this.unsafeValue) || ''; // Important to provide a default value if sanitize returns null! } } """ **Explanation:** The "DomSanitizer.sanitize" method removes potentially harmful code from the "unsafeValue", making it safe to render in the UI. The "SecurityContext.HTML" parameter specifies that the value is being used as HTML. Remember to provide a default value to handle cases where sanitize might return "null". #### 2.1.3. Using Angular Material's Built-in Security Features * **Do This:** Leverage Angular Material's built-in security features, such as its handling of HTML content in components like "MatDialog". * **Don't Do This:** Override or bypass Angular Material's security mechanisms without careful consideration. **Why:** Angular Material is designed with security in mind, and its components include safeguards against common vulnerabilities. **Example (Correct Implementation with MatDialog):** """typescript import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ selector: 'app-dialog-content', template: "<div [innerHTML]="data.content"></div>" }) export class DialogContentComponent { constructor( public dialogRef: MatDialogRef<DialogContentComponent>, @Inject(MAT_DIALOG_DATA) public data: { content: SafeHtml } ) {} } // In the parent component: import { MatDialog } from '@angular/material/dialog'; constructor(private dialog: MatDialog, private sanitizer: DomSanitizer) {} openDialog(userInput: string) { const safeContent = this.sanitizer.bypassSecurityTrustHtml(userInput); this.dialog.open(DialogContentComponent, { data: { content: safeContent }, }); } """ **Explanation:** By using "MatDialog" and passing the sanitized HTML content to the dialog, you ensure that the content displayed in the dialog is safe from XSS attacks. However, reminder: "bypassSecurityTrustHtml" should only be use on trusted content. ### 2.2. Cross-Site Request Forgery (CSRF) Prevention #### 2.2.1. Implementing CSRF Tokens * **Do This:** Implement CSRF tokens to protect against CSRF attacks. Send a unique, unpredictable token with each state-changing request. * **Don't Do This:** Rely solely on cookies for authentication, as they are vulnerable to CSRF attacks. **Why:** CSRF tokens ensure that requests originate from the application itself, preventing attackers from forging requests on behalf of authenticated users. **Implementation:** 1. **Server-Side:** Generate a unique CSRF token for each user session and include it in the response to the initial request. 2. **Client-Side (Angular):** Store the CSRF token and include it in the headers of all state-changing requests (e.g., POST, PUT, DELETE). 3. **Server-Side Verification:** Verify the CSRF token on the server-side before processing the request. **Example (Angular Interceptor):** """typescript import { Injectable } from '@angular/core'; import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse, } from '@angular/common/http'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class CsrfInterceptor implements HttpInterceptor { intercept( request: HttpRequest<any>, next: HttpHandler ): Observable<HttpEvent<any>> { const csrfToken = localStorage.getItem('csrfToken'); // Or session storage if (csrfToken) { request = request.clone({ setHeaders: { 'X-CSRF-TOKEN': csrfToken, }, }); } return next.handle(request).pipe( tap( { next: (event: HttpEvent<any>) => { if (event instanceof HttpResponse) { const newCsrfToken = event.headers.get('X-CSRF-TOKEN'); if (newCsrfToken) { localStorage.setItem('csrfToken', newCsrfToken); } } return event; } }) ); } } """ **Explanation:** This interceptor retrieves the CSRF token form localStorage (where your server sets it), then adds it to the "X-CSRF-TOKEN" header on each outgoing request. The server is responsible for validating the token. Moreover, the interceptor takes care of renewing the token if the server sends a new one back in the response headers. #### 2.2.2. Using "withCredentials" for Cross-Origin Requests * **Do This:** Set the "withCredentials" flag to "true" for cross-origin requests that require authentication. * **Don't Do This:** Omit the "withCredentials" flag for authenticated cross-origin requests, as this can prevent cookies from being sent, leading to authentication issues. **Why:** The "withCredentials" flag ensures that cookies and other credentials are included in cross-origin requests, allowing the server to authenticate the user. **Example:** """typescript import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class ApiService { constructor(private http: HttpClient) {} getData() { return this.http.get('/api/data', { withCredentials: true, // Ensure cookies are sent }); } } """ ### 2.3. Clickjacking Protection #### 2.3.1. Setting the "X-Frame-Options" Header * **Do This:** Set the "X-Frame-Options" header to "DENY" or "SAMEORIGIN" in the server response to prevent clickjacking attacks. * **Don't Do This:** Omit the "X-Frame-Options" header, as this allows attackers to embed your application in a frame and trick users into performing unintended actions. **Why:** The "X-Frame-Options" header prevents your application from being embedded in a frame, mitigating the risk of clickjacking attacks. "DENY" prevents all framing, and "SAMEORIGIN" allows framing only from the same origin. **Implementation:** This is typically configured on the server-side (e.g., in the web server configuration or in the application's middleware). #### 2.3.2. Content Security Policy (CSP) * **Do This**: Implement a strict CSP policy in your web server configuration to control the resources that the browser is allowed to load for your application. This includes scripts, styles, images, fonts, and other assets. * **Don't Do This**: Use a permissive or overly broad CSP policy, or omit CSP entirely. A weak CSP can leave your application vulnerable to XSS and other injection attacks. **Why**: CSP acts as an additional layer of security that can detect and mitigate certain types of attacks, including XSS. It informs the browser which sources of content are trusted, and the browser will block content from untrusted sources. **Example (CSP Header):** """ Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' https://fonts.googleapis.com; img-src 'self' data:; font-src 'self' https://fonts.gstatic.com; """ **Explanation**: This CSP policy does the following: * "default-src 'self'": Only allows resources from the same origin by default. * "script-src 'self' https://trusted.cdn.com": Allows JavaScript from the same origin and a trusted CDN. * "style-src 'self' https://fonts.googleapis.com": Allows CSS from the same origin and Google Fonts. * "img-src 'self' data:": Allows images from the same origin and data URIs (Base64 encoded images). * "font-src 'self' https://fonts.gstatic.com": Allows fonts from the same origin and Google Fonts. ### 2.4. Dependency Vulnerabilities #### 2.4.1. Using "npm audit" or "yarn audit" * **Do This:** Regularly use "npm audit" or "yarn audit" to identify and fix known vulnerabilities in your project's dependencies. * **Don't Do This:** Ignore security warnings from "npm audit" or "yarn audit", as they may indicate critical vulnerabilities that need to be addressed. **Why:** Dependency vulnerabilities can expose your application to security risks. Regularly auditing and updating dependencies helps to mitigate these risks. **Example:** """bash npm audit fix # or yarn audit yarn audit fix """ #### 2.4.2. Using Snyk or Other Security Scanning Tools * **Do This:** Integrate security scanning tools like Snyk into your development workflow to automatically detect and fix vulnerabilities in your dependencies. * **Don't Do This:** Rely solely on manual checks for dependency vulnerabilities, as they can be time-consuming and error-prone. **Why:** Automated security scanning tools provide continuous monitoring and alerting for dependency vulnerabilities, helping to ensure that your application remains secure. ## 3. Secure Coding Patterns ### 3.1. Secure Authentication and Authorization #### 3.1.1. Using JWT (JSON Web Tokens) * **Do This:** Use JWT for stateless authentication and authorization. * **Don't Do This:** Store sensitive user data directly in the JWT, as this can expose it to unauthorized parties. **Why:** JWT provides a secure and stateless mechanism for authenticating users and authorizing access to protected resources. However, JWTs can be decoded easily, so avoid placing sensitive or confidential data in the payload. **Example:** """typescript // Example of setting the JWT in localStorage after successful login localStorage.setItem('jwtToken', response.token); // Example of retrieving the JWT from localStorage for API calls const token = localStorage.getItem('jwtToken'); """ **Note:** Securely storing and handling JWTs in the client-side is critical. Consider using HTTP-only cookies for enhanced protection. #### 3.1.2. Implementing Role-Based Access Control (RBAC) * **Do This:** Implement RBAC to control access to different parts of the application based on user roles. * **Don't Do This:** Grant all users the same level of access, as this can lead to unauthorized access and potential security breaches. **Why:** RBAC ensures that users have access only to the resources and functionalities that are appropriate for their roles. **Example (Using Angular Guards):** """typescript import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AdminGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): boolean { if (this.authService.isAdmin()) { return true; } else { this.router.navigate(['/unauthorized']); return false; } } } """ **Explanation:** This guard checks if the user has the "admin" role. If not, it redirects them to an unauthorized page. This guard can then be applied to routes that should only be accessible to administrators in the "app-routing.module.ts" file. ### 3.2. Secure Data Handling #### 3.2.1. Encrypting Sensitive Data * **Do This:** Encrypt sensitive data both in transit and at rest. * **Don't Do This:** Store sensitive data in plain text, as this can expose it to unauthorized parties. **Why:** Encryption protects sensitive data from being accessed by unauthorized parties, even if the storage or transmission medium is compromised. #### 3.2.2. Using HTTPS * **Do This:** Always use HTTPS to encrypt data in transit between the client and the server. * **Don't Do This:** Use HTTP for sensitive data, as it can be intercepted by attackers. **Why:** HTTPS ensures that data is encrypted during transmission, preventing eavesdropping and tampering. ## 4. Specific Angular Material Considerations ### 4.1. Template Injection Angular Material is designed to minimize risks, but dynamic content and complex templates require extra care. * Use "DomSanitizer" for user-provided HTML as mentioned previously. * When using "MatTooltip", "MatDialog", or other components with dynamic templates, validate and sanitize any user-provided data that influences the template. ### 4.2. Theme Security * Ensure custom themes and styles do not introduce vulnerabilities. * Review custom CSS or SCSS added to themes, preventing injection of malicious styles. ## 5. Monitoring and Logging ### 5.1. Logging Security Events * **Do This:** Log all security-related events, such as authentication failures, authorization failures, and suspicious activity. * **Don't Do This:** Log sensitive data, such as passwords or API keys. **Why:** Logging security events provides valuable information for detecting and investigating security incidents. ### 5.2. Monitoring Application Health * **Do This:** Monitor the application's health and performance to detect anomalies that may indicate a security issue. * **Don't Do This:** Ignore performance degradation or error spikes, as they may be signs of an attack or vulnerability. ## 6. Conclusion By adhering to these security best practices, developers can create Angular Material applications that are robust, secure, and resistant to common vulnerabilities. Regularly reviewing and updating these standards is essential to keep pace with evolving security threats and ensure the ongoing security of your applications. Remember to combine these guidelines with thorough testing and security assessments to achieve a comprehensive security posture.