# State Management Standards for Angular Material
This document outlines the standards for state management when developing Angular applications using Angular Material. It covers approaches to managing application state, data flow, and reactivity, with specific applications to Angular Material components and overall application architecture. Adhering to these standards ensures maintainability, performance, and a unified development experience.
## 1. Core Principles
* **Single Source of Truth:** Maintain a single, reliable source for your application's state. Avoid duplicating state across components.
* **Unidirectional Data Flow:** Data should flow in one direction through your application. This makes it easier to track changes and debug issues.
* **Immutability:** Prefer immutable data structures. Changes to state should create new objects rather than modifying existing ones.
* **Separation of Concerns:** Keep state management logic separate from Angular Material component implementation details. This improves testability and reusability.
* **Observable State:** Use Observables to represent application state. This enables reactive updates and efficient change detection.
## 2. State Management Approaches
Choosing the right approach is crucial for scalable and maintainable applications.
### 2.1 Local Component State
#### Standard
* Use local component state for UI-specific concerns that _do not_ need to be shared across components or persist beyond the component's lifecycle. This includes managing the "open" or "closed" state of a dialog, the selected tab in a "mat-tab-group", or the temporary state of a single form.
#### Do This
"""typescript
import { Component } from '@angular/core';
@Component({
selector: 'app-example',
template: "
Panel
<p>Content</p>
",
})
export class ExampleComponent {
panelOpenState = false;
}
"""
#### Don't Do This
* Don't use local component state to manage data that needs to be shared between components or that you want to persist across route changes. This can quickly lead to inconsistent state.
* Don't use "@Input()" bindings as primary sources of truth. They are for receiving data, not managing it internally.
#### Why This Matters
* **Simplicity:** Local state avoids the overhead of more complex state management solutions for simple UI interactions.
* **Performance:** Directly manipulating component properties is more performant than dispatching actions and waiting for state updates for trivial UI changes.
### 2.2 Services with RxJS (Recommended for Small to Medium Applications)
#### Standard
* Encapsulate related pieces of application state within a service.
* Use an RxJS "BehaviorSubject" (or "ReplaySubject" if history is needed) to hold the current state.
* Expose the state as an "Observable" using ".asObservable()" to prevent direct modification from components.
* Provide methods in the service to update the state. These methods should be the _only_ way to modify the state.
* Services should be kept as pure as possible, with side-effects such as API calls delegated to side-effect handlers.
* Services should be "@Injectable({ providedIn: 'root' })" unless a more limited scope is explicitly required.
#### Do This
"""typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
interface User {
id: number;
name: string;
// ... potentially more user properties
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private readonly _users = new BehaviorSubject([]);
readonly users$: Observable = this._users.asObservable();
private users: User[] = []; // Private backing field
constructor() {
// Simulate loading users (in a real app, this would be an API call)
setTimeout(() => {
this.users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
this._users.next([...this.users]); // Update the BehaviorSubject
}, 500);
}
addUser(newUser: User): void {
this.users = [...this.users, newUser];
this._users.next([...this.users]);
}
updateUser(updatedUser: User): void {
this.users = this.users.map(user => user.id === updatedUser.id ? updatedUser : user);
this._users.next([...this.users]);
}
deleteUser(userId: number): void {
this.users = this.users.filter(user => user.id !== userId);
this._users.next([...this.users]);
}
}
"""
##### Consuming the Service in a Component:
"""typescript
import { Component, OnInit } from '@angular/core';
import { UserService } from './user.service';
import { Observable } from 'rxjs';
import { MatTableDataSource } from '@angular/material/table'; // Example use of a Material component
@Component({
selector: 'app-user-list',
template: "
ID
{{element.id}}
Name
{{element.name}}
",
styleUrls: ['./user-list.component.css']
})
export class UserListComponent implements OnInit {
users$: Observable;
dataSource: MatTableDataSource;
displayedColumns: string[] = ['id', 'name']; // Columns for the Material table
constructor(private userService: UserService) {
this.users$ = this.userService.users$; // Get the observable of users
this.dataSource = new MatTableDataSource([]); // Initialize data source for Material Table.
}
ngOnInit(): void {
this.users$.subscribe(users => {
this.dataSource.data = users; // Update the Material Table's data source
});
}
}
"""
#### Don't Do This
* Don't directly modify the "BehaviorSubject"'s value from outside the service (hence the ".asObservable()").
* Avoid performing complex logic or transformations directly within the component's template using pipes. This decreases performance and makes debugging difficult. Instead, transform the data in the component or service.
* Do not subscribe directly in the template using the "async" pipe when you only need the *current* value once. It is more efficient in those cases to subscribe in the component and get the value into a local property.
* Don't inject application services into other application services without careful consideration of dependencies and potential circular references. Use factory functions to create instances with dependencies, or use the "forwardRef" token to break circular dependencies if absolutely necessary.
#### Why This Matters
* **Centralized State:** Enforces a single source of truth for user data.
* **Reactivity:** Components react automatically to changes in the user list.
* **Testability:** Easier to test the "UserService" in isolation.
* **Maintainability:** Clear separation of concerns makes the code easier to understand and modify.
* **Integration with Angular Material:** Easily bind data from the service to Angular Material components like "mat-table".
### 2.3 NgRx (Recommended for Large, Complex Applications)
#### Standard
* Use NgRx when your application requires complex state management, handling side effects, or benefits significantly from predictable state changes and time-travel debugging. This typically applies to larger applications with multiple features that share data and complex interactions.
* Follow the NgRx architecture:
* **State:** Define the state of your application using interfaces or classes.
* **Actions:** Represent events that trigger state changes.
* **Reducers:** Pure functions that take the current state and an action, and return the *new* state.
* **Selectors:** Functions that extract specific pieces of data from the state.
* **Effects:** Handle side effects such as API calls.
* **Store:** The central data store that holds the application state.
* Use the NgRx CLI schematics to generate boilerplate code.
* Use the "OnPush" change detection strategy in components that consume state from the store.
#### Do This
##### Defining State
"""typescript
// src/app/state/user.state.ts
import { User } from '../models/user.model';
export interface UserState {
users: User[];
loading: boolean;
error: string | null;
}
export const initialState: UserState = {
users: [],
loading: false,
error: null
};
"""
##### Defining Actions
"""typescript
// src/app/state/user.actions.ts
import { createAction, props } from '@ngrx/store';
import { User } from '../models/user.model';
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction('[User] Load Users Success', props<{ users: User[] }>());
export const loadUsersFailure = createAction('[User] Load Users Failure', props<{ error: string }>());
export const addUser = createAction('[User] Add User', props<{ user: User }>());
// More actions for update, delete, etc.
"""
##### Defining Reducers
"""typescript
// src/app/state/user.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { initialState, UserState } from './user.state';
import * as UserActions from './user.actions';
export const userReducer = createReducer(
initialState,
on(UserActions.loadUsers, (state) => ({ ...state, loading: true })),
on(UserActions.loadUsersSuccess, (state, { users }) => ({ ...state, users: users, loading: false, error: null })),
on(UserActions.loadUsersFailure, (state, { error }) => ({ ...state, loading: false, error: error })),
on(UserActions.addUser, (state, { user }) => ({ ...state, users: [...state.users, user] }))
// More reducers for other actions
);
"""
##### Defining Effects
"""typescript
// src/app/state/user.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { UserService } from '../user.service';
import * as UserActions from './user.actions';
import { catchError, map, mergeMap, of } from 'rxjs';
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() => this.actions$.pipe(
ofType(UserActions.loadUsers),
mergeMap(() => this.userService.getUsers() // Assume a getUsers method exists in your UserService
.pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error => of(UserActions.loadUsersFailure({ error: error.message })))
)
)
));
constructor(private actions$: Actions, private userService: UserService) {}
}
"""
##### Defining Selectors
"""typescript
// src/app/state/user.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { UserState } from './user.state';
export const selectUserState = createFeatureSelector('user'); // 'user' is the feature key in combineReducers
export const selectUsers = createSelector(selectUserState, (state: UserState) => state.users);
export const selectUserLoading = createSelector(selectUserState, (state: UserState) => state.loading);
export const selectUserError = createSelector(selectUserState, (state: UserState) => state.error);
"""
##### Consuming state in a Component
"""typescript
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { User } from '../models/user.model';
import * as UserActions from '../state/user.actions';
import * as UserSelectors from '../state/user.selectors';
import { MatTableDataSource } from '@angular/material/table';
@Component({
selector: 'app-user-list',
template: "
ID
{{element.id}}
Name
{{element.name}}
Error: {{error}}
",
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class UserListComponent implements OnInit {
users$: Observable;
loading$: Observable;
error$: Observable;
dataSource: MatTableDataSource;
displayedColumns: string[] = ['id', 'name'];
constructor(private store: Store) {
this.users$ = this.store.select(UserSelectors.selectUsers);
this.loading$ = this.store.select(UserSelectors.selectUserLoading);
this.error$ = this.store.select(UserSelectors.selectUserError);
this.dataSource = new MatTableDataSource([]); // Initialize data source for Material Table.
}
ngOnInit(): void {
this.store.dispatch(UserActions.loadUsers()); // Dispatch the action to load users
this.users$.subscribe(users => {
this.dataSource.data = users; // Update the Material Table's data source
});
}
}
"""
##### AppModule setup
"""typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { StoreDevtoolsModule } from '@ngrx/store-devtools'; // For debugging
import { userReducer } from './state/user.reducer';
import { UserEffects } from './state/user.effects'; // Import your UserEffects
import { environment } from '../environments/environment'; // Import environment.ts
@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot({ user: userReducer }), // Register the userReducer under the 'user' feature key
EffectsModule.forRoot([UserEffects]), // Register the UserEffects
// Only add StoreDevtoolsModule in development
environment.production ? [] : StoreDevtoolsModule.instrument({
maxAge: 25, // Retains last 25 states
logOnly: environment.production, // Restrict extension to log-only mode
})
],
declarations: [ /* Your components */ ],
bootstrap: [ /* Your root component */ ]
})
export class AppModule { }
"""
#### Don't Do This
* Don't skip defining actions and directly dispatch anonymous objects to the store. This makes debugging and tracking state changes difficult.
* Don't perform API calls directly within reducers. Reducers must be pure functions.
* Don't mutate the state directly within reducers; always return a new state object. Spread operator ("...") is your friend.
* Don't select the entire state object when you only need a small portion of it. This can trigger unnecessary change detection cycles. Use memoized selectors (using "createSelector") for derived data.
#### Why This Matters
* **Predictable State:** State changes are triggered by actions, making debugging and testing easier.
* **Centralized State:** All application state is managed in a single store.
* **Scalability:** Well-suited for large, complex applications.
* **Debugging:** NgRx DevTools provides time-travel debugging capabilities.
* **Performance:** "OnPush" change detection minimizes unnecessary re-renders.
* **Testability:** Reducers and effects are easily testable in isolation.
### 2.4 Akita (Alternative to NgRx, Simpler Setup)
#### Standard
* Consider Akita for a simpler, more straightforward state management solution compared to NgRx, especially if you are working in a team less experienced with Redux-style architectures.
* Akita uses entities stores and queries, making it easier to manage collections of data.
* Leverage Akita's built-in immutability and optimistic updates.
#### Do This
##### Defining an Entity State
"""typescript
// src/app/state/user.state.ts
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';
import { Injectable } from '@angular/core';
export interface User {
id: number;
name: string;
// ... other properties
}
export interface UserState extends EntityState {}
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'users' }) // A unique name for the store
export class UserStore extends EntityStore {
constructor() {
super();
}
}
"""
##### Defining a Query
"""typescript
// src/app/state/user.query.ts
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { UserStore, UserState, User } from './user.store';
import { Observable } from 'rxjs';
@Injectable({ providedIn: 'root' })
export class UserQuery extends QueryEntity {
constructor(protected store: UserStore) {
super(store);
}
selectLoading$: Observable = this.selectLoading(); // Built-in loading selector
}
"""
##### Updating the Store and Using the Query
"""typescript
// src/app/user.service.ts
import { Injectable } from '@angular/core';
import { UserStore, User } from './state/user.store';
import { HttpClient } from '@angular/common/http'; // Example of using HTTP
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private userStore: UserStore, private http: HttpClient) {}
getUsers() {
this.userStore.setLoading(true); // Set loading state
this.http.get('/api/users') // Replace with your API endpoint
.subscribe(users => {
this.userStore.set(users); // Set the users in the store
this.userStore.setLoading(false); // Reset loading state
}, error => {
// Handle error
this.userStore.setLoading(false);
});
}
addUser(user: User) {
this.userStore.add(user);
}
updateUser(id: number, user: Partial) {
this.userStore.update(id, user);
}
removeUser(id: number) {
this.userStore.remove(id);
}
}
"""
##### Consuming State in a Component
"""typescript
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from './state/user.store';
import { UserQuery } from './state/user.query';
import { UserService } from './user.service'; // Import UserService
import { MatTableDataSource } from '@angular/material/table';
@Component({
selector: 'app-user-list',
template: "
ID
{{element.id}}
Name
{{element.name}}
Loading...
{{ user.name }}
",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent implements OnInit {
users$: Observable;
loading$: Observable;
dataSource: MatTableDataSource;
displayedColumns: string[] = ['id', 'name'];
constructor(private userQuery: UserQuery, private userService: UserService) {
this.users$ = this.userQuery.selectAll();
this.loading$ = this.userQuery.selectLoading$;
this.dataSource = new MatTableDataSource([]);
}
ngOnInit() {
this.users$.subscribe(users => {
this.dataSource.data = users;
});
this.userService.getUsers(); // Load users on init via the service
}
}
"""
#### Don't Do This
* Don't modify state directly using "store.update()" without considering optimistic updates or error handling.
* Avoid creating too many stores. Group related entities into a single store where appropriate.
#### Why This Matters
* **Simplicity:** Easier to learn and implement compared to NgRx.
* **Entity Management:** Simplifies managing collections of data.
* **Boilerplate Reduction:** Requires less boilerplate code than NgRx.
* **Optimistic Updates:** Built-in support for optimistic updates.
## 3. Angular Material Specific Considerations
Angular Material components often involve managing internal state for UI interactions.
### 3.1 Dialogs ("MatDialog")
#### Standard
* When passing data to a dialog, use the "data" configuration option of "MatDialogConfig".
* Make the dialog component responsible for managing its own form state and validation.
* Return data from the dialog using the "MatDialogRef.close()" method.
* Consume the data returned from the dialog using "MatDialogRef.afterClosed()".
#### Do This
##### Opening Dialog
"""typescript
import { MatDialog } from '@angular/material/dialog';
import { UserEditDialogComponent } from './user-edit-dialog.component';
@Component({
selector: 'app-user-profile',
template: "
Edit Profile
"
})
export class UserProfileComponent {
constructor(public dialog: MatDialog) {}
openEditDialog(): void {
const dialogRef = this.dialog.open(UserEditDialogComponent, {
width: '400px',
data: { name: 'John Doe', email: 'john.doe@example.com' } // Initial data
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
if (result) {
console.log('Dialog result:', result); // Updated data from the dialog
}
});
}
}
"""
##### Dialog Component
"""typescript
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
@Component({
selector: 'app-user-edit-dialog',
template: "
Edit Profile
Name
Cancel
Save
"
})
export class UserEditDialogComponent {
form: FormGroup;
constructor(
public dialogRef: MatDialogRef,
@Inject(MAT_DIALOG_DATA) public data: any,
private fb: FormBuilder
) {
this.form = this.fb.group({
name: [data.name, Validators.required],
email: [data.email, [Validators.required, Validators.email]]
});
}
onNoClick(): void {
this.dialogRef.close();
}
}
"""
#### Don't Do This
* Don't directly modify the parent component's state from within the dialog. Use the "MatDialogRef.close()" method to return data.
* Don't pass complex objects or services directly into the "data" property. This can lead to dependency injection issues.
#### Why This Matters
* **Encapsulation:** Dialog component manages its own state, keeping concerns separate.
* **Testability:** Dialog component is easily testable in isolation.
* **Data Integrity:** Ensures data is validated before being passed back to the parent component.
### 3.2 Tables ("MatTable")
#### Standard
* Use "MatTableDataSource" to manage the data source for "MatTable".
* Use "MatSort" and "MatPaginator" to provide sorting and pagination functionality.
* Implement custom data access logic in a separate service.
* Leverage "trackBy" function for performance optimisation.
#### Do This
"""typescript
import { Component, OnInit, ViewChild } from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { MatPaginator } from '@angular/material/paginator';
import { UserService } from '../user.service'; // Assuming you have a UserService
export interface UserData {
id: string;
name: string;
progress: string;
fruit: string;
}
/** Constants used to fill up our data base. */
const FRUITS: string[] = [
'apple', 'orange', 'banana', 'melon', 'grape', 'kiwi', 'mango',
];
const NAMES: string[] = [
'Maia', 'Asher', 'Olivia', 'Atticus', 'Amelia', 'Jack',
'Charlotte', 'Theodore', 'Isla', 'Oliver', 'Isabella', 'Jasper',
'Cora', 'Levi', 'Violet', 'Arthur', 'Mia', 'Thomas',
'Elizabeth',
];
@Component({
selector: 'app-table-example',
styleUrls: ['table-example.css'],
templateUrl: 'table-example.html',
})
export class TableExample implements OnInit {
displayedColumns: string[] = ['id', 'name', 'progress', 'fruit'];
dataSource: MatTableDataSource;
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
constructor(private userService: UserService) {
// Create 100 users
const users = Array.from({length: 100}, (_, k) => createNewUser(k + 1));
// Assign the data to the data source for the table to render
this.dataSource = new MatTableDataSource(users);
}
ngOnInit() {
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();
}
}
}
/** Builds and returns a new User. */
function createNewUser(id: number): UserData {
const name = NAMES[Math.round(Math.random() * (NAMES.length - 1))] + ' ' +
NAMES[Math.round(Math.random() * (NAMES.length - 1))].charAt(0) + '.';
return {
id: id.toString(),
name: name,
progress: Math.round(Math.random() * 100).toString(),
fruit: FRUITS[Math.round(Math.random() * (FRUITS.length - 1))]
};
}
"""
"""html
Filter
ID
{{row.id}}
Name
{{row.name}}
Progress
{{row.progress}}%
Fruit
{{row.fruit}}
"""
#### Don't Do This
* Don't directly manipulate the DOM to update table data. Use "MatTableDataSource".
* Don't perform complex data transformations directly in the template. Do this at Service level.
#### Why This Matters
* **Performance:** "MatTableDataSource" optimizes data rendering and change detection.
* **Sorting and Pagination:** Provides built-in sorting and pagination functionality.
* **Maintainability:** Separates data access logic from the component.
### 3.3 Autocomplete ("MatAutocomplete")
#### Standard
* Use reactive forms to bind the autocomplete input to a data source asynchronously.
* Use the "displayWith" property to control how the selected option is displayed in the input field.
* Debounce input events to prevent excessive API calls.
#### Do This
"""typescript
import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { startWith, map, debounceTime } from 'rxjs/operators';
export interface User {
name: string;
}
@Component({
selector: 'app-autocomplete-example',
template: "
Select a user
{{user.name}}
"
})
export class AutocompleteExample implements OnInit {
userControl = new FormControl('');
users: User[] = [
{name: 'Alice'},
{name: 'Bob'},
{name: 'Charlie'}
];
filteredUsers$: Observable;
ngOnInit() {
this.filteredUsers$ = this.userControl.valueChanges.pipe(
startWith(''),
debounceTime(300), // Debounce for 300ms
map(value => (typeof value === 'string' ? value : value.name)),
map(name => (name ? this._filter(name) : this.users.slice())),
);
}
displayFn(user: User): string {
return user && user.name ? user.name : '';
}
private _filter(name: string): User[] {
const filterValue = name.toLowerCase();
return this.users.filter(user => user.name.toLowerCase().includes(filterValue));
}
}
"""
#### Don't Do This
* Don't perform synchronous filtering on large datasets. Use asynchronous filtering with API calls.
* Avoid making API calls on every input event. Use "debounceTime" to limit the number of calls.
#### Why This Matters
* **Asynchronous Data:** Handles large datasets
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'
# 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.
# Component Design Standards for Angular Material This document outlines component design standards for Angular Material projects. These guidelines promote reusable, maintainable, and performant Angular Material components, leveraging the latest framework features and architectural patterns. ## 1. Architectural Principles ### 1.1. Single Responsibility Principle (SRP) **Standard:** A component should have one, and only one, reason to change. Avoid creating "god components" that handle multiple unrelated responsibilities. **Do This:** Break down complex features into smaller, focused components. **Don't Do This:** Bundle unrelated logic (e.g., data fetching, form validation, UI presentation) within a single component. **Why:** SRP leads to improved testability, reusability, and lower maintenance costs due to reduced coupling. **Example:** Instead of a single "UserManagementComponent" that handles user creation, editing, and deletion, create three separate components: "UserCreateComponent", "UserEditComponent", and "UserDeleteComponent". """typescript // user-create.component.ts import { Component, EventEmitter, Output } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; @Component({ selector: 'app-user-create', templateUrl: './user-create.component.html', styleUrls: ['./user-create.component.scss'] }) export class UserCreateComponent { userForm: FormGroup; @Output() userCreated = new EventEmitter<any>(); constructor(private fb: FormBuilder) { this.userForm = this.fb.group({ username: ['', Validators.required], email: ['', [Validators.required, Validators.email]] }); } onSubmit() { if (this.userForm.valid) { this.userCreated.emit(this.userForm.value); } } } """ """html <!-- user-create.component.html --> <form [formGroup]="userForm" (ngSubmit)="onSubmit()"> <mat-form-field appearance="outline"> <mat-label>Username</mat-label> <input matInput placeholder="Username" formControlName="username"> <mat-error *ngIf="userForm.get('username')?.hasError('required')">Username is required</mat-error> </mat-form-field> <mat-form-field appearance="outline"> <mat-label>Email</mat-label> <input matInput placeholder="Email" formControlName="email"> <mat-error *ngIf="userForm.get('email')?.hasError('required')">Email is required</mat-error> <mat-error *ngIf="userForm.get('email')?.hasError('email')">Invalid email</mat-error> </mat-form-field> <button mat-raised-button color="primary" type="submit" [disabled]="!userForm.valid">Create User</button> </form> """ ### 1.2. Separation of Concerns (SoC) **Standard:** Divide the application into distinct sections, each addressing a separate concern. **Do This:** Isolate presentation logic (component templates), component logic (component class), and data access (services). **Don't Do This:** Mix UI logic directly within data services or perform database operations within component templates. **Why:** SoC improves code readability, testability, and maintainability by decoupling different parts of the application. **Example:** A "ProductListComponent" displays a list of products. The data fetching logic resides within a "ProductService". The component only concerns itself with displaying the products retrieved by the service. """typescript // product-list.component.ts import { Component, OnInit } from '@angular/core'; import { Product } from './product.model'; import { ProductService } from './product.service'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.scss'] }) export class ProductListComponent implements OnInit { products: Product[] = []; isLoading = true; constructor(private productService: ProductService) {} ngOnInit(): void { this.productService.getProducts().subscribe(products => { this.products = products; this.isLoading = false; }); } } """ """typescript // product.service.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Product } from './product.model'; @Injectable({ providedIn: 'root' }) export class ProductService { private apiUrl = '/api/products'; // Replace with your API endpoint constructor(private http: HttpClient) {} getProducts(): Observable<Product[]> { return this.http.get<Product[]>(this.apiUrl); } } """ ### 1.3. Don't Repeat Yourself (DRY) **Standard:** Avoid duplicating code. Extract common logic into reusable functions, services, or components. **Do This:** Create shared components, pipes, or services for functionality used in multiple parts of the application. **Don't Do This:** Copy and paste the same code block into multiple components. **Why:** DRY promotes code efficiency, minimizes errors, and simplifies maintenance. **Example:** Create a reusable "ConfirmationDialogComponent" for all confirmation prompts across the application. This way the dialog's logic and styling is centralized. """typescript // confirmation-dialog.component.ts import { Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; export interface ConfirmationDialogData { title: string; message: string; } @Component({ selector: 'app-confirmation-dialog', templateUrl: './confirmation-dialog.component.html', styleUrls: ['./confirmation-dialog.component.scss'] }) export class ConfirmationDialogComponent { constructor( public dialogRef: MatDialogRef<ConfirmationDialogComponent>, @Inject(MAT_DIALOG_DATA) public data: ConfirmationDialogData ) {} onNoClick(): void { this.dialogRef.close(false); } } """ """html <!-- confirmation-dialog.component.html --> <h1 mat-dialog-title>{{data.title}}</h1> <div mat-dialog-content> <p>{{data.message}}</p> </div> <div mat-dialog-actions> <button mat-button (click)="onNoClick()">No</button> <button mat-button [mat-dialog-close]="true" cdkFocusInitial>Yes</button> </div> """ ## 2. Component Structure ### 2.1. Folder Structure **Standard:** Organize components into logical folders based on feature or module. **Do This:** Create separate folders for different sections of the application (e.g., "/components/user", "/components/product"). **Don't Do This:** Place all components in a single flat directory. **Why:** A well-defined folder structure enhances project navigability and maintainability. **Example:** """ src/app/ ├── components/ │ ├── user/ │ │ ├── user-list/ │ │ │ ├── user-list.component.ts │ │ │ ├── user-list.component.html │ │ │ ├── user-list.component.scss │ │ ├── user.module.ts │ ├── product/ │ │ ├── product-details/ │ │ │ ├── product-details.component.ts │ │ │ ├── product-details.component.html │ │ │ ├── product-details.component.scss │ │ ├── product.module.ts """ ### 2.2. Component File Naming **Standard:** Follow a consistent naming convention for component files: "[component-name].component.[ts|html|scss]". **Do This:** Use hyphenated names for component selectors (e.g., "app-user-list"). **Don't Do This:** Use camelCase or PascalCase for selector names. **Why:** Consistent naming promotes code readability and reduces ambiguity. **Example:** * "user-list.component.ts" * "user-list.component.html" * "user-list.component.scss" ### 2.3. Component Class Naming **Standard:** Use PascalCase for component class names and append "Component" to the name. **Do This:** Name the "user-list" component as "UserListComponent". **Don't Do This:** Omit the "Component" suffix or use camelCase. **Why:** Consistent class naming improves code readability and maintainability. **Example:** """typescript // user-list.component.ts import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.scss'] }) export class UserListComponent implements OnInit { // ... } """ ## 3. Component Implementation ### 3.1. Input and Output Properties **Standard:** Use "@Input()" and "@Output()" decorators for passing data into and out of components. **Do This:** Clearly define the type of data being passed using TypeScript. **Don't Do This:** Rely on component state mutations for communication. **Why:** "@Input()" and "@Output()" create a clear, unidirectional data flow, which improves component reusability and testability. **Example:** """typescript // user-details.component.ts import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core'; import { User } from '../user.model'; @Component({ selector: 'app-user-details', templateUrl: './user-details.component.html', styleUrls: ['./user-details.component.scss'] }) export class UserDetailsComponent implements OnInit { @Input() user!: User; // User object passed in as input @Output() userDeleted = new EventEmitter<number>(); // Event emitted when a user is deleted ngOnInit() { if (!this.user) { throw new Error("User input is required!"); } } deleteUser() { this.userDeleted.emit(this.user.id); } } """ """html <!-- user-list.component.html (Parent Component) --> <app-user-details [user]="selectedUser" (userDeleted)="onUserDeleted($event)"></app-user-details> """ ### 3.2. Immutability **Standard:** Treat input properties as immutable within the component. Avoid modifying input values directly. **Do This:** Create local copies of the input data if modification is required. **Don't Do This:** Modify the original "@Input()" property directly. **Why:** Immutability prevents unexpected side effects and simplifies debugging. **Example:** """typescript // product-details.component.ts import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { Product } from '../product.model'; @Component({ selector: 'app-product-details', templateUrl: './product-details.component.html', styleUrls: ['./product-details.component.scss'] }) export class ProductDetailsComponent implements OnChanges { @Input() product!: Product; productCopy!: Product; // create a local copy ngOnChanges(changes: SimpleChanges): void { if (changes['product'] && this.product) { this.productCopy = { ...this.product }; // Create a shallow copy using the spread operator } } updatePrice(newPrice: number) { this.productCopy.price = newPrice; // Modify the local copy } } """ ### 3.3. Change Detection Strategy **Standard:** Use "ChangeDetectionStrategy.OnPush" for components that rely on input properties and immutable data. **Do This:** Configure components to only update when their input properties change. **Don't Do This:** Rely on default change detection for all components, which can lead to performance issues. **Why:** "OnPush" change detection optimizes performance by reducing the number of unnecessary component updates. **Example:** """typescript // display-user.component.ts import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; import { User } from '../user.model'; @Component({ selector: 'app-display-user', templateUrl: './display-user.component.html', styleUrls: ['./display-user.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush // Enable OnPush change detection }) export class DisplayUserComponent { @Input() user!: User; } """ ### 3.4. Lifecycle Hooks **Standard:** Use lifecycle hooks judiciously and only when necessary. **Do This:** Use "OnInit" for initialization logic, "OnChanges" for responding to input property changes and "OnDestroy" for cleanup (e.g., unsubscribing from observables). **Don't Do This:** Perform expensive operations in every lifecycle hook. **Why:** Proper use of lifecycle hooks optimizes component lifecycle management and avoids performance bottlenecks. **Example:** """typescript // data-table.component.ts import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { DataService } from '../data.service'; @Component({ selector: 'app-data-table', templateUrl: './data-table.component.html', styleUrls: ['./data-table.component.scss'] }) export class DataTableComponent implements OnInit, OnDestroy { data: any[] = []; private dataSubscription!: Subscription; constructor(private dataService: DataService) {} ngOnInit(): void { this.dataSubscription = this.dataService.getData().subscribe(data => { this.data = data; }); } ngOnDestroy(): void { if (this.dataSubscription) { this.dataSubscription.unsubscribe(); // Unsubscribe to prevent memory leaks } } } """ ### 3.5. Template Design **Standard:** Keep templates concise and readable. **Do This:** Use template variables ("#") and the "*ngIf", "*ngFor", and "*ngSwitch" directives for conditional rendering and iteration. **Don't Do This:** Embed complex logic directly within templates. **Why:** Concise templates improve readability and maintainability. **Example:** """html <!-- user-list.component.html --> <mat-list> <mat-list-item *ngFor="let user of users; let i = index"> <h3 mat-line> {{user.username}} </h3> <p mat-line> {{user.email}} </p> <button mat-icon-button (click)="selectUser(user)"> <mat-icon>edit</mat-icon> </button> <mat-divider *ngIf="i !== users.length - 1"></mat-divider> </mat-list-item> </mat-list> <div *ngIf="selectedUser"> <app-user-details [user]="selectedUser"></app-user-details> </div> """ ### 3.6. Angular Material Component Usage **Standard:** Utilize Angular Material components effectively and consistently. **Do This:** Follow Material Design guidelines for UI elements. Use the various "Mat" components (e.g., "MatButton", "MatCard", "MatTable"). Configure these components using their corresponding APIs (e.g., color properties, input configurations). **Don't Do This:** Reinvent the wheel by creating custom UI elements that duplicate Angular Material components. Misuse them by overriding too much of their default styling/behavior. **Why:** Leveraging Angular Material ensures a consistent, accessible, and performant user interface. """html <!-- product-card.component.html --> <mat-card class="product-card"> <mat-card-header> <mat-card-title>{{ product.name }}</mat-card-title> <mat-card-subtitle>{{ product.category }}</mat-card-subtitle> </mat-card-header> <img mat-card-image [src]="product.imageUrl" alt="{{ product.name }}"> <mat-card-content> <p>{{ product.description }}</p> </mat-card-content> <mat-card-actions> <button mat-button color="primary">Buy Now</button> <button mat-button>Learn More</button> </mat-card-actions> </mat-card> """ ### 3.7. Accessibility **Standard:** Ensure all components are accessible and comply with accessibility standards (WCAG). **Do This:** Use semantic HTML elements, provide ARIA attributes, and ensure proper keyboard navigation. Use accessibility testing tools. **Don't Do This:** Ignore accessibility considerations during component development. **Why:** Accessibility ensures that the application is usable by everyone, including users with disabilities. **Example:** """html <!-- accessible-button.component.html --> <button mat-raised-button color="primary" aria-label="Submit form" type="submit"> <mat-icon>send</mat-icon> Submit </button> <mat-form-field appearance="outline"> <mat-label>Search</mat-label> <input matInput placeholder="Search" aria-label="Search input"> <mat-icon matSuffix>search</mat-icon> </mat-form-field> """ ### 3.8 Styling **Standard:** Use SCSS for component styling. **Do This:** Use variables for colors, fonts, and spacing to ensure consistency. Use flexbox or grid layout for responsive design. Follow a BEM-like naming convention (Block, Element, Modifier) in class names. Encapsulate styles within the component using "ViewEncapsulation.Emulated" (default) or "ViewEncapsulation.ShadowDom". **Don't Do This:** Use inline styles or global styles that affect other components unexpectedly. Overspecify selectors. **Why:** Consistent styling improves the visual appeal and maintainability of the application. **Example:** """scss // _variables.scss (Shared variables file) $primary-color: #3f51b5; $accent-color: #e91e63; $font-family: 'Roboto', sans-serif; // user-list.component.scss @import 'variables'; .user-list { font-family: $font-family; &__item { // Element display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee; &--highlighted { // Modifier background-color: lighten($primary-color, 40%); } } } """ ## 4. Communication Patterns ### 4.1. Component Interaction **Standard:** Establish clear communication patterns between components. **Do This:** Use "@Input()" and "@Output()" for parent-child communication. For more complex scenarios, consider using a service with RxJS Subjects or a state management library like NgRx or Akita. **Don't Do This:** Pass data directly between unrelated components or rely on global state for simple interactions. **Why:** Well-defined communication patterns improve component decoupling and testability. **Example:** For cross-component communication, use a shared service with RxJS Subjects: """typescript // shared-data.service.ts import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; @Injectable({ providedIn: 'root' }) export class SharedDataService { private dataSubject = new Subject<any>(); data$ = this.dataSubject.asObservable(); setData(data: any) { this.dataSubject.next(data); } } // component-a.component.ts import { Component } from '@angular/core'; import { SharedDataService } from './shared-data.service'; @Component({ selector: 'app-component-a', template: '<button (click)="sendData()">Send Data</button>' }) export class ComponentAComponent { constructor(private sharedDataService: SharedDataService) {} sendData() { this.sharedDataService.setData({ message: 'Hello from Component A!' }); } } // component-b.component.ts import { Component, OnInit } from '@angular/core'; import { SharedDataService } from './shared-data.service'; @Component({ selector: 'app-component-b', template: '<div>Received Data: {{ data }}</div>' }) export class ComponentBComponent implements OnInit { data: any; constructor(private sharedDataService: SharedDataService) {} ngOnInit() { this.sharedDataService.data$.subscribe(data => { this.data = data; }); } } """ ## 5. Testing ### 5.1. Unit Testing **Standard:** Write unit tests for each component to verify its functionality. **Do This:** Use the Angular testing utilities ("TestBed", "ComponentFixture") to create and interact with components in a test environment. Mock dependencies (services) to isolate the component being tested. Test "@Input" and "@Output" functionality. **Don't Do This:** Skip unit tests or write tests that are too shallow (e.g., only checking that the component renders without errors). **Why:** Unit tests ensure that components function correctly and prevent regressions. **Example:** """typescript // user-list.component.spec.ts import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserListComponent } from './user-list.component'; import { UserService } from './user.service'; import { of } from 'rxjs'; describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture<UserListComponent>; let userService: UserService; beforeEach(async () => { const userServiceSpy = jasmine.createSpyObj('UserService', ['getUsers']); await TestBed.configureTestingModule({ declarations: [ UserListComponent ], providers: [ { provide: UserService, useValue: userServiceSpy } ] }) .compileComponents(); userService = TestBed.inject(UserService); userServiceSpy.getUsers.and.returnValue(of([{ id: 1, username: 'testuser', email: 'test@example.com' }])); fixture = TestBed.createComponent(UserListComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should load users on init', () => { expect(component.users.length).toBe(1); }); }); """ ## 6. Performance Optimization ### 6.1. Lazy Loading **Standard:** Lazy load modules and components that are not immediately required on application startup. **Do This:** Use Angular's lazy loading feature (using "loadChildren" in the routing configuration) to load modules on demand. **Don't Do This:** Load all modules upfront, which can significantly increase application startup time. **Why:** Lazy loading improves application startup time and reduces initial bundle size. **Example:** """typescript // app-routing.module.ts import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; const routes: Routes = [ { path: 'users', loadChildren: () => import('./components/user/user.module').then(m => m.UserModule) }, { path: 'products', loadChildren: () => import('./components/product/product.module').then(m => m.ProductModule) } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule {} """ ### 6.2. Virtualization **Standard:** Use Angular Material's virtualization capabilities ("cdk-virtual-scroll-viewport") for displaying large lists of data. **Do This:** Use "cdk-virtual-scroll-viewport" with "MatList" or "MatTable" to render only the visible items in the list. **Don't Do This:** Render all items in the list at once, which can lead to performance issues with large datasets. **Why:** Virtualization dramatically improves performance when rendering large lists by only rendering visible items. **Example:** """html <!-- large-list.component.html --> <cdk-virtual-scroll-viewport itemSize="50" class="example-viewport"> <mat-list-item *cdkVirtualFor="let item of items">{{item}}</mat-list-item> </cdk-virtual-scroll-viewport> """ """scss .example-viewport { height: 200px; width: 300px; border: 1px solid black; } """ ### 6.3. Avoid Unnecessary Re-renders **Standard:** Prevent unnecessary component re-renders. **Do This:** Implement "OnPush" change detection strategy, use "trackBy" function in "*ngFor" to minimize DOM updates. **Don't Do This:** Trigger change detection manually without a valid reason. **Why:** Reducing re-renders improves performance. """html <div *ngFor="let item of items; trackBy: trackByFn"> {{ item.name }} </div> """ """typescript trackByFn(index: number, item: any): any { return item.id; } """ ## 7. Security ### 7.1. Input Sanitization and Validation **Standard:** Sanitize and validate all user inputs to prevent Cross-Site Scripting (XSS) and other injection attacks. **Do This:** Use Angular's built-in sanitization features (e.g., "DomSanitizer") when displaying user-generated content. Use Angular Forms validators to validate user inputs. **Don't Do This:** Trust user inputs without validation or sanitization. **Why:** Proper sanitization and validation protect the application from security vulnerabilities. **Example:** """typescript // safe-html.component.ts import { Component, Input, SecurityContext } from '@angular/core'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @Component({ selector: 'app-safe-html', template: '<div [innerHTML]="safeHtml"></div>' }) export class SafeHtmlComponent { @Input() unsafeHtml!: string; safeHtml!: SafeHtml; constructor(private sanitizer: DomSanitizer) {} ngOnChanges(): void { this.safeHtml = this.sanitizer.sanitize(SecurityContext.HTML, this.unsafeHtml) || ''; } } """ ### 7.2. Authentication and Authorization **Standard:** Implement robust authentication and authorization mechanisms to protect sensitive data and functionality. **Do This:** Use Angular's "HttpClient" to communicate with a secure backend API. Implement role-based access control (RBAC) using guards in Angular Routing. **Don't Do This:** Store sensitive data in the client-side or implement authentication logic in the front end. **Why:** Proper authentication and authorization ensure that only authorized users can access specific parts of the application. This comprehensive guide provides a solid foundation for creating high-quality Angular Material components. Adhering to these standards will lead to more maintainable, performant, and secure applications. Remember to regularly review and update these guidelines to keep pace with the evolving Angular Material ecosystem.
# 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.
# Testing Methodologies Standards for Angular Material This document outlines the testing methodologies standards for Angular Material projects. It provides guidelines for writing effective unit, integration, and end-to-end tests to ensure the quality, stability, and maintainability of Angular Material applications. The focus is on modern approaches and patterns, leveraging the latest features of Angular and Angular Material. ## Unit Testing Unit tests isolate and test individual components, directives, pipes, and services in isolation. In the context of Angular Material, it’s crucial to verify that these parts of the application work as expected and interact correctly with Material components. ### Standards for Unit Testing Angular Material Components * **Do This:** Use "TestBed" to configure a testing module that mocks dependencies and provides a clean testing environment. * **Don't Do This:** Directly instantiate components or services without using "TestBed". This can lead to brittle tests that are tightly coupled to implementation details. **Why:** "TestBed" provides a consistent and reliable way to configure the Angular testing environment, simplifying dependency injection and component creation. **Example:** """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatButtonModule } from '@angular/material/button'; import { MyComponent } from './my.component'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('MyComponent', () => { let component: MyComponent; let fixture: ComponentFixture<MyComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyComponent ], imports: [ MatButtonModule, NoopAnimationsModule ] // Import necessary Material modules }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should display a button with the correct text', () => { const button = fixture.nativeElement.querySelector('button'); expect(button.textContent).toContain('Click me'); }); }); """ **Common Anti-Pattern:** * Forgetting to import the required Angular Material Modules into the Testing Module. Without importing "MatButtonModule", the "fixture.detectChanges()" call would throw an error. Also, include "NoopAnimationsModule" to prevent animation related issues. ### Testing Component Interactions with Material Components * **Do This:** Use "DebugElement" and "By" to query for specific Material components and elements within your component's template. * **Don't Do This:** Rely on fragile CSS selectors or hardcoded element IDs. **Why:** "DebugElement" and "By" provide a more robust and maintainable way to access elements in the DOM. **Example:** """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatInputModule } from '@angular/material/input'; import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; import { MyFormComponent } from './my-form.component'; import { FormsModule } from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('MyFormComponent', () => { let component: MyFormComponent; let fixture: ComponentFixture<MyFormComponent>; let inputElement: DebugElement; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyFormComponent ], imports: [ MatInputModule, FormsModule, NoopAnimationsModule ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyFormComponent); component = fixture.componentInstance; fixture.detectChanges(); inputElement = fixture.debugElement.query(By.css('input[matInput]')); }); it('should update the component property when the input value changes', () => { const newValue = 'test value'; inputElement.nativeElement.value = newValue; inputElement.nativeElement.dispatchEvent(new Event('input')); fixture.detectChanges(); expect(component.myInputValue).toBe(newValue); // myInputValue is a property bound to the input }); }); """ **Common Anti-Pattern:** * Not triggering change detection after an event. "fixture.detectChanges()" is crucial to update the component's view after "inputElement.nativeElement.dispatchEvent(new Event('input'))". ### Mocking Services with Angular Material * **Do This:** Mock services that interact with Angular Material components using spies and stubs. * **Don't Do This:** Rely on real service implementations in unit tests. **Why:** Mocking services allows you to isolate the component being tested and control the behavior of its dependencies. **Example:** """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatDialog } from '@angular/material/dialog'; import { MyDialogComponent } from './my-dialog.component'; import { MyComponent } from './my.component'; describe('MyComponent', () => { let component: MyComponent; let fixture: ComponentFixture<MyComponent>; let dialogSpy: jasmine.Spy; const dialogMock = { open: () => { return { afterClosed: () => { return { subscribe: () => {} } } } } }; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyComponent ], providers: [ { provide: MatDialog, useValue: dialogMock } ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyComponent); component = fixture.componentInstance; fixture.detectChanges(); dialogSpy = spyOn(TestBed.inject(MatDialog), 'open').and.callThrough(); }); it('should open a dialog when openDialog is called', () => { component.openDialog(); expect(dialogSpy).toHaveBeenCalled(); }); }); """ **Common Anti-Pattern:** * Not spying on the "open" method and verifying it was called. The "dialogSpy" ensures that our test verifies the key behaviour (opening the dialog). ### Testing Angular Material Theming * **Do This:** Verify that the component is correctly themed based on your application's theme configuration. Use CSS class assertions to check for theme styles being applied. * **Don't Do This:** Visual testing is not generally part of unit testing. Instead, focus on verifying the correct classes are added to an element. """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatButtonModule } from '@angular/material/button'; import { MyThemedComponent } from './my-themed.component'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ThemePalette } from '@angular/material/core'; describe('MyThemedComponent', () => { let component: MyThemedComponent; let fixture: ComponentFixture<MyThemedComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyThemedComponent ], imports: [ MatButtonModule, NoopAnimationsModule ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyThemedComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should apply the primary theme color', () => { const button = fixture.nativeElement.querySelector('.mat-mdc-button'); expect(button.classList).toContain('mat-primary'); }); it('should set color property of button to primary',()=>{ component.color = 'primary'; fixture.detectChanges(); const button = fixture.nativeElement.querySelector('.mat-mdc-button'); expect(button.classList).toContain('mat-primary'); }) }); """ **Common Anti-Pattern:** * Not checking the generated CSS class, and checking a computed style instead. Checking the class provides better confidence that the theme is configured correctly, not just the end styling. ## Integration Testing Integration tests verify the interaction between different parts of the application, usually multiple components or services working together. For Angular Material application, this involves testing how components integrate with data services, state management, and other parts of the system. ### Standards for Integration Testing Angular Material Modules * **Do This:** Use a more comprehensive "TestBed" configuration that includes multiple components, services, and modules. * **Don't Do This:** Isolate components too much. The goal is to test their interaction in a more realistic scenario. **Why:** Integration tests reveal integration issues that unit tests might miss. **Example:** """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatTableModule } from '@angular/material/table'; import { MatPaginatorModule } from '@angular/material/paginator'; import { DataSource } from '@angular/cdk/collections'; import { BehaviorSubject, Observable } from 'rxjs'; import { MyTableComponent } from './my-table.component'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; interface PeriodicElement { name: string; position: number; weight: number; symbol: string; } const ELEMENT_DATA: PeriodicElement[] = [ {position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'}, {position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}, {position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'}, ]; class ExampleDataSource extends DataSource<PeriodicElement> { private data = new BehaviorSubject<PeriodicElement[]>(ELEMENT_DATA); connect(): Observable<PeriodicElement[]> { return this.data; } disconnect() {} } describe('MyTableComponent', () => { let component: MyTableComponent; let fixture: ComponentFixture<MyTableComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyTableComponent ], imports: [ MatTableModule, MatPaginatorModule, NoopAnimationsModule ], providers: [ { provide: DataSource, useClass: ExampleDataSource }] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyTableComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should display rows of data', () => { const tableRows = fixture.nativeElement.querySelectorAll('mat-row'); expect(tableRows.length).toBe(ELEMENT_DATA.length); }); }); """ **Common Anti-Pattern:** Oversimplifying the data source. The data source used provides the data being represented within the table. If you oversimplify it, you are effectively unit testing the table, not integration testing the data binding. ### Testing Data Binding with Angular Material Components * **Do This:** Ensure that data is correctly bound to Angular Material components and that changes in the data are reflected in the UI. * **Don't Do This:** Only test static data. Test the full lifecycle of bound properties. **Why:** Data binding is a core feature of Angular, and it’s essential to verify that it works correctly with Angular Material. **Example:** """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MatListModule } from '@angular/material/list'; import { MyListComponent } from './my-list.component'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('MyListComponent', () => { let component: MyListComponent; let fixture: ComponentFixture<MyListComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyListComponent ], imports: [ MatListModule, NoopAnimationsModule ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyListComponent); component = fixture.componentInstance; component.items = ['Item 1', 'Item 2', 'Item 3']; // Initialize Items fixture.detectChanges(); }); it('should display the correct number of list items', () => { const listItems = fixture.nativeElement.querySelectorAll('mat-list-item'); expect(listItems.length).toBe(component.items.length); }); it('should update the list when items are added', () => { component.items.push('Item 4'); fixture.detectChanges(); const listItems = fixture.nativeElement.querySelectorAll('mat-list-item'); expect(listItems.length).toBe(4); }); }); """ **Common Anti-Pattern:** * Forgetting to call "fixture.detectChanges()" after changes to the component's properties. ### Testing Angular Material Forms * **Do This:** Test the validation of form elements and the submission of form data. * **Don't Do This:** Only test the UI. Make sure the submitted values are correct. **Why:** Angular Material provides a rich set of form controls, and it’s important to ensure that they work correctly with Angular forms. **Example:** """typescript import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule, FormControl } from '@angular/forms'; import { MatInputModule } from '@angular/material/input'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MyFormComponent } from './my-form.component'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; describe('MyFormComponent', () => { let component: MyFormComponent; let fixture: ComponentFixture<MyFormComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ MyFormComponent ], imports: [ ReactiveFormsModule, MatInputModule, MatFormFieldModule, NoopAnimationsModule ] }) .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(MyFormComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should validate the required field', () => { component.formGroup.get('name')?.setValue(''); expect(component.formGroup.get('name')?.valid).toBeFalsy(); }); it('should submit the form with valid data', () => { component.formGroup.get('name')?.setValue('Test Name'); component.onSubmit(); expect(component.submittedData).toEqual({ name: 'Test Name' }); // Assuming you store submitted data in the component. }); }); """ **Common Anti-Pattern:** * Skipping validation tests. Ensure that all validations you declared are tested. ## End-to-End (E2E) Testing E2E tests simulate real user interactions with the application, verifying that the entire system works correctly from the user's perspective. Frameworks like Cypress and Playwright are commonly used. ### Standards for E2E Testing Angular Material Applications * **Do This:** Focus on testing critical user flows and interactions with Angular Material components. * **Don't Do This:** Attempt to test every single UI element. Prioritize the most important workflows. **Why:** E2E tests provide the highest level of confidence that the application works correctly in a real-world environment. Example using Cypress: """typescript // cypress/e2e/spec.cy.ts describe('Angular Material App E2E Tests', () => { it('should navigate to the login page and log in', () => { cy.visit('/login'); // Assuming your login page is at /login cy.get('input[formControlName="username"]').type('testuser'); cy.get('input[formControlName="password"]').type('password123'); cy.get('button[type="submit"]').click(); // Assuming you have a submit button // Assert that the application navigates to the dashboard after login cy.url().should('include', '/dashboard'); }); it('should open a dialog using mat-dialog', () => { cy.visit('/somepage'); // Navigate to page where you open dialog cy.get('button#openDialogButton').click(); // Replace with your actual button selector // Assert that material dialog is visible. cy.get('.mat-dialog-container').should('be.visible'); // You can further interact with the dialog here if needed. cy.get('button#confirmButton').click(); // Example: click confirmation button }); it('should display data in a mat-table', () => { cy.visit('/datatable'); // Navigate to page with data table // Assert certain rows or values being displayed with assertions based on your data. cy.get('mat-row').should('have.length.greaterThan', 0); cy.get('mat-cell').contains('Some Data'); // Check for sample data }); }); """ **Common Anti-Pattern:** * Writing overly specific tests that break with minor UI changes. Use data attributes or ARIA roles to target elements instead of brittle CSS selectors. ### Testing Accessibility with Angular Material * **Do This:** Use tools like axe-core to automatically check for accessibility issues in E2E tests. * **Don't Do This:** Neglect accessibility testing. Angular Material is designed to be accessible, but it’s important to verify that your application is using it correctly. **Why:** Accessibility is critical for ensuring that your application is usable by everyone. ### Performance Testing Considerations * **Do This:** Measure rendering performance of Material components, especially in scenarios with large datasets or complex interactions. * **Don't Do This:** Ignore performance until late in the development cycle. **Why:** Angular Material provides high performing components but its usage needs to be understood, especially with larger apps. **Example (Cypress):** """typescript // cypress/e2e/performance.cy.ts describe('Performance Tests', () => { it('should load a table with 1000 rows within acceptable time limits', () => { cy.visit('/large-table'); const startTime = performance.now(); cy.get('mat-row', { timeout: 10000 }).should('have.length', 1000).then(() => { const endTime = performance.now(); const loadTime = endTime - startTime; cy.log("Table loaded in ${loadTime}ms"); expect(loadTime).to.be.lessThan(5000); // Maximum acceptable loading time (adjust as needed) }); }); }); """ **Common Anti-Pattern:** * Using excessive amounts of data without pagination. Implement pagination, virtualization, or other techniques to optimize performance with large datasets. * Heavy calculations are being done directly in the template, affecting rendering duration. ## General Testing Principles * **Test Driven Development (TDD):** Writing tests before writing code can help drive the design and ensure that the code is testable. * **Behavior Driven Development (BDD):** Focusing on the behavior of the application from the user's perspective can help ensure that the tests are relevant and valuable. * **Code Coverage:** Aim for high code coverage, but don’t sacrifice quality for quantity. Focus on testing critical code paths and scenarios. * **Continuous Integration (CI):** Integrate tests into your CI/CD pipeline to automatically run tests on every commit and pull request. By following these guidelines, you can ensure that your Angular Material applications are well-tested, maintainable, and reliable.
# 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.